blob: a78cd6e8d16c151b3e68d0b1cac68499c3ebedc1 [file] [log] [blame]
<template>
<div :class="[inEditor && !shared.isMobile ? '' : 'full']">
<div
v-loading="loading"
class="right-panel"
id="chart-panel"
ref="chartPanel"
:style="{ background: backgroundColor }"
></div>
<div id="tool-panel">
<div class="left-panel">
<el-switch
class="dark-mode"
v-model="shared.darkMode"
active-color="#181432"
:active-text="$t('editor.darkMode')"
:inactive-text="''"
>
</el-switch>
<el-switch
v-if="!isGL"
class="enable-decal"
v-model="shared.enableDecal"
:active-text="$t('editor.enableDecal')"
:inactive-text="''"
>
</el-switch>
<!-- Not display when random button is displayed on mobile devices. -->
<el-popover
placement="bottom"
trigger="click"
v-if="!isGL && !(shared.isMobile && hasRandomData)"
>
<div class="render-config-container">
<div>
<label class="tool-label">{{ $t('editor.renderer') }}</label>
<el-radio-group
v-model="shared.renderer"
size="mini"
style="text-transform: uppercase"
>
<el-radio-button label="svg" />
<el-radio-button label="canvas" />
</el-radio-group>
</div>
<el-switch
v-if="shared.renderer === 'canvas'"
v-model="shared.useDirtyRect"
:active-text="$t('editor.useDirtyRect')"
:inactive-text="''"
/>
</div>
<span class="render-config-trigger" slot="reference">
<el-button size="mini">
{{ $t('editor.renderCfgTitle')
}}<i class="el-icon-setting el-icon--right"></i>
</el-button>
</span>
</el-popover>
<el-button
class="random"
v-if="hasRandomData"
size="mini"
@click="changeRandomSeed"
>{{ $t('editor.randomData') }}</el-button
>
<el-select
v-if="shared.echartsVersion && !shared.isMobile"
class="version-select"
:class="{ nightly }"
size="mini"
id="choose-echarts-version"
v-model="shared.echartsVersion"
@change="changeVersion"
>
<el-option
v-for="version in versionList"
:key="version"
:label="version"
:value="version"
>
{{ version }}
</el-option>
</el-select>
<el-checkbox
v-if="inEditor && !shared.isMobile"
v-model="nightly"
class="use-nightly"
>Nightly</el-checkbox
>
</div>
<a
:href="editLink"
target="_blank"
v-if="!inEditor"
class="edit btn btn-sm"
>{{ $t('editor.edit') }}</a
>
</div>
<div id="preview-status">
<div class="left-buttons">
<template v-if="inEditor && !shared.isMobile">
<el-button icon="el-icon-download" size="mini" @click="download">
{{ $t('editor.download') }}
</el-button>
<el-button
@click="screenshot"
icon="el-icon-camera-solid"
size="mini"
>
{{ $t('editor.screenshot') }}
</el-button>
<el-button @click="share" icon="el-icon-share" size="mini">
{{ $t('editor.share.title') }}
</el-button>
</template>
</div>
<div
id="run-log"
v-if="inEditor && !shared.isMobile && shared.editorStatus.message"
>
<span class="run-log-time">{{ shared.editorStatus.time }}</span>
<span :class="'run-log-type-' + shared.editorStatus.type">{{
shared.editorStatus.message
}}</span>
</div>
</div>
</div>
</template>
<script>
import {
getExampleConfig,
isGLExample,
saveExampleCodeToLocal,
store,
updateRandomSeed,
updateRunHash
} from '../common/store';
import { SCRIPT_URLS, URL_PARAMS } from '../common/config';
import { compressStr } from '../common/helper';
import { createSandbox } from './sandbox';
import debounce from 'lodash/debounce';
import { download } from './downloadExample';
import { gotoURL } from '../common/route';
import { gt } from 'semver';
const example = getExampleConfig();
const isGL = 'gl' in URL_PARAMS || isGLExample();
const isLocal = 'local' in URL_PARAMS;
const isDebug = 'debug' in URL_PARAMS;
const hasBMap = example && example.tags.indexOf('bmap') >= 0;
function getScriptURL(link) {
return isDebug || isLocal ? link.replace('.min.', '.') : link;
}
function getScripts(nightly) {
const echartsDir = SCRIPT_URLS[
isLocal ? 'localEChartsDir' : nightly ? 'echartsNightlyDir' : 'echartsDir'
].replace('{{version}}', store.echartsVersion);
const code = store.runCode;
return [
echartsDir + getScriptURL(SCRIPT_URLS.echartsJS),
...(isGL
? [
isLocal
? SCRIPT_URLS.localEChartsGLJS
: getScriptURL(SCRIPT_URLS.echartsGLJS)
]
: []),
...(hasBMap || /coordinateSystem.*:.*['"]bmap['"]/g.test(code)
? [
SCRIPT_URLS.bmapLibJS,
echartsDir + getScriptURL(SCRIPT_URLS.echartsBMapJS)
]
: []),
...(code.indexOf('ecStat.') > -1
? [getScriptURL(SCRIPT_URLS.echartsStatJS)]
: []),
...(/map.*:.*['"]world['"]/g.test(code)
? [SCRIPT_URLS.echartsWorldMapJS]
: []),
...(code.indexOf('app.config') > -1 ? [SCRIPT_URLS.datGUIMinJS] : [])
].map((url) => ({ src: url }));
}
function log(text, type) {
if (type !== 'warn' && type !== 'error') {
type = 'info';
}
const now = new Date();
store.editorStatus.time = [now.getHours(), now.getMinutes(), now.getSeconds()]
.map((t) => (t + '').padStart(2, '0'))
.join(':');
store.editorStatus.message = text;
store.editorStatus.type = type;
}
function run(recreateInstance) {
if (!store.runCode) {
return;
}
const runCode = () => {
this.sandbox.run(store, recreateInstance);
// Update run hash to let others known chart has been changed.
updateRunHash();
};
const scripts = getScripts(this.nightly);
const ECScriptReg = /\/echarts(?:\.min)?\.js/;
const scriptsChanged =
!this.scripts ||
scripts.some(
(s) =>
!ECScriptReg.test(s.src) &&
this.scripts.findIndex((s1) => s1.src === s.src) === -1
);
if (!this.sandbox || scriptsChanged) {
this.loading = true;
let isFirstRun = true;
this.dispose();
this.sandbox = createSandbox(
this.$refs.chartPanel,
(this.scripts = scripts),
store.isSharedCode,
() => {
runCode();
this.loading = false;
},
() => {
// TODO show error hints
console.error('failed to run sandbox');
this.loading = false;
this.dispose();
},
(errMsg) => {
const infiniteLoopInEditor =
errMsg && errMsg.indexOf('loop executes') > -1;
const potentialRedirection =
errMsg && errMsg.indexOf('potential redirection') > -1;
log(
this.$t(
`editor.${
infiniteLoopInEditor
? 'infiniteLoopInEditor'
: potentialRedirection
? 'potentialRedirectionInEditor'
: 'errorInEditor'
}`
),
'error'
);
},
(updateTime) => {
const option = this.getOption();
if (
typeof option.backgroundColor === 'string' &&
option.backgroundColor !== 'transparent'
) {
this.backgroundColor = option.backgroundColor;
} else {
this.backgroundColor = '#fff';
}
log(this.$t('editor.chartOK') + updateTime.toFixed(2) + 'ms');
// Find the appropriate throttle time
const debounceTimeQuantities = [0, 500, 2000, 5000, 10000];
for (let i = debounceTimeQuantities.length - 1; i >= 0; i--) {
const quantity = debounceTimeQuantities[i];
const preferredDebounceTime =
debounceTimeQuantities[i + 1] || 1000000;
if (
updateTime >= quantity &&
this.debouncedTime !== preferredDebounceTime
) {
this.debouncedRun = debounce(run, preferredDebounceTime, {
trailing: true
});
this.debouncedTime = preferredDebounceTime;
break;
}
}
if (isFirstRun) {
this.$emit('ready');
isFirstRun = false;
}
},
(css) => {
this.css = css;
}
);
} else {
runCode();
}
}
export default {
props: {
inEditor: {
type: Boolean
}
},
data() {
return {
shared: store,
debouncedTime: undefined,
backgroundColor: '',
autoRun: true,
loading: false,
isGL,
allEChartsVersions: [],
nightlyVersions: [],
nightly: false
};
},
mounted() {
this.run();
this.fetchVersionList();
},
computed: {
hasRandomData() {
return (
this.shared.runCode && this.shared.runCode.indexOf('Math.random()') >= 0
);
},
editLink() {
return './editor.html' + location.search;
},
versionList() {
return this.nightly ? this.nightlyVersions : this.allEChartsVersions;
},
isNightlyVersion() {
return store.echartsVersion && store.echartsVersion.indexOf('dev') > -1;
}
},
watch: {
'shared.runCode'() {
if (this.autoRun || !this.sandbox) {
if (!this.debouncedRun) {
// First run
this.run();
} else {
this.debouncedRun();
}
}
},
'shared.renderer'() {
this.refresh();
},
'shared.darkMode'() {
this.refresh();
},
'shared.enableDecal'() {
this.refresh();
},
'shared.useDirtyRect'() {
this.refresh();
},
isNightlyVersion: {
handler(val) {
this.nightly = val;
},
immediate: true
}
},
methods: {
run,
// debouncedRun will be created at first run
// debouncedRun: null,
refresh() {
this.run(true);
},
refreshAll() {
// trigger reload
this.scripts = null;
this.run(true);
},
dispose() {
if (this.sandbox) {
this.sandbox.dispose();
this.sandbox = null;
}
},
download,
screenshot() {
this.sandbox &&
this.sandbox.screenshot(
(URL_PARAMS.c || Date.now()) +
'.' +
(store.renderer === 'svg' ? 'svg' : 'png')
);
},
share() {
let shareURL = new URL(location.href);
if (store.initialCode !== store.sourceCode) {
shareURL.searchParams.set('code', compressStr(store.sourceCode));
}
navigator.clipboard
.writeText(shareURL.toString())
.then(() => {
this.$message.closeAll();
this.$message({
type: 'success',
message: this.$t('editor.share.success'),
customClass: 'no-min-width'
});
})
// PENDING
.catch((e) => {
console.error('failed to write share url to the clipboard', e);
window.open(shareURL, '_blank');
});
},
getOption() {
return this.sandbox && this.sandbox.getOption();
},
changeVersion() {
saveExampleCodeToLocal();
setTimeout(() => {
gotoURL(
Object.assign({}, URL_PARAMS, {
version: store.echartsVersion
})
);
});
},
changeRandomSeed() {
updateRandomSeed();
gotoURL(
{
...URL_PARAMS,
random: store.randomSeed
},
true
);
this.run();
},
// hasEditorError() {
// const annotations = this.editor.getSession().getAnnotations();
// for (let aid = 0, alen = annotations.length; aid < alen; ++aid) {
// if (annotations[aid].type === 'error') {
// return true;
// }
// }
// return false;
// },
fetchVersionList() {
$.getJSON('https://data.jsdelivr.com/v1/package/npm/echarts').done(
(data) => {
const versions = data.versions.filter(
(version) =>
version.indexOf('beta') < 0 &&
version.indexOf('rc') < 0 &&
version.indexOf('alpha') < 0 &&
version.startsWith('5') // Only version 5.
);
this.allEChartsVersions = versions;
// Use lastest version
if (
!store.echartsVersion ||
store.echartsVersion === '5' ||
store.echartsVersion === 'latest'
) {
store.echartsVersion = versions[0];
}
// put latest rc version for preview
if (gt(data.tags.rc.split('-')[0], data.tags.latest)) {
versions.unshift(data.tags.rc);
store.echartsVersion === 'rc' &&
(store.echartsVersion = versions[0]);
}
}
);
$.getJSON(
'https://data.jsdelivr.com/v1/package/npm/echarts-nightly'
).done(({ tags: { latest, next }, versions }) => {
const nextIdx = versions.indexOf(next);
const latestIdx = versions.indexOf(latest);
this.nightlyVersions = versions
.slice(nextIdx, nextIdx + 10)
.concat(versions.slice(latestIdx, latestIdx + 10));
});
},
getAssets() {
return {
scripts: this.scripts,
css: this.css
};
}
}
};
</script>
<style lang="scss">
@import '../style/color.scss';
@mixin flex-center {
display: flex;
align-items: center;
justify-content: space-between;
}
#chart-panel {
position: absolute;
// top: $control-panel-height;
top: 42px;
right: 15px;
bottom: 50px;
left: 15px;
box-sizing: border-box;
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 20px;
border-radius: 5px;
background: #fff;
overflow: hidden;
padding: 10px;
}
.render-config-container {
@include flex-center;
user-select: none;
> * {
margin-left: 10px;
margin-right: 10px;
}
.tool-label {
font-weight: bold;
margin-right: 5px;
}
.el-radio-group {
label {
margin-bottom: 0;
}
}
}
#tool-panel {
position: absolute;
top: 5px;
right: 15px;
left: 15px;
@include flex-center;
user-select: none;
* {
font-size: 12px;
}
.render-config-trigger {
cursor: pointer;
font-weight: 500;
margin-left: 10px;
}
.version-select {
width: 80px;
margin-left: 10px;
&.nightly {
width: 160px;
}
}
.random,
.use-nightly {
margin-left: 10px;
}
label {
margin-bottom: 0;
}
.dark-mode {
margin-right: 5px;
}
.edit {
margin-left: 5px;
cursor: pointer;
}
}
.full {
#chart-panel {
top: 40px;
right: 5px;
bottom: 5px;
left: 5px;
box-shadow: rgba(10, 9, 9, 0.1) 0px 0px 5px;
}
#tool-panel {
right: 5px;
left: 5px;
}
}
#preview-status {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
padding: 0 15px;
font-size: 0.9rem;
@include flex-center;
.left-buttons {
flex-shrink: 0;
}
#run-log {
@include flex-center;
font-size: 12px;
margin-left: 10px;
text-align: right;
.run-log-time {
color: $clr-text;
margin-right: 10px;
white-space: nowrap;
}
.run-log-type-info {
color: $clr-text;
}
.run-log-type-warn {
color: $clr-warn;
}
.run-log-type-error {
color: $clr-error;
}
}
}
.el-message {
&.no-min-width {
min-width: auto;
}
}
</style>