Comparing version 3.4.1 to 3.5.0
[English](./CHANGELOG.md) | 简体中文 | ||
#### 3.5.0 (2021-04-28) | ||
- `Feature(Log)` 新增复制单行日志的能力。 (by @akai) | ||
- `Feature(Plugin)` 新第三方插件 [vconsole-vue-devtools-plugin](https://github.com/Zippowxk/vue-vconsole-devtools)。 (by @Zippowxk) | ||
- `Perf(System)` 将 "System" 字段改名为 "Client",并新增 `MacOS` 系统版本号。 | ||
- `Fix(Log)` 使用自然排序来排序 object 和 array 的键值。 (issue #372) | ||
- `Fix(Network)` 修复当 `contentType` 为 `text/html` 时潜在的 JSON 错误。 (by @zimv) | ||
- `Fix(Network)` 修复 `disableLogScrolling` 参数在 Network 面板中不生效的问题。 (issue #282, #379) | ||
#### V3.4.1 (2021-04-09) | ||
@@ -4,0 +14,0 @@ |
English | [简体中文](./CHANGELOG_CN.md) | ||
#### 3.5.0 (2021-04-28) | ||
- `Feature(Log)` Add ability to copy a single line of logs. (by @akai) | ||
- `Feature(Plugin)` New third-party plugin [vconsole-vue-devtools-plugin](https://github.com/Zippowxk/vue-vconsole-devtools). (by @Zippowxk) | ||
- `Perf(System)` Rename "System" field to "Client", and add `MacOS` version. | ||
- `Fix(Log)` Use natural sorting to sort object and array's keys. (issue #372) | ||
- `Fix(Network)` Fix JSON parse error when `contentType` is `text/html`. (by @zimv) | ||
- `Fix(Network)` Fix `disableLogScrolling` not working in Network panel. (issue #282, #379) | ||
#### V3.4.1 (2021-04-09) | ||
@@ -22,3 +32,3 @@ | ||
- `Feature(Network)` Use short URL and display parameters in Network tab. (issue #291) | ||
- `Feature(Plugin)` New plugin [vconsole-stats-plugin](https://github.com/smackgg/vConsole-Stats). (by @smackgg) | ||
- `Feature(Plugin)` New third-party plugin [vconsole-stats-plugin](https://github.com/smackgg/vConsole-Stats). (by @smackgg) | ||
- `Fix(General)` The position of the switch button will be reset by mistake when clicked. | ||
@@ -25,0 +35,0 @@ - `Fix(General)` Fix `document.documentElement.offsetHeight|offsetWidth` is unreliable in newer browsers. (by @littlee) |
{ | ||
"name": "vconsole", | ||
"version": "3.4.1", | ||
"version": "3.5.0", | ||
"description": "A lightweight, extendable front-end developer tool for mobile web page.", | ||
@@ -9,3 +9,2 @@ "homepage": "https://github.com/Tencent/vConsole", | ||
"scripts": { | ||
"test": "mocha", | ||
"build": "webpack", | ||
@@ -24,14 +23,15 @@ "dev": "webpack-dev-server --config webpack.dev.config" | ||
"dependencies": { | ||
"core-js": "^3.8.3", | ||
"copy-text-to-clipboard": "^3.0.1", | ||
"core-js": "^3.11.0", | ||
"mutation-observer": "^1.0.3" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.12.10", | ||
"@babel/plugin-proposal-class-properties": "^7.12.1", | ||
"@babel/plugin-proposal-export-namespace-from": "^7.12.1", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1", | ||
"@babel/preset-env": "^7.12.11", | ||
"@babel/core": "^7.13.16", | ||
"@babel/plugin-proposal-class-properties": "^7.13.0", | ||
"@babel/plugin-proposal-export-namespace-from": "^7.12.13", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.13.8", | ||
"@babel/preset-env": "^7.13.15", | ||
"babel-loader": "^8.2.2", | ||
"babel-plugin-add-module-exports": "^1.0.4", | ||
"chai": "^4.2.0", | ||
"chai": "^4.3.4", | ||
"copy-webpack-plugin": "^5.1.2", | ||
@@ -44,7 +44,6 @@ "css-loader": "^3.6.0", | ||
"less-loader": "^5.0.0", | ||
"mocha": "^5.2.0", | ||
"style-loader": "^1.3.0", | ||
"webpack": "^4.44.2", | ||
"webpack": "^4.46.0", | ||
"webpack-cli": "^3.3.12", | ||
"webpack-dev-server": "^3.11.0", | ||
"webpack-dev-server": "^3.11.2", | ||
"webpack-merge": "^4.2.2" | ||
@@ -51,0 +50,0 @@ }, |
@@ -77,3 +77,3 @@ [English](./README.md) | 简体中文 | ||
## 插件列表 | ||
## 第三方插件列表 | ||
@@ -83,2 +83,4 @@ - [vConsole-sources](https://github.com/WechatFE/vConsole-sources) | ||
- [vconsole-stats-plugin](https://github.com/smackgg/vConsole-Stats) | ||
- [vconsole-vue-devtools-plugin](https://github.com/Zippowxk/vue-vconsole-devtools) | ||
- [vconsole-outputlog-plugin](https://github.com/sunlanda/vconsole-outputlog-plugin) | ||
@@ -85,0 +87,0 @@ ## 更新记录 |
@@ -74,3 +74,3 @@ English | [简体中文](./README_CN.md) | ||
## Plugins | ||
## Third-party Plugins | ||
@@ -80,2 +80,4 @@ - [vConsole-sources](https://github.com/WechatFE/vConsole-sources) | ||
- [vconsole-stats-plugin](https://github.com/smackgg/vConsole-Stats) | ||
- [vconsole-vue-devtools-plugin](https://github.com/Zippowxk/vue-vconsole-devtools) | ||
- [vconsole-outputlog-plugin](https://github.com/sunlanda/vconsole-outputlog-plugin) | ||
@@ -82,0 +84,0 @@ ## Changelog |
1558
src/core/core.js
@@ -16,786 +16,774 @@ /* | ||
import 'transitionEnd' | ||
import pkg from '../../package.json'; | ||
import * as tool from '../lib/tool.js'; | ||
import $ from '../lib/query.js'; | ||
import './core.less'; | ||
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'; | ||
// built-in plugins | ||
import VConsolePlugin from '../lib/plugin.js'; | ||
import VConsoleLogPlugin from '../log/log.js'; | ||
import VConsoleDefaultPlugin from '../log/default.js'; | ||
import VConsoleSystemPlugin from '../log/system.js'; | ||
import VConsoleNetworkPlugin from '../network/network.js'; | ||
import VConsoleElementPlugin from '../element/element.js'; | ||
import VConsoleStoragePlugin from '../storage/storage.js'; | ||
const VCONSOLE_ID = '#__vconsole'; | ||
class VConsole { | ||
constructor(opt) { | ||
if (!!$.one(VCONSOLE_ID)) { | ||
console.debug('vConsole is already exists.'); | ||
return; | ||
} | ||
let that = this; | ||
this.version = pkg.version; | ||
this.$dom = null; | ||
this.isInited = false; | ||
this.option = { | ||
defaultPlugins: ['system', 'network', 'element', 'storage'] | ||
}; | ||
this.activedTab = ''; | ||
this.tabList = []; | ||
this.pluginList = {}; | ||
this.switchPos = { | ||
hasMoved: false, // exclude click event | ||
x: 0, // right | ||
y: 0, // bottom | ||
startX: 0, | ||
startY: 0, | ||
endX: 0, | ||
endY: 0 | ||
}; | ||
// export helper functions to public | ||
this.tool = tool; | ||
this.$ = $; | ||
// merge options | ||
if (tool.isObject(opt)) { | ||
for (let key in opt) { | ||
this.option[key] = opt[key]; | ||
} | ||
} | ||
// add built-in plugins | ||
this._addBuiltInPlugins(); | ||
// try to init | ||
let _onload = function() { | ||
if (that.isInited) { | ||
return; | ||
} | ||
that._render(); | ||
that._mockTap(); | ||
that._bindEvent(); | ||
that._autoRun(); | ||
}; | ||
if (document !== undefined) { | ||
if (document.readyState === 'loading') { | ||
$.bind(window, 'DOMContentLoaded', _onload); | ||
} else { | ||
_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); | ||
} | ||
} | ||
/** | ||
* add built-in plugins | ||
*/ | ||
_addBuiltInPlugins() { | ||
// add default log plugin | ||
this.addPlugin(new VConsoleDefaultPlugin('default', 'Log')); | ||
// add other built-in plugins according to user's config | ||
const list = this.option.defaultPlugins; | ||
const plugins = { | ||
'system': {proto: VConsoleSystemPlugin, name: 'System'}, | ||
'network': {proto: VConsoleNetworkPlugin, name: 'Network'}, | ||
'element': {proto: VConsoleElementPlugin, name: 'Element'}, | ||
'storage': {proto: VConsoleStoragePlugin, name: 'Storage'} | ||
}; | ||
if (!!list && tool.isArray(list)) { | ||
for (let i=0; i<list.length; i++) { | ||
let tab = plugins[list[i]]; | ||
if (!!tab) { | ||
this.addPlugin(new tab.proto(list[i], tab.name)); | ||
} else { | ||
console.debug('Unrecognized default plugin ID:', list[i]); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* render panel DOM | ||
* @private | ||
*/ | ||
_render() { | ||
if (! $.one(VCONSOLE_ID)) { | ||
const e = document.createElement('div'); | ||
e.innerHTML = tpl; | ||
document.documentElement.insertAdjacentElement('beforeend', e.children[0]); | ||
} | ||
this.$dom = $.one(VCONSOLE_ID); | ||
// reposition switch button | ||
const $switch = $.one('.vc-switch', this.$dom); | ||
let switchX = tool.getStorage('switch_x') * 1, | ||
switchY = tool.getStorage('switch_y') * 1; | ||
this.setSwitchPosition(switchX, switchY); | ||
// modify font-size | ||
const dpr = window.devicePixelRatio || 1; | ||
const viewportEl = document.querySelector('[name="viewport"]'); | ||
if (viewportEl && viewportEl.content) { | ||
const initialScale = viewportEl.content.match(/initial\-scale\=\d+(\.\d+)?/); | ||
const scale = initialScale ? parseFloat(initialScale[0].split('=')[1]) : 1; | ||
if (scale < 1) { | ||
this.$dom.style.fontSize = 13 * dpr + 'px'; | ||
} | ||
} | ||
// remove from less to present transition effect | ||
$.one('.vc-mask', this.$dom).style.display = 'none'; | ||
// set theme | ||
this._updateTheme(); | ||
} | ||
/** | ||
* Update theme | ||
* @private | ||
*/ | ||
_updateTheme() { | ||
const theme = this.option.theme || 'light'; | ||
this.$dom.setAttribute('data-theme', theme); | ||
} | ||
setSwitchPosition(switchX, switchY) { | ||
const $switch = $.one('.vc-switch', this.$dom); | ||
[switchX, switchY] = this._getSwitchButtonSafeAreaXY($switch, switchX, switchY); | ||
this.switchPos.x = switchX; | ||
this.switchPos.y = switchY; | ||
$switch.style.right = switchX + 'px'; | ||
$switch.style.bottom = switchY + 'px'; | ||
tool.setStorage('switch_x', switchX); | ||
tool.setStorage('switch_y', switchY); | ||
} | ||
/** | ||
* Get an safe [x, y] position for switch button | ||
* @private | ||
*/ | ||
_getSwitchButtonSafeAreaXY($switch, x, y) { | ||
const docWidth = Math.max(document.documentElement.offsetWidth, window.innerWidth); | ||
const docHeight = Math.max(document.documentElement.offsetHeight, window.innerHeight); | ||
// check edge | ||
if (x + $switch.offsetWidth > docWidth) { | ||
x = docWidth - $switch.offsetWidth; | ||
} | ||
if (y + $switch.offsetHeight > docHeight) { | ||
y = docHeight - $switch.offsetHeight; | ||
} | ||
if (x < 0) { x = 0; } | ||
if (y < 20) { y = 20; } // safe area for iOS Home indicator | ||
return [x, y]; | ||
} | ||
/** | ||
* Get an safe [x, y] position for switch button | ||
* @private | ||
*/ | ||
_getSwitchButtonSafeAreaXY($switch, x, y) { | ||
const docWidth = Math.max(document.documentElement.offsetWidth, window.innerWidth); | ||
const docHeight = Math.max(document.documentElement.offsetHeight, window.innerHeight); | ||
// check edge | ||
if (x + $switch.offsetWidth > docWidth) { | ||
x = docWidth - $switch.offsetWidth; | ||
} | ||
if (y + $switch.offsetHeight > docHeight) { | ||
y = docHeight - $switch.offsetHeight; | ||
} | ||
if (x < 0) { x = 0; } | ||
if (y < 20) { y = 20; } // safe area for iOS Home indicator | ||
return [x, y]; | ||
} | ||
/** | ||
* 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 if (typeof window.getSelection !== 'function' || !getSelection().rangeCount) { | ||
e.preventDefault(); // prevent click 300ms later | ||
} | ||
if (!targetElem.disabled && !targetElem.readOnly) { | ||
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() { | ||
const that = this; | ||
// drag & drop switch button | ||
const $switch = $.one('.vc-switch', that.$dom); | ||
$.bind($switch, 'touchstart', function(e) { | ||
that.switchPos.startX = e.touches[0].pageX; | ||
that.switchPos.startY = e.touches[0].pageY; | ||
that.switchPos.hasMoved = false; | ||
}); | ||
$.bind($switch, 'touchend', function(e) { | ||
if (!that.switchPos.hasMoved) { | ||
return; | ||
} | ||
that.switchPos.startX = 0; | ||
that.switchPos.startY = 0; | ||
that.switchPos.hasMoved = false; | ||
that.setSwitchPosition(that.switchPos.endX, that.switchPos.endY); | ||
}); | ||
$.bind($switch, 'touchmove', function(e) { | ||
if (e.touches.length <= 0) { | ||
return; | ||
} | ||
const offsetX = e.touches[0].pageX - that.switchPos.startX, | ||
offsetY = e.touches[0].pageY - that.switchPos.startY; | ||
let x = Math.floor(that.switchPos.x - offsetX), | ||
y = Math.floor(that.switchPos.y - offsetY); | ||
[x, y] = that._getSwitchButtonSafeAreaXY($switch, x, y); | ||
$switch.style.right = x + 'px'; | ||
$switch.style.bottom = y + 'px'; | ||
that.switchPos.endX = x; | ||
that.switchPos.endY = y; | ||
that.switchPos.hasMoved = true; | ||
e.preventDefault(); | ||
}); | ||
// 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 | ||
let $mask = $.one('.vc-mask', this.$dom); | ||
let $panel = $.one('.vc-panel', this.$dom); | ||
const transitionEnd = window.transitionEnd($mask).whichTransitionEnd() | ||
const onMaskTransitionEnd = function() { | ||
$mask.style.display = 'none'; | ||
$panel.style.display = 'none'; | ||
}; | ||
if (transitionEnd) { | ||
$.bind($mask, transitionEnd, onMaskTransitionEnd); | ||
} else { | ||
onMaskTransitionEnd(); | ||
} | ||
$.bind($mask, '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); | ||
}); | ||
// 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.isInited = 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]); | ||
} | ||
this.triggerEvent('ready'); | ||
} | ||
/** | ||
* trigger a vConsole.option event | ||
* @protect | ||
*/ | ||
triggerEvent(eventName, param) { | ||
eventName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1); | ||
if (tool.isFunction(this.option[eventName])) { | ||
this.option[eventName].apply(this, param); | ||
} | ||
} | ||
/** | ||
* init a plugin | ||
* @private | ||
*/ | ||
_initPlugin(plugin) { | ||
let that = this; | ||
plugin.vConsole = 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', that.$dom); | ||
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.isReady = true; | ||
plugin.trigger('ready'); | ||
} | ||
/** | ||
* trigger an event for each plugin | ||
* @private | ||
*/ | ||
_triggerPluginsEvent(eventName) { | ||
for (let id in this.pluginList) { | ||
if (this.pluginList[id].isReady) { | ||
this.pluginList[id].trigger(eventName); | ||
} | ||
} | ||
} | ||
/** | ||
* trigger an event by plugin's name | ||
* @private | ||
*/ | ||
_triggerPluginEvent(pluginName, eventName) { | ||
let plugin = this.pluginList[pluginName]; | ||
if (!!plugin && plugin.isReady) { | ||
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.debug('Plugin ' + plugin.id + ' has already been added.'); | ||
return false; | ||
} | ||
this.pluginList[plugin.id] = plugin; | ||
// init plugin only if vConsole is ready | ||
if (this.isInited) { | ||
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.debug('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.isInited) { | ||
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() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
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 panel | ||
* @public | ||
*/ | ||
hide() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
$.removeClass(this.$dom, 'vc-toggle'); | ||
setTimeout(() => { | ||
// panel will be hidden by CSS transition in 0.3s | ||
$.one('.vc-mask', this.$dom).style.display = 'none'; | ||
$.one('.vc-panel', this.$dom).style.display = 'none'; | ||
}, 330); | ||
this._triggerPluginsEvent('hideConsole'); | ||
} | ||
/** | ||
* show switch button | ||
* @public | ||
*/ | ||
showSwitch() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
let $switch = $.one('.vc-switch', this.$dom); | ||
$switch.style.display = 'block'; | ||
} | ||
/** | ||
* hide switch button | ||
*/ | ||
hideSwitch() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
let $switch = $.one('.vc-switch', this.$dom); | ||
$switch.style.display = 'none'; | ||
} | ||
/** | ||
* show a tab | ||
* @public | ||
*/ | ||
showTab(tabID) { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
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.activedTab && this._triggerPluginEvent(this.activedTab, 'hide'); | ||
this.activedTab = tabID; | ||
this._triggerPluginEvent(this.activedTab, 'show'); | ||
} | ||
/** | ||
* update option(s) | ||
* @public | ||
*/ | ||
setOption(keyOrObj, value) { | ||
if (tool.isString(keyOrObj)) { | ||
this.option[keyOrObj] = value; | ||
this._triggerPluginsEvent('updateOption'); | ||
this._updateTheme(); | ||
} else if (tool.isObject(keyOrObj)) { | ||
for (let k in keyOrObj) { | ||
this.option[k] = keyOrObj[k]; | ||
} | ||
this._triggerPluginsEvent('updateOption'); | ||
this._updateTheme(); | ||
} else { | ||
console.debug('The first parameter of vConsole.setOption() must be a string or an object.'); | ||
} | ||
} | ||
/** | ||
* uninstall vConsole | ||
* @public | ||
*/ | ||
destroy() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
// remove plugins | ||
let IDs = Object.keys(this.pluginList); | ||
for (let i = IDs.length - 1; i >= 0; i--) { | ||
this.removePlugin(IDs[i]); | ||
} | ||
// remove DOM | ||
this.$dom.parentNode.removeChild(this.$dom); | ||
// reverse isInited when destroyed | ||
this.isInited = false; | ||
} | ||
} // END class | ||
// export static class | ||
VConsole.VConsolePlugin = VConsolePlugin; | ||
VConsole.VConsoleLogPlugin = VConsoleLogPlugin; | ||
VConsole.VConsoleDefaultPlugin = VConsoleDefaultPlugin; | ||
VConsole.VConsoleSystemPlugin = VConsoleSystemPlugin; | ||
VConsole.VConsoleNetworkPlugin = VConsoleNetworkPlugin; | ||
VConsole.VConsoleElementPlugin = VConsoleElementPlugin; | ||
VConsole.VConsoleStoragePlugin = VConsoleStoragePlugin; | ||
export default VConsole; | ||
import pkg from '../../package.json'; | ||
import * as tool from '../lib/tool.js'; | ||
import $ from '../lib/query.js'; | ||
import './core.less'; | ||
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'; | ||
// built-in plugins | ||
import VConsolePlugin from '../lib/plugin.js'; | ||
import VConsoleLogPlugin from '../log/log.js'; | ||
import VConsoleDefaultPlugin from '../log/default.js'; | ||
import VConsoleSystemPlugin from '../log/system.js'; | ||
import VConsoleNetworkPlugin from '../network/network.js'; | ||
import VConsoleElementPlugin from '../element/element.js'; | ||
import VConsoleStoragePlugin from '../storage/storage.js'; | ||
const VCONSOLE_ID = '#__vconsole'; | ||
class VConsole { | ||
constructor(opt) { | ||
if (!!$.one(VCONSOLE_ID)) { | ||
console.debug('vConsole is already exists.'); | ||
return; | ||
} | ||
let that = this; | ||
this.version = pkg.version; | ||
this.$dom = null; | ||
this.isInited = false; | ||
this.option = { | ||
defaultPlugins: ['system', 'network', 'element', 'storage'] | ||
}; | ||
this.activedTab = ''; | ||
this.tabList = []; | ||
this.pluginList = {}; | ||
this.switchPos = { | ||
hasMoved: false, // exclude click event | ||
x: 0, // right | ||
y: 0, // bottom | ||
startX: 0, | ||
startY: 0, | ||
endX: 0, | ||
endY: 0 | ||
}; | ||
// export helper functions to public | ||
this.tool = tool; | ||
this.$ = $; | ||
// merge options | ||
if (tool.isObject(opt)) { | ||
for (let key in opt) { | ||
this.option[key] = opt[key]; | ||
} | ||
} | ||
// add built-in plugins | ||
this._addBuiltInPlugins(); | ||
// try to init | ||
let _onload = function() { | ||
if (that.isInited) { | ||
return; | ||
} | ||
that._render(); | ||
that._mockTap(); | ||
that._bindEvent(); | ||
that._autoRun(); | ||
}; | ||
if (document !== undefined) { | ||
if (document.readyState === 'loading') { | ||
$.bind(window, 'DOMContentLoaded', _onload); | ||
} else { | ||
_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); | ||
} | ||
} | ||
/** | ||
* add built-in plugins | ||
*/ | ||
_addBuiltInPlugins() { | ||
// add default log plugin | ||
this.addPlugin(new VConsoleDefaultPlugin('default', 'Log')); | ||
// add other built-in plugins according to user's config | ||
const list = this.option.defaultPlugins; | ||
const plugins = { | ||
'system': {proto: VConsoleSystemPlugin, name: 'System'}, | ||
'network': {proto: VConsoleNetworkPlugin, name: 'Network'}, | ||
'element': {proto: VConsoleElementPlugin, name: 'Element'}, | ||
'storage': {proto: VConsoleStoragePlugin, name: 'Storage'} | ||
}; | ||
if (!!list && tool.isArray(list)) { | ||
for (let i=0; i<list.length; i++) { | ||
let tab = plugins[list[i]]; | ||
if (!!tab) { | ||
this.addPlugin(new tab.proto(list[i], tab.name)); | ||
} else { | ||
console.debug('Unrecognized default plugin ID:', list[i]); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* render panel DOM | ||
* @private | ||
*/ | ||
_render() { | ||
if (! $.one(VCONSOLE_ID)) { | ||
const e = document.createElement('div'); | ||
e.innerHTML = tpl; | ||
document.documentElement.insertAdjacentElement('beforeend', e.children[0]); | ||
} | ||
this.$dom = $.one(VCONSOLE_ID); | ||
// reposition switch button | ||
const $switch = $.one('.vc-switch', this.$dom); | ||
let switchX = tool.getStorage('switch_x') * 1, | ||
switchY = tool.getStorage('switch_y') * 1; | ||
this.setSwitchPosition(switchX, switchY); | ||
// modify font-size | ||
const dpr = window.devicePixelRatio || 1; | ||
const viewportEl = document.querySelector('[name="viewport"]'); | ||
if (viewportEl && viewportEl.content) { | ||
const initialScale = viewportEl.content.match(/initial\-scale\=\d+(\.\d+)?/); | ||
const scale = initialScale ? parseFloat(initialScale[0].split('=')[1]) : 1; | ||
if (scale < 1) { | ||
this.$dom.style.fontSize = 13 * dpr + 'px'; | ||
} | ||
} | ||
// remove from less to present transition effect | ||
$.one('.vc-mask', this.$dom).style.display = 'none'; | ||
// set theme | ||
this._updateTheme(); | ||
} | ||
/** | ||
* Update theme | ||
* @private | ||
*/ | ||
_updateTheme() { | ||
const theme = this.option.theme || 'light'; | ||
this.$dom.setAttribute('data-theme', theme); | ||
} | ||
setSwitchPosition(switchX, switchY) { | ||
const $switch = $.one('.vc-switch', this.$dom); | ||
[switchX, switchY] = this._getSwitchButtonSafeAreaXY($switch, switchX, switchY); | ||
this.switchPos.x = switchX; | ||
this.switchPos.y = switchY; | ||
$switch.style.right = switchX + 'px'; | ||
$switch.style.bottom = switchY + 'px'; | ||
tool.setStorage('switch_x', switchX); | ||
tool.setStorage('switch_y', switchY); | ||
} | ||
/** | ||
* Get an safe [x, y] position for switch button | ||
* @private | ||
*/ | ||
_getSwitchButtonSafeAreaXY($switch, x, y) { | ||
const docWidth = Math.max(document.documentElement.offsetWidth, window.innerWidth); | ||
const docHeight = Math.max(document.documentElement.offsetHeight, window.innerHeight); | ||
// check edge | ||
if (x + $switch.offsetWidth > docWidth) { | ||
x = docWidth - $switch.offsetWidth; | ||
} | ||
if (y + $switch.offsetHeight > docHeight) { | ||
y = docHeight - $switch.offsetHeight; | ||
} | ||
if (x < 0) { x = 0; } | ||
if (y < 20) { y = 20; } // safe area for iOS Home indicator | ||
return [x, y]; | ||
} | ||
/** | ||
* Get an safe [x, y] position for switch button | ||
* @private | ||
*/ | ||
_getSwitchButtonSafeAreaXY($switch, x, y) { | ||
const docWidth = Math.max(document.documentElement.offsetWidth, window.innerWidth); | ||
const docHeight = Math.max(document.documentElement.offsetHeight, window.innerHeight); | ||
// check edge | ||
if (x + $switch.offsetWidth > docWidth) { | ||
x = docWidth - $switch.offsetWidth; | ||
} | ||
if (y + $switch.offsetHeight > docHeight) { | ||
y = docHeight - $switch.offsetHeight; | ||
} | ||
if (x < 0) { x = 0; } | ||
if (y < 20) { y = 20; } // safe area for iOS Home indicator | ||
return [x, y]; | ||
} | ||
/** | ||
* 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 if (typeof window.getSelection !== 'function' || !getSelection().rangeCount) { | ||
e.preventDefault(); // prevent click 300ms later | ||
} | ||
if (!targetElem.disabled && !targetElem.readOnly) { | ||
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() { | ||
const that = this; | ||
// drag & drop switch button | ||
const $switch = $.one('.vc-switch', that.$dom); | ||
$.bind($switch, 'touchstart', function(e) { | ||
that.switchPos.startX = e.touches[0].pageX; | ||
that.switchPos.startY = e.touches[0].pageY; | ||
that.switchPos.hasMoved = false; | ||
}); | ||
$.bind($switch, 'touchend', function(e) { | ||
if (!that.switchPos.hasMoved) { | ||
return; | ||
} | ||
that.switchPos.startX = 0; | ||
that.switchPos.startY = 0; | ||
that.switchPos.hasMoved = false; | ||
that.setSwitchPosition(that.switchPos.endX, that.switchPos.endY); | ||
}); | ||
$.bind($switch, 'touchmove', function(e) { | ||
if (e.touches.length <= 0) { | ||
return; | ||
} | ||
const offsetX = e.touches[0].pageX - that.switchPos.startX, | ||
offsetY = e.touches[0].pageY - that.switchPos.startY; | ||
let x = Math.floor(that.switchPos.x - offsetX), | ||
y = Math.floor(that.switchPos.y - offsetY); | ||
[x, y] = that._getSwitchButtonSafeAreaXY($switch, x, y); | ||
$switch.style.right = x + 'px'; | ||
$switch.style.bottom = y + 'px'; | ||
that.switchPos.endX = x; | ||
that.switchPos.endY = y; | ||
that.switchPos.hasMoved = true; | ||
e.preventDefault(); | ||
}); | ||
// 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); | ||
}); | ||
// 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.isInited = 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]); | ||
} | ||
this.triggerEvent('ready'); | ||
} | ||
/** | ||
* trigger a vConsole.option event | ||
* @protect | ||
*/ | ||
triggerEvent(eventName, param) { | ||
eventName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1); | ||
if (tool.isFunction(this.option[eventName])) { | ||
this.option[eventName].apply(this, param); | ||
} | ||
} | ||
/** | ||
* init a plugin | ||
* @private | ||
*/ | ||
_initPlugin(plugin) { | ||
let that = this; | ||
plugin.vConsole = 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', that.$dom); | ||
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.isReady = true; | ||
plugin.trigger('ready'); | ||
} | ||
/** | ||
* trigger an event for each plugin | ||
* @private | ||
*/ | ||
_triggerPluginsEvent(eventName) { | ||
for (let id in this.pluginList) { | ||
if (this.pluginList[id].isReady) { | ||
this.pluginList[id].trigger(eventName); | ||
} | ||
} | ||
} | ||
/** | ||
* trigger an event by plugin's name | ||
* @private | ||
*/ | ||
_triggerPluginEvent(pluginName, eventName) { | ||
let plugin = this.pluginList[pluginName]; | ||
if (!!plugin && plugin.isReady) { | ||
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.debug('Plugin ' + plugin.id + ' has already been added.'); | ||
return false; | ||
} | ||
this.pluginList[plugin.id] = plugin; | ||
// init plugin only if vConsole is ready | ||
if (this.isInited) { | ||
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.debug('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.isInited) { | ||
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() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
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 panel | ||
* @public | ||
*/ | ||
hide() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
$.removeClass(this.$dom, 'vc-toggle'); | ||
setTimeout(() => { | ||
// panel will be hidden by CSS transition in 0.3s | ||
$.one('.vc-mask', this.$dom).style.display = 'none'; | ||
$.one('.vc-panel', this.$dom).style.display = 'none'; | ||
}, 330); | ||
this._triggerPluginsEvent('hideConsole'); | ||
} | ||
/** | ||
* show switch button | ||
* @public | ||
*/ | ||
showSwitch() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
let $switch = $.one('.vc-switch', this.$dom); | ||
$switch.style.display = 'block'; | ||
} | ||
/** | ||
* hide switch button | ||
*/ | ||
hideSwitch() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
let $switch = $.one('.vc-switch', this.$dom); | ||
$switch.style.display = 'none'; | ||
} | ||
/** | ||
* show a tab | ||
* @public | ||
*/ | ||
showTab(tabID) { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
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.activedTab && this._triggerPluginEvent(this.activedTab, 'hide'); | ||
this.activedTab = tabID; | ||
this._triggerPluginEvent(this.activedTab, 'show'); | ||
} | ||
/** | ||
* update option(s) | ||
* @public | ||
*/ | ||
setOption(keyOrObj, value) { | ||
if (tool.isString(keyOrObj)) { | ||
this.option[keyOrObj] = value; | ||
this._triggerPluginsEvent('updateOption'); | ||
this._updateTheme(); | ||
} else if (tool.isObject(keyOrObj)) { | ||
for (let k in keyOrObj) { | ||
this.option[k] = keyOrObj[k]; | ||
} | ||
this._triggerPluginsEvent('updateOption'); | ||
this._updateTheme(); | ||
} else { | ||
console.debug('The first parameter of vConsole.setOption() must be a string or an object.'); | ||
} | ||
} | ||
/** | ||
* uninstall vConsole | ||
* @public | ||
*/ | ||
destroy() { | ||
if (!this.isInited) { | ||
return; | ||
} | ||
// remove plugins | ||
let IDs = Object.keys(this.pluginList); | ||
for (let i = IDs.length - 1; i >= 0; i--) { | ||
this.removePlugin(IDs[i]); | ||
} | ||
// remove DOM | ||
this.$dom.parentNode.removeChild(this.$dom); | ||
// reverse isInited when destroyed | ||
this.isInited = false; | ||
} | ||
} // END class | ||
// export static class | ||
VConsole.VConsolePlugin = VConsolePlugin; | ||
VConsole.VConsoleLogPlugin = VConsoleLogPlugin; | ||
VConsole.VConsoleDefaultPlugin = VConsoleDefaultPlugin; | ||
VConsole.VConsoleSystemPlugin = VConsoleSystemPlugin; | ||
VConsole.VConsoleNetworkPlugin = VConsoleNetworkPlugin; | ||
VConsole.VConsoleElementPlugin = VConsoleElementPlugin; | ||
VConsole.VConsoleStoragePlugin = VConsoleStoragePlugin; | ||
export default VConsole; | ||
@@ -182,2 +182,15 @@ /* | ||
export function circularReplacer() { | ||
const seen = []; | ||
return (key, value) => { | ||
if (typeof(value) === 'object' && value !== null) { | ||
if (seen.indexOf(value) >= 0) { | ||
return '[Circular]'; | ||
} | ||
seen.push(value); | ||
} | ||
return value; | ||
}; | ||
}; | ||
/** | ||
@@ -202,3 +215,5 @@ * get an object's all keys ignore whether they are not enumerable | ||
} | ||
return keys.sort(); | ||
return keys.sort((a, b) => { | ||
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }); | ||
}); | ||
} | ||
@@ -205,0 +220,0 @@ |
@@ -16,2 +16,3 @@ /* | ||
import copy from 'copy-text-to-clipboard'; | ||
import * as tool from '../lib/tool.js'; | ||
@@ -31,2 +32,3 @@ import $ from '../lib/query.js'; | ||
}; | ||
let cachedLogs = {}; // for copy | ||
@@ -158,2 +160,17 @@ class VConsoleLogTab extends VConsolePlugin { | ||
that.logList = []; | ||
// copy | ||
$.delegate(that.$tabbox, 'click', '.vc-item-copy', (e) => { | ||
const btn = e.target.closest('.vc-item-copy'); | ||
const { id } = btn.closest('.vc-item'); | ||
const text = cachedLogs[id]; | ||
if (text != null && copy(text)) { | ||
btn.classList.add('vc-item-copy-success'); | ||
setTimeout(() => { | ||
btn.classList.remove('vc-item-copy-success'); | ||
}, 600); | ||
}; | ||
}); | ||
} | ||
@@ -180,2 +197,3 @@ | ||
} | ||
cachedLogs = {}; | ||
} | ||
@@ -305,2 +323,3 @@ | ||
preLog = {}; | ||
cachedLogs = {}; | ||
} | ||
@@ -497,23 +516,33 @@ | ||
let $content = $.one('.vc-item-content', $line); | ||
const rawLogs = []; | ||
// generate content from item.logs | ||
for (let i = 0; i < logs.length; i++) { | ||
const curLog = logs[i]; | ||
let rawLog; | ||
let log; | ||
try { | ||
if (logs[i] === '') { | ||
if (curLog === '') { | ||
// ignore empty string | ||
continue; | ||
} else if (tool.isFunction(logs[i])) { | ||
} else if (tool.isFunction(curLog)) { | ||
// convert function to string | ||
log = '<span> ' + logs[i].toString() + '</span>'; | ||
} else if (tool.isObject(logs[i]) || tool.isArray(logs[i])) { | ||
rawLog = curLog.toString(); | ||
log = `<span> ${rawLog}</span>`; | ||
} else if (tool.isObject(curLog) || tool.isArray(curLog)) { | ||
// object or array | ||
log = this.getFoldedLine(logs[i]); | ||
rawLog = JSON.stringify(curLog, tool.circularReplacer(), 2) | ||
log = this.getFoldedLine(curLog); | ||
} else { | ||
// default | ||
log = (logStyle[i] ? `<span style="${logStyle[i]}"> ` : '<span> ') + tool.htmlEncode(logs[i]).replace(/\n/g, '<br/>') + '</span>'; | ||
rawLog = curLog; | ||
log = (logStyle[i] ? `<span style="${logStyle[i]}"> ` : '<span> ') + tool.htmlEncode(curLog).replace(/\n/g, '<br/>') + '</span>'; | ||
} | ||
} catch (e) { | ||
log = '<span> [' + (typeof logs[i]) + ']</span>'; | ||
rawLog = typeof curLog; | ||
log = `<span> [${rawLog}]</span>`; | ||
} | ||
if (log) { | ||
rawLogs.push(rawLog); | ||
if (typeof log === 'string') | ||
@@ -526,2 +555,5 @@ $content.insertAdjacentHTML('beforeend', log); | ||
// for copy | ||
cachedLogs[item._id] = rawLogs.join(' '); | ||
// generate content from item.content | ||
@@ -528,0 +560,0 @@ if (tool.isObject(item.content)) { |
@@ -34,43 +34,45 @@ /* | ||
// print system info | ||
let ua = navigator.userAgent, | ||
logMsg = ''; | ||
const ua = navigator.userAgent; | ||
let logMsg = []; | ||
// wechat client version | ||
let wxVersion = ua.match(/MicroMessenger\/([\d\.]+)/i); | ||
wxVersion = wxVersion && wxVersion[1] ? wxVersion[1] : null; | ||
const isMiniprogram = location.host === 'servicewechat.com'; | ||
// location | ||
console.info('[system]', 'Location:', location.href); | ||
if (!isMiniprogram) { | ||
console.info('[system]', 'Location:', location.href); | ||
} | ||
// device & system | ||
let ipod = ua.match(/(ipod).*\s([\d_]+)/i), | ||
const ipod = ua.match(/(ipod).*\s([\d_]+)/i), | ||
ipad = ua.match(/(ipad).*\s([\d_]+)/i), | ||
iphone = ua.match(/(iphone)\sos\s([\d_]+)/i), | ||
android = ua.match(/(android)\s([\d\.]+)/i); | ||
android = ua.match(/(android)\s([\d\.]+)/i), | ||
mac = ua.match(/(Mac OS X)\s([\d_]+)/i); | ||
logMsg = 'Unknown'; | ||
logMsg = []; | ||
if (android) { | ||
logMsg = 'Android ' + android[2]; | ||
logMsg.push('Android ' + android[2]); | ||
} else if (iphone) { | ||
logMsg = 'iPhone, iOS ' + iphone[2].replace(/_/g,'.'); | ||
logMsg.push('iPhone, iOS ' + iphone[2].replace(/_/g,'.')); | ||
} else if (ipad) { | ||
logMsg = 'iPad, iOS ' + ipad[2].replace(/_/g, '.'); | ||
logMsg.push('iPad, iOS ' + ipad[2].replace(/_/g, '.')); | ||
} else if (ipod) { | ||
logMsg = 'iPod, iOS ' + ipod[2].replace(/_/g, '.'); | ||
logMsg.push('iPod, iOS ' + ipod[2].replace(/_/g, '.')); | ||
} else if (mac) { | ||
logMsg.push('Mac, MacOS ' + mac[2].replace(/_/g, '.')); | ||
} | ||
let templogMsg = logMsg; | ||
// wechat client version | ||
let version = ua.match(/MicroMessenger\/([\d\.]+)/i); | ||
logMsg = 'Unknown'; | ||
if (version && version[1]) { | ||
logMsg = version[1]; | ||
templogMsg += (', WeChat ' + logMsg); | ||
console.info('[system]', 'System:', templogMsg); | ||
} else { | ||
console.info('[system]', 'System:', templogMsg); | ||
if (wxVersion) { | ||
logMsg.push('WeChat ' + wxVersion); | ||
} | ||
console.info('[system]', 'Client:', logMsg.length ? logMsg.join(', ') : 'Unknown'); | ||
// network type | ||
let network = ua.toLowerCase().match(/ nettype\/([^ ]+)/g); | ||
logMsg = 'Unknown'; | ||
const network = ua.toLowerCase().match(/ nettype\/([^ ]+)/g); | ||
if (network && network[0]) { | ||
network = network[0].split('/'); | ||
logMsg = network[1]; | ||
console.info('[system]', 'Network:', logMsg); | ||
logMsg = [network[1]]; | ||
console.info('[system]', 'Network:', logMsg.length ? logMsg.join(', ') : 'Unknown'); | ||
} | ||
@@ -77,0 +79,0 @@ |
@@ -112,3 +112,3 @@ /* | ||
if (this.isInBottom == true) { | ||
this.scrollToBottom(); | ||
this.autoScrollToBottom(); | ||
} | ||
@@ -123,2 +123,8 @@ } | ||
if (this.isInBottom == true) { | ||
this.autoScrollToBottom(); | ||
} | ||
} | ||
autoScrollToBottom() { | ||
if (!this.vConsole.option.disableLogScrolling) { | ||
this.scrollToBottom(); | ||
@@ -259,4 +265,4 @@ } | ||
// scroll to bottom | ||
if (this.isInBottom) { | ||
this.scrollToBottom(); | ||
if (this.isInBottom && this.isShow) { | ||
this.autoScrollToBottom(); | ||
} | ||
@@ -512,17 +518,33 @@ } | ||
return _fetch(url, init).then((response) => { | ||
response.clone().json().then((json) => { | ||
item.endTime = +new Date(), | ||
item.costTime = item.endTime - (item.startTime || item.endTime); | ||
item.status = response.status; | ||
item.header = {}; | ||
for (let pair of response.headers.entries()) { | ||
item.header[pair[0]] = pair[1]; | ||
} | ||
item.response = json; | ||
item.readyState = 4; | ||
const contentType = response.headers.get('content-type'); | ||
item.responseType = contentType.includes('application/json') ? 'json' : contentType.includes('text/html') ? 'text' : ''; | ||
return json; | ||
}) | ||
that.updateRequest(id, item); | ||
response | ||
.clone() | ||
.text() | ||
.then((text) => { | ||
const contentType = response.headers.get('content-type'); | ||
// use 'text' as default type in case of contentType is json but response is not real JSON | ||
let itemResponse = text; | ||
let itemResponseType = ''; | ||
if (contentType.includes('application/json')) { | ||
try { | ||
itemResponse = JSON.parse(text); | ||
itemResponseType = 'json'; | ||
} catch (e) {} | ||
} else if (contentType.includes('text/html')) { | ||
itemResponseType = 'text'; | ||
} | ||
item.endTime = +new Date(); | ||
item.costTime = item.endTime - (item.startTime || item.endTime); | ||
item.status = response.status; | ||
item.header = {}; | ||
for (let pair of response.headers.entries()) { | ||
item.header[pair[0]] = pair[1]; | ||
} | ||
item.response = itemResponse; | ||
item.readyState = 4; | ||
item.responseType = itemResponseType; | ||
return itemResponse; | ||
}).finally(() => { | ||
that.updateRequest(id, item); | ||
}); | ||
return response; | ||
@@ -529,0 +551,0 @@ }) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
651105
20
97
0
3
79
3851
+ Addedcopy-text-to-clipboard@3.2.0(transitive)
Updatedcore-js@^3.11.0