blob: a08fecf8228436f30c27ae21ec5e1e8a83f66e02 [file] [log] [blame]
<template>
<div id="example-panel" :class="shared.computedOptionExampleLayout + '-layout'">
<h2>{{$t('example.title')}}</h2>
<p class="intro">{{ shared.allOptionExamples ? $t('example.intro') : $t('example.noExample')}}</p>
<div class="preview-and-code" v-if="shared.currentExampleOption">
<div class="preview-main"></div>
<div class="example-code">
<div class="codemirror-main"></div>
</div>
<el-alert
:title="$t('example.setOptionError')"
v-if="hasError"
type="error"
>
</el-alert>
</div>
<div class="toolbar">
<el-select size='mini' v-if="shared.allOptionExamples" class="example-list" v-model="shared.currentExampleName"
:popper-append-to-body="false">
<el-option v-for="item in shared.allOptionExamples"
:key="item.name"
:value="item.name"
:label="shared.locale === 'en' ? item['title-en'] : item.title"
></el-option>
</el-select>
<el-button v-if="shared.currentExampleOption"
type="primary"
icon="el-icon-refresh"
size="mini"
:title="$t('example.refresh')"
@click="refreshForce"></el-button>
<el-button
style="margin-left: 0"
type="primary"
icon="el-icon-s-operation"
size="mini"
:title="$t('example.changeLayout')"
v-popover:changeLayoutPopover></el-button>
<el-button size='mini' circle icon="el-icon-close" @click="closeExamplePanel"></el-button>
</div>
<el-popover
ref="changeLayoutPopover"
placement="bottom"
trigger="click"
v-model="showChangeLayoutPopover">
<div class="example-change-layout">
<div class="layout-title"><i class="el-icon-s-operation"></i>{{$t('example.changeLayout')}}</div>
<div class="layout-mode">
<el-radio-group v-model="shared.optionExampleLayout" @change="changeLayout" size="mini">
<el-radio-button
v-for="layout in optionExampleLayouts"
:key="layout"
:label="layout">{{$t('example.layout.' + layout)}}</el-radio-button>
</el-radio-group>
</div>
</div>
</el-popover>
</div>
</template>
<script>
// Remarks:
// 代码不能编辑,可以跳转到 examples 带上 base64,在 examples 页面编辑
import {store, updateOptionExampleLayout, optionExampleLayouts} from '../store';
import CodeMirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
// import 'codemirror/theme/paraiso-dark.css';
import 'codemirror/theme/dracula.css';
// import 'codemirror/mode/javascript/javascript.js'
import beautifier from 'js-beautify';
import {throttle} from 'lodash-es';
import arrayDiff from 'zrender/lib/core/arrayDiff';
import {ECHARTS_LIB} from '../config';
let echartsLoadPromise;
function fetchECharts() {
return echartsLoadPromise || (echartsLoadPromise = new Promise(function (resolve) {
const script = document.createElement('script');
script.src = ECHARTS_LIB;
script.async = true;
script.onload = function () {
resolve();
echartsLoadPromise = null;
}
document.body.appendChild(script);
}));
}
function diffUpdateCode(oldCode, newCode, cmInstance) {
const oldLines = oldCode.split(/\n/);
const newLines = newCode.split(/\n/);
const diff = arrayDiff(oldLines, newLines);
const changedLines = [];
// Remove lines from bottom to top so the line number won't be changed.
for (let i = diff.length - 1; i >= 0; i--) {
const item = diff[i];
if (item.removed) {
for (let k = item.count - 1; k >= 0; k--) {
const idx = item.indices[k];
cmInstance.replaceRange(
'', {line: idx, ch: 0}, {line: idx + 1, ch: 0}
);
}
}
}
for (let i = 0; i < diff.length; i++) {
const item = diff[i];
if (item.added) {
for (let k = 0; k < item.count; k++) {
const idx = item.indices[k];
cmInstance.replaceRange(
newLines[idx] + '\n', {line: idx, ch: 0}
);
changedLines.push(idx);
}
}
}
changedLines.forEach(function (idx) {
cmInstance.addLineClass(idx, 'wrap', 'option-changed');
});
if (diff.length) {
setTimeout(() => {
cmInstance.scrollIntoView({
line: changedLines[0],
ch: 0
}, cmInstance.getWrapperElement().clientHeight - 50);
}, 20);
}
return changedLines;
}
function updateOption(option, isRefreshForce) {
if (this.shared.currentExampleName !== this.lastUpdateExampleName) {
this.lastUpdateExampleName = this.shared.currentExampleName;
// Refresh all if example base option is changed.
this.refreshForce();
return;
}
const viewport = this.$el.querySelector('.preview-main');
if (!viewport) {
return;
}
// Clear error msg.
this.hasError = false;
if (typeof echarts === 'undefined') {
// TODO Put fetch charts when component is initialized.
fetchECharts().then(() => {
if (!this.echartsInstance) {
this.chartInstance = echarts.init(viewport);
}
if (this.shared.cleanMode) {
this.chartInstance.clear();
}
this.chartInstance.setOption(option, true);
})
}
else {
if (!this.echartsInstance) {
this.chartInstance = echarts.init(viewport);
}
try {
if (this.shared.cleanMode) {
this.chartInstance.clear();
}
this.chartInstance.setOption(option, true);
}
catch (e) {
// 一些属性切换的时候可能会出现一些位置的错误
console.error(e);
this.hasError = true;
}
}
if (!this.cmInstance) {
this.cmInstance = CodeMirror(this.$el.querySelector('.codemirror-main'), {
value: this.formattedOptionCodeStr,
mode: 'javascript',
// theme: 'paraiso-dark',
theme: 'dracula',
readOnly: true
});
}
else {
// TODO: Highlight the diff lines.
// TODO: Only change the changed line. optimize
const oldCode = this.cmInstance.getValue();
const newCode = this.formattedOptionCodeStr;
if (this.oldHighlightedLines) {
this.oldHighlightedLines.forEach((idx) => {
this.cmInstance.removeLineClass(idx, 'wrap', 'option-changed');
});
}
if (!isRefreshForce) {
this.oldHighlightedLines = diffUpdateCode(oldCode, newCode, this.cmInstance);
}
else {
this.cmInstance.setValue(newCode);
this.oldHighlightedLines = [];
}
}
this.lastUpdateExampleName = this.shared.currentExampleName;
}
export default {
data() {
return {
shared: store,
hasError: false,
lastUpdateExampleName: '',
oldHighlightedLines: [],
showChangeLayoutPopover: false,
optionExampleLayouts
};
},
mounted() {
// TODO use css?
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
this.resize();
if (this.shared.currentExampleOption) {
this.updateOptionThrottled(this.shared.currentExampleOption);
}
if (this.shared.allOptionExamples) {
this.shared.currentExampleName = this.shared.allOptionExamples[0].name;
}
else {
this.shared.currentExampleName = ''
}
},
destroyed() {
if (this.chartInstance) {
this.chartInstance.dispose();
this.chartInstance = null;
}
window.removeEventListener('resize', this.resize);
},
watch: {
'shared.currentExampleOption'(newVal) {
if (newVal) {
this.updateOptionThrottled(newVal);
}
},
'shared.allOptionExamples'(newVal) {
// Use the first example as default.
if (newVal) {
this.shared.currentExampleName = newVal[0].name;
}
else {
this.shared.currentExampleName = '';
}
},
'shared.currentExampleName'(newVal) {
this.changeExample(newVal);
}
},
methods: {
updateOption,
updateOptionThrottled: throttle(updateOption, 300, {
leading: false
}),
resize() {
const examplePanel = this.$el;
const previewMain = examplePanel.querySelector('.preview-main');
if (this.shared.computedOptionExampleLayout !== 'right') {
examplePanel.style.height = (window.innerHeight * 0.5 - 60) + 'px';
examplePanel.style.width = 'auto';
}
else {
examplePanel.style.width = examplePanel.parentNode.clientWidth * 0.45 + 'px';
examplePanel.style.height = 'auto';
}
if (this.chartInstance) {
this.chartInstance.resize();
}
},
refreshForce: function () {
// Dispose first
if (this.shared.currentExampleOption) {
if (this.chartInstance) {
this.chartInstance.dispose();
this.chartInstance = null;
}
this.updateOption(this.shared.currentExampleOption, true);
}
},
closeExamplePanel() {
this.shared.showOptionExample = false;
},
changeExample(exampleName) {
const example = this.shared.allOptionExamples &&
this.shared.allOptionExamples.find(item => item.name === exampleName);
if (!example) {
this.shared.currentExampleOption = null;
return false;
}
const code = example.code;
try {
const func = new Function(code + '\n return option');
this.shared.currentExampleOption = Object.freeze(func());
}
catch(e) {
console.error(e);
console.log(code);
}
},
changeLayout(layout) {
this.showChangeLayoutPopover = false;
updateOptionExampleLayout(layout);
this.$nextTick(() => {
this.resize();
});
}
},
computed: {
optionCodeStr() {
const optStr = JSON.stringify(this.shared.currentExampleOption, function (key, value) {
if (typeof value === 'function') {
return "__functionstart__"
+ value.toString().replace(/\n/g, '__newline__') // avoid \n being escaped by JSON.stringify
+ "__functionend__";
}
return value;
});
return `option = ${optStr}`;
},
formattedOptionCodeStr() {
return beautifier.js(this.optionCodeStr
.replace(/"(\w+)"\s*:/g, '$1:')
.replace(/"__functionstart__/g, "")
.replace(/__functionend__"/g, "")
// newline from function
.replace(/__newline__/g, '\n'), {
indent_size: 2
});
}
}
}
</script>
<style lang="scss">
#example-panel {
position: fixed;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 10px 0px;
text-align: left;
background: #fff;
// transition: width 500ms cubic-bezier(0.215, 0.610, 0.355, 1);
// background: #162436;
// border-left: 1px solid #ddd;
h2 {
font-weight: normal;
font-size: 20px;
color: #333;
padding-left: 20px;
font-weight: bold;
margin: 5px 0;
}
p.intro {
color: #aaa;
padding-left: 20px;
margin: 5px 0;
font-size: 12px;
}
.preview-and-code {
position: absolute;
top: 75px;
bottom: 0;
left: 0;
right: 0;
}
.el-alert {
position: absolute;
top: 0;
}
.preview-main {
position: relative;
padding: 0 10px;
background: #fefefe;
box-sizing: border-box;
}
.example-code {
position: relative;
.codemirror-main {
position: absolute;
left: 10px;
top: 10px;
right: 10px;
bottom: 15px;
box-shadow: -5px -5px 15px rgba(0, 0, 0, 0.1);
.CodeMirror {
height: 100%;
overflow-y: scroll;
border-radius: 6px;
.CodeMirror-scroll {
padding: 15px;
}
font-family: 'Source Code Pro', monospace;
font-size: 13px;
::-webkit-scrollbar-thumb {
width: 8px;
min-height: 15px;
background: rgba(255, 255, 255, 0.3) !important;
-webkit-transition: all 0.3s ease-in-out;
transition: all 0.3s ease-in-out;
border-radius: 2px
}
.option-changed {
background: rgba(255, 255, 255, 0.1);
// border-left: 3px solid #32dde6;
}
.CodeMirror-cursor {
display: none;
}
}
}
}
.toolbar {
position: absolute;
top: 15px;
right: 10px;
.example-list {
width: 180px;
}
}
&.right-layout {
bottom: 0;
top: 40px;
right: 10px;
.preview-main {
width: 100%;
height: 50%;
}
.example-code {
width: 100%;
height: 50%;
}
}
&.bottom-layout {
left: 300px;
bottom: 0;
right: 10px;
.preview-main {
width: 50%;
height: 100%;
float: left;
}
.example-code {
width: 50%;
height: 100%;
float: left;
}
}
&.top-layout {
left: 300px;
// dev
// top: 40px;
top: 50px;
right: 10px;
.preview-main {
width: 50%;
height: 100%;
float: left;
}
.example-code {
width: 50%;
height: 100%;
float: left;
}
}
}
.example-change-layout {
.layout-title > i {
font-size: 14px;
margin-right: 5px;
}
.layout-mode {
margin-top: 10px;
}
}
</style>