Chore: Theme Extension (#16686)

This commit is contained in:
iamkun 2019-08-09 17:53:49 +08:00 committed by GitHub
parent 8b8a1a2e87
commit f29f49a17d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 540 additions and 12 deletions

View File

@ -1,6 +1,7 @@
{
"globals": {
"ga": true
"ga": true,
"chrome": true
},
"plugins": ["html", "json"],
"extends": "elemefe",

View File

@ -0,0 +1,33 @@
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const demoConfig = require('./webpack.demo');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
demoConfig.entry = {
background: path.join(process.cwd(), './examples/extension/src/background'),
entry: path.join(process.cwd(), './examples/extension/src/entry')
};
demoConfig.output = {
path: path.join(process.cwd(), './examples/extension/dist'),
filename: '[name].js'
};
demoConfig.plugins = [
new CopyWebpackPlugin([
{ from: 'examples/extension/src/manifest.json' },
{ from: 'examples/extension/src/icon.png' }
]),
new VueLoaderPlugin(),
new ProgressBarPlugin(),
new webpack.LoaderOptionsPlugin({
vue: {
compilerOptions: {
preserveWhitespace: false
}
}
}),
new webpack.HotModuleReplacementPlugin()
];
demoConfig.module.rules.find(a => a.loader === 'url-loader').query = {};
module.exports = demoConfig;

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
</div>

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
<el-button

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
</div>

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
</div>

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
</div>

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
</div>

View File

@ -1,7 +1,7 @@
<template>
<section class="config" :key="displayName">
<div class="config-label">
<el-tooltip :content="displayName">
<el-tooltip :content="displayName" placement="top">
<span>{{displayKeyName}}</span>
</el-tooltip>
</div>

View File

@ -102,7 +102,7 @@ export default {
defaultConfig = res;
})
.catch(err => {
this.onError(err);
this.onError && this.onError(err);
})
.then(() => {
setTimeout(() => {

View File

@ -1,5 +1,5 @@
<template>
<div class="main" ref="mainPanel">
<div class="editor-main" ref="mainPanel">
<!-- <span>{{configName}}</span> -->
<div v-for="(config, key) in configByOrder" :key="key">
<span
@ -22,7 +22,7 @@
</template>
<style>
.main {
.editor-main {
padding: 0 18px 15px;
overflow-y: auto;
}

View File

@ -260,7 +260,8 @@ export default {
base: {
type: String,
default: ''
}
},
from: String
},
data() {
return {
@ -308,6 +309,10 @@ export default {
switch (e) {
case 'preview':
case 'edit':
if (this.from) {
this.$emit('action', e, this.config);
return;
}
const { name, theme } = this.config;
savePreviewToLocal({
type: this.type,

1
examples/extension/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -0,0 +1,18 @@
import Vue from 'vue';
import App from './editor/index';
import Element from 'main/index.js';
import 'packages/theme-chalk/src/index.scss';
export default () => {
Vue.use(Element, { zIndex: 100000 });
const root = document.createElement('div');
document.body.appendChild(root);
window.ga = function() {
console.log(arguments);
};
new Vue({ // eslint-disable-line
render: h => h(App)
}).$mount(root);
};

View File

@ -0,0 +1,6 @@
chrome.browserAction.onClicked.addListener(tab => {
chrome.tabs.executeScript(tab.id, {
file: 'entry.js'
});
})
;

View File

@ -0,0 +1,212 @@
<template>
<div class="ext-panel" :class="{moving : moving}" :style="{top: `${top}px`, left: `${left}px`}" ref="editor" >
<img class="entrance touch-icon" src="./icon-entrance.png" v-show="!showSidebar" @click="toggleSidebar" />
<img class="close touch-icon" src="./icon-close.png" v-show="showSidebar" @click="toggleSidebar" />
<div class="editor" :style="{height: `${height}px`}" v-show="showSidebar">
<el-tabs v-model="activeTab" @tab-click="onTabClick">
<el-tab-pane label="Config" name="config">
<theme-configurator
:themeConfig="themeConfig"
:previewConfig="previewConfig"
:onUserConfigUpdate="onUserConfigUpdate"
from="extension"
></theme-configurator>
</el-tab-pane>
<el-tab-pane label="Gallery" name="gallery">
<gallery
ref='gallery'
:height="height"
:width="width - 7"
@action="onGalleryAction"
/>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import ThemeConfigurator from '../../../components/theme-configurator';
import themeLoader from '../../../components/theme/loader';
import gallery from './gallery';
import { loadUserThemeFromLocal, saveUserThemeToLocal } from './utils';
import bus from '../../../bus.js';
import {
ACTION_APPLY_THEME
} from '../../../components/theme/constant.js';
let initX;
let initY;
let leftX = 0;
let topY = 25;
export default {
mixins: [themeLoader],
components: {
ThemeConfigurator,
gallery
},
data() {
return {
showSidebar: true,
previewConfig: {},
themeConfig: {},
top: topY,
left: leftX,
height: window.innerHeight - 30 * 2,
width: 0,
moving: false,
activeTab: 'config',
themeName: '',
userTheme: []
};
},
mounted() {
const editor = this.$refs.editor;
this.width = editor.offsetWidth;
leftX = window.innerWidth - 20 - this.width;
this.left = leftX;
editor.addEventListener('mousedown', e => {
initX = e.clientX;
initY = e.clientY;
leftX = this.left;
topY = this.top;
document.addEventListener('mousemove', this.moveFunc);
});
document.addEventListener('mouseup', e => {
document.removeEventListener('mousemove', this.moveFunc);
setTimeout(() => {this.moving = false;}, 0);
});
// chrome.storage.local.remove('ELEMENT_THEME_USER_CONFIG');
loadUserThemeFromLocal()
.then((result) => {
if (result) {
this.activeTab = 'gallery';
this.userTheme = result;
}
});
},
methods: {
checkLocalThemeConfig() {}, // disable mixin auto loading
toggleSidebar() {
if (this.moving) return;
this.showSidebar = !this.showSidebar;
if (!this.showSidebar) {
const windowWidth = window.innerWidth;
if (this.left + this.width * 0.5 < windowWidth * 0.5) {
this.left = 0;
} else {
this.left = windowWidth - 50;
}
} else {
this.moveEditor(this.left, this.top);
}
},
moveFunc(e) {
e.preventDefault();
const deltaX = initX - e.clientX;
const deltaY = initY - e.clientY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
this.moving = true;
}
this.moveEditor(leftX - deltaX, topY - deltaY);
},
moveEditor(x, y) {
const showSidebar = this.showSidebar;
let nextTop = y;
if (nextTop < 0) nextTop = 0;
const maxTop = window.innerHeight - (showSidebar ? (this.height + 5) : 50);
if (nextTop > maxTop) nextTop = maxTop;
this.top = nextTop;
let nextLeft = x;
if (nextLeft < 0) nextLeft = 0;
const maxLeft = window.innerWidth - (showSidebar ? (this.width + 5) : 50);
if (nextLeft > maxLeft) nextLeft = maxLeft;
this.left = nextLeft;
},
onGalleryAction(key, value) {
switch (key) {
case 'edit':
this.themeName = value.name;
this.themeConfig = JSON.parse(value.theme);
bus.$emit(ACTION_APPLY_THEME, this.themeConfig);
this.activeTab = 'config';
break;
default:
return;
}
},
onTabClick(e) {
if (e && e.name === 'gallery') {
this.$refs.gallery.init();
}
},
onUserConfigUpdate(userConfig) {
if (this.themeName) {
this.userTheme.forEach((config) => {
if (config.name === this.themeName) {
config.update = Date.now();
config.theme = JSON.stringify(userConfig);
}
});
} else {
this.themeName = `Theme-${Date.now()}`;
this.userTheme.push({
update: Date.now(),
name: this.themeName,
theme: JSON.stringify(userConfig)
});
}
saveUserThemeToLocal(this.userTheme);
}
}
};
</script>
<style scoped>
.ext-panel {
position: fixed;
background: transparent;
user-select: none;
z-index: 100000;
}
.ext-panel.moving{
cursor: move;
}
.ext-panel.moving .touch-icon{
cursor: move;
}
.ext-panel .touch-icon{
cursor: pointer;
}
.ext-panel .close {
position: absolute;
right: 0;
top: 0;
height: 20px;
width: 20px;
z-index: 100001;
}
.ext-panel .entrance {
height: 50px;
width: 50px;
}
.ext-panel .editor {
overflow: hidden;
background: #f5f7fa;
border: 1px solid #ebeef5;
border-radius: 5px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
margin: 5px 5px 0 0;
min-width: 261px;
}
</style>
<style>
.ext-panel .editor .el-tabs__content, .ext-panel .editor .el-tabs--top, .ext-panel .editor .el-tab-pane {
height: 100%;
}
.ext-panel .el-tabs__nav-scroll >div {
transform: translateX(60px)!important;
}
.ext-panel .editor-main {
padding: 0 18px 70px;
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div :style="{
height: `${height}px`,
width: `${width}px`
}"
class="main"
>
<ul
class="theme-card-list"
>
<li class="theme-card" v-for="item in userTheme" :key="item.name">
<theme-card type="user" :config="item" @action="onAction" from="extension"></theme-card>
</li>
<li class="theme-card">
<theme-card type="upload" :config="{name: 'upload'}" @action="onAction"></theme-card>
</li>
</ul>
<el-dialog :visible.sync="copyDialogVisible" :modal-append-to-body="false">
<el-form :model="copyForm" ref="copyForm" :rules="copyFormRule">
<el-form-item label="主题名称" prop="name">
<el-input v-model="copyForm.name"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeCopyForm">取消</el-button>
<el-button type="primary" @click="copyToUser">确认</el-button>
</div>
</el-dialog>
</div>
</template>
<style scoped>
ul, li {
padding: 0;
margin: 0;
}
.main {
overflow: auto;
}
.theme-card-list {
padding-bottom: 50px;
width: 90%;
margin: 0 auto;
}
.theme-card {
display: inline-block;
height: 140px;
height: 15vw;
width: 100%;
max-height: 230px;
flex: 0 0 24%;
cursor: default;
vertical-align: bottom;
}
</style>
<style>
.theme-card .theme-card-item {
margin-top: 0;
}
.theme-card .theme-card-item.is-upload {
height: 80%
}
</style>
<script>
import ThemeCard from '../../../components/theme/theme-card.vue';
import { loadUserThemeFromLocal, saveUserThemeToLocal } from './utils';
export default {
props: {
height: Number,
width: Number
},
data() {
return {
userTheme: [],
copyDialogVisible: false,
copyForm: {},
copyFormRule: {
name: [{
validator: this.validateCopyName,
trigger: 'blur'
}]
}
};
},
components: {
ThemeCard
},
mounted() {
this.init();
},
methods: {
init() {
loadUserThemeFromLocal().then(result => {
if (!result) return;
this.userTheme = result;
});
},
onAction(key, value) {
switch (key) {
case 'copy':
this.openCopyForm(value.theme);
break;
case 'upload':
this.openCopyForm(value);
break;
case 'delete':
this.$confirm('确定要删除这个主题?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteUserThemeByName(value.name);
}).catch(() => {});
break;
case 'rename':
this.openRenameForm(value.name);
break;
default:
this.$emit('action', key, value);
return;
}
},
openCopyForm(theme) {
this.copyForm.theme = theme;
this.copyDialogVisible = true;
},
openRenameForm(name) {
this.copyForm.oldname = name;
this.copyDialogVisible = true;
},
closeCopyForm() {
this.copyDialogVisible = false;
this.$nextTick(() => {
this.copyForm = {};
});
},
validateCopyName(rule, value, callback) {
if (!value) {
callback(new Error('主题名称是必填项'));
} else if (this.filterUserThemeByName(value).length > 0) {
callback(new Error('主题名称重复'));
} else {
callback();
}
},
copyToUser() {
this.$refs.copyForm.validate((valid) => {
if (valid) {
const { theme, name, oldname } = this.copyForm;
if (theme) {
// copy
this.userTheme.push({
update: Date.now(),
name,
theme
});
} else {
// rename
this.userTheme.forEach((config) => {
if (config.name === oldname) {
config.update = Date.now();
config.name = name;
}
});
}
this.saveToLocal();
this.closeCopyForm();
}
});
},
filterUserThemeByName(name, include = true) {
return this.userTheme.filter((theme) => (include ? theme.name === name : theme.name !== name));
},
saveToLocal() {
saveUserThemeToLocal(this.userTheme);
},
deleteUserThemeByName(name) {
this.userTheme = this.filterUserThemeByName(name, false);
this.saveToLocal();
}
}
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,13 @@
<template>
<editor />
</template>
<script>
import editor from './editor';
export default {
components: {
editor
}
};
</script>

View File

@ -0,0 +1,18 @@
const ELEMENT_THEME_USER_CONFIG = 'ELEMENT_THEME_USER_CONFIG';
export const loadFromLocal = (key) => {
return new window.Promise((resolve) => {
chrome.storage.local.get([key], (result) => {
resolve(result[key]);
});
});
};
export const saveToLocal = (key, value) => {
chrome.storage.local.set({[key]: value});
};
export const loadUserThemeFromLocal = () => {
return loadFromLocal(ELEMENT_THEME_USER_CONFIG);
};
export const saveUserThemeToLocal = (value) => {
saveToLocal(ELEMENT_THEME_USER_CONFIG, value);
};

View File

@ -0,0 +1,7 @@
import init from './app';
if (!window.ElementThemeRollerInit) {
window.ElementThemeRollerInit = true;
init();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,27 @@
{
"name": "Element Theme Roller",
"version": "0.0.9",
"description": "Customize all Design Tokens of Element UI and preview in real-time",
"web_accessible_resources": ["entry.js"],
"background": {
"scripts": [
"background.js"
],
"persistent": true
},
"icons": {
"128": "icon.png"
},
"browser_action": {
"default_icon": "icon.png",
"default_title": "Element Theme Roller"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"manifest_version": 2,
"permissions": [
"activeTab",
"storage",
"https://*.ele.me/",
"https://*.elenet.me/"
]
}

View File

@ -18,6 +18,8 @@
"build:umd": "node build/bin/build-locale.js",
"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
"deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME",
"deploy:extension": "cross-env NODE_ENV=production webpack --config build/webpack.extension.js",
"dev:extension": "rimraf examples/extension/dist && cross-env NODE_ENV=development webpack --watch --config build/webpack.extension.js",
"dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
"dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js",
"dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",