Merge pull request #128 from samuelthomas2774/css-editor

Add SCSS and saving support to the CSS editor
This commit is contained in:
Alexei Stukov 2018-02-14 00:06:00 +02:00 committed by GitHub
commit 13e9c53520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 216 additions and 103 deletions

View File

@ -9,18 +9,70 @@
*/ */
import { ClientIPC } from 'common'; import { ClientIPC } from 'common';
import Settings from './settings';
import { DOM } from 'ui'; import { DOM } from 'ui';
export default class { export default class {
static async show() { static init() {
const t = await ClientIPC.send('openCssEditor', {}); ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss }));
ClientIPC.send('setCss', { css: DOM.getStyleCss('bd-customcss') }); ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds));
ClientIPC.on('bd-update-css', this.updateCss); ClientIPC.on('bd-save-scss', async (e, scss) => {
await this.updateScss(scss);
await this.save();
});
} }
static updateCss(e, css) { static async show() {
await ClientIPC.send('openCssEditor', this.editor_bounds);
}
static updateScss(scss, sendSource) {
if (sendSource)
this.sendToEditor('set-scss', { scss });
return new Promise((resolve, reject) => {
this.compile(scss).then(css => {
this.css = css;
this._scss = scss;
this.sendToEditor('scss-error', null);
resolve();
}).catch(err => {
this.sendToEditor('scss-error', err);
reject(err);
});
});
}
static async save() {
Settings.saveSettings();
}
static saveEditorBounds(bounds) {
this.editor_bounds = bounds;
Settings.saveSettings();
}
static async compile(scss) {
return await ClientIPC.send('bd-compileSass', { data: scss });
}
static async sendToEditor(channel, data) {
return await ClientIPC.send('sendToCssEditor', { channel, data });
}
static get scss() {
return this._scss || '';
}
static set scss(scss) {
this.updateScss(scss, true);
}
static set css(css) {
DOM.injectStyle(css, 'bd-customcss'); DOM.injectStyle(css, 'bd-customcss');
} }
} }

View File

@ -10,7 +10,7 @@
/*Module Manager initializes all modules when everything is ready*/ /*Module Manager initializes all modules when everything is ready*/
import { Events, SocketProxy, EventHook } from 'modules'; import { Events, SocketProxy, EventHook, CssEditor } from 'modules';
import { ProfileBadges } from 'ui'; import { ProfileBadges } from 'ui';
export default class { export default class {
@ -19,7 +19,8 @@ export default class {
return this._modules ? this._modules : (this._modules = [ return this._modules ? this._modules : (this._modules = [
new ProfileBadges(), new ProfileBadges(),
new SocketProxy(), new SocketProxy(),
new EventHook() new EventHook(),
CssEditor
]); ]);
} }

View File

@ -9,7 +9,8 @@
*/ */
import defaultSettings from '../data/user.settings.default'; import defaultSettings from '../data/user.settings.default';
import { default as Globals } from './globals'; import Globals from './globals';
import CssEditor from './csseditor';
import { FileUtils, ClientLogger as Logger } from 'common'; import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path'; import path from 'path';
@ -20,7 +21,7 @@ export default class {
const settingsPath = path.resolve(this.dataPath, 'user.settings.json'); const settingsPath = path.resolve(this.dataPath, 'user.settings.json');
const user_config = await FileUtils.readJsonFromFile(settingsPath); const user_config = await FileUtils.readJsonFromFile(settingsPath);
const { settings } = user_config; const { settings, scss, css_editor_bounds } = user_config;
this.settings = defaultSettings; this.settings = defaultSettings;
@ -40,6 +41,9 @@ export default class {
} }
} }
} }
CssEditor.updateScss(scss, true);
CssEditor.editor_bounds = css_editor_bounds;
} catch (err) { } catch (err) {
// There was an error loading settings // There was an error loading settings
// This probably means that the user doesn't have any settings yet // This probably means that the user doesn't have any settings yet
@ -68,7 +72,14 @@ export default class {
}; };
}) })
}; };
}) }),
scss: CssEditor.scss,
css_editor_bounds: {
width: CssEditor.editor_bounds.width,
height: CssEditor.editor_bounds.height,
x: CssEditor.editor_bounds.x,
y: CssEditor.editor_bounds.y
}
}); });
} catch (err) { } catch (err) {
// There was an error loading settings // There was an error loading settings

View File

@ -105,7 +105,7 @@ class Theme {
let css = ''; let css = '';
if (this.info.type === 'sass') { if (this.info.type === 'sass') {
css = await ClientIPC.send('bd-compileSass', { css = await ClientIPC.send('bd-compileSass', {
scss: ThemeManager.getConfigAsSCSS(this.themeConfig), data: ThemeManager.getConfigAsSCSS(this.themeConfig),
path: this.paths.mainPath.replace(/\\/g, '/') path: this.paths.mainPath.replace(/\\/g, '/')
}); });
console.log(css); console.log(css);

View File

@ -45,7 +45,8 @@ console.log(dummyArgs);
class Comms { class Comms {
constructor() { constructor(bd) {
this.bd = bd;
this.initListeners(); this.initListeners();
} }
@ -54,8 +55,11 @@ class Comms {
o.reply(Common.Config.config); o.reply(Common.Config.config);
}); });
BDIpc.on('bd-openCssEditor', o => CSSEditor.openEditor(o)); BDIpc.on('bd-sendToDiscord', event => this.bd.windowUtils.send(event.args.channel, event.args.message));
BDIpc.on('bd-setCss', o => CSSEditor.setCSS(o.args));
BDIpc.on('bd-openCssEditor', o => this.bd.csseditor.openEditor(o));
// BDIpc.on('bd-setScss', o => this.bd.csseditor.setSCSS(o.args.scss));
BDIpc.on('bd-sendToCssEditor', o => this.bd.csseditor.send(o.args.channel, o.args.data));
BDIpc.on('bd-readFile', this.readFile); BDIpc.on('bd-readFile', this.readFile);
BDIpc.on('bd-readJson', o => this.readFile(o, true)); BDIpc.on('bd-readJson', o => this.readFile(o, true));
@ -67,11 +71,13 @@ class Comms {
}); });
BDIpc.on('bd-compileSass', o => { BDIpc.on('bd-compileSass', o => {
const { scss, path } = o.args; if (!o.args.data && !o.args.path) return;
if (!scss && !path) return; if (o.args.path && o.args.data) {
const data = scss ? `${scss} @import '${path}';` : `@import '${path}';`; o.args.data = `${o.args.data} @import '${o.args.path}';`;
o.args.path = undefined;
}
sass.render({ data }, (err, result) => { sass.render(o.args, (err, result) => {
if (err) { if (err) {
o.reply({ err }); o.reply({ err });
return; return;
@ -100,10 +106,17 @@ class Comms {
class BetterDiscord { class BetterDiscord {
constructor(args) { constructor(args) {
if (BetterDiscord.loaded) {
// Creating two BetterDiscord objects???
console.log('Creating two BetterDiscord objects???');
return null;
}
BetterDiscord.loaded = true;
this.injectScripts = this.injectScripts.bind(this); this.injectScripts = this.injectScripts.bind(this);
this.ignite = this.ignite.bind(this); this.ignite = this.ignite.bind(this);
Common.Config = new Config(args || dummyArgs); Common.Config = new Config(args || dummyArgs);
this.comms = new Comms(); this.comms = new Comms(this);
this.init(); this.init();
} }
@ -111,6 +124,8 @@ class BetterDiscord {
const window = await this.waitForWindow(); const window = await this.waitForWindow();
this.windowUtils = new WindowUtils({ window }); this.windowUtils = new WindowUtils({ window });
this.csseditor = new CSSEditor(this);
//Log some events for now //Log some events for now
//this.windowUtils.webContents.on('did-start-loading', e => this.windowUtils.executeJavascript(`console.info('did-start-loading');`)); //this.windowUtils.webContents.on('did-start-loading', e => this.windowUtils.executeJavascript(`console.info('did-start-loading');`));
//this.windowUtils.webContents.on('did-stop-loading', e => this.windowUtils.executeJavascript(`console.info('did-stop-loading');`)); //this.windowUtils.webContents.on('did-stop-loading', e => this.windowUtils.executeJavascript(`console.info('did-stop-loading');`));
@ -128,8 +143,6 @@ class BetterDiscord {
this.windowUtils.send('did-navigate-in-page', { event, url, isMainFrame }); this.windowUtils.send('did-navigate-in-page', { event, url, isMainFrame });
}); });
BDIpc.on('bd-sendToDiscord', event => this.windowUtils.send(event.args.channel, event.args.message))
setTimeout(() => { setTimeout(() => {
if (__DEV) { this.injectScripts(); } if (__DEV) { this.injectScripts(); }
}, 500); }, 500);
@ -180,4 +193,4 @@ class BetterDiscord {
module.exports = { module.exports = {
BetterDiscord BetterDiscord
} };

View File

@ -12,9 +12,15 @@ const path = require('path');
const { BrowserWindow } = require('electron'); const { BrowserWindow } = require('electron');
const { Module } = require('./modulebase'); const { Module } = require('./modulebase');
const { WindowUtils } = require('./utils');
class CSSEditor extends Module { class CSSEditor extends Module {
constructor(bd) {
super();
this.bd = bd;
}
openEditor(o) { openEditor(o) {
if (this.editor) { if (this.editor) {
if (this.editor.isFocused()) return; if (this.editor.isFocused()) return;
@ -25,33 +31,45 @@ class CSSEditor extends Module {
return; return;
} }
this.editor = new BrowserWindow(this.options); const options = this.options;
this.editor.loadURL(`file://${this.editorPath}/index.html`); for (let option in o.args) {
this.editor.open = true; options[option] = o.args[option];
this.editor.setSheetOffset(33); }
this.editor.webContents.on('close', () => { this.editor = new BrowserWindow(options);
this.editor.loadURL('about:blank');
this.editor.setSheetOffset(33);
this.editorUtils = new WindowUtils({ window: this.editor });
this.editor.on('close', () => {
this.bd.windowUtils.send('bd-save-csseditor-bounds', this.editor.getBounds());
this.editor = null; this.editor = null;
}); });
this.editor.once('ready-to-show', () => { this.editor.once('ready-to-show', () => {
this.editor.show() this.editor.show();
}); });
this.editor.webContents.on('did-finish-load', () => { this.editor.webContents.on('did-finish-load', () => {
this.editorUtils.injectScript(path.join(this.editorPath, 'csseditor.js'));
o.reply(true); o.reply(true);
}); });
} }
setCSS(css) { setSCSS(scss) {
this.editor.webContents.send("set-css", css); this.send('set-scss', scss);
}
send(channel, data) {
if (!this.editor) return;
this.editor.webContents.send(channel, data);
} }
set alwaysOnTop(state) { set alwaysOnTop(state) {
if (!this.editor) return;
this.editor.setAlwaysOnTop(state); this.editor.setAlwaysOnTop(state);
} }
//TODO user options from config
get options() { get options() {
return { return {
width: 800, width: 800,
@ -69,4 +87,4 @@ class CSSEditor extends Module {
} }
module.exports = { 'CSSEditor': new CSSEditor() }; module.exports = { CSSEditor };

View File

@ -133,8 +133,12 @@ class WindowUtils extends Module {
injectScript(fpath, variable) { injectScript(fpath, variable) {
console.log(`Injecting: ${fpath}`); console.log(`Injecting: ${fpath}`);
if (variable) this.executeJavascript(`${variable} = require("${fpath}");`);
else this.executeJavascript(`require("${fpath}");`); const escaped_path = fpath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const escaped_variable = variable ? variable.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : null;
if (variable) this.executeJavascript(`window["${escaped_variable}"] = require("${escaped_path}");`);
else this.executeJavascript(`require("${escaped_path}");`);
} }
events(event, callback) { events(event, callback) {
@ -152,4 +156,4 @@ module.exports = {
Utils, Utils,
FileUtils, FileUtils,
WindowUtils WindowUtils
} };

View File

@ -1,6 +1,6 @@
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
class BDIpc { export default class {
static on(channel, cb) { static on(channel, cb) {
ipcRenderer.on(channel, (event, args) => cb(event, args)); ipcRenderer.on(channel, (event, args) => cb(event, args));
@ -21,6 +21,8 @@ class BDIpc {
}); });
} }
} static sendToDiscord(channel, message) {
this.send('bd-sendToDiscord', { channel, message });
}
module.exports = { BDIpc }; }

View File

@ -8,7 +8,7 @@
<div class="title">CSS Editor</div> <div class="title">CSS Editor</div>
<div class="flex-spacer"></div> <div class="flex-spacer"></div>
<div class="controls"> <div class="controls">
<button :class="{active: alwaysOnTop}"ref="aot" title="Toggle always on top" @click="toggleaot">P</button> <button :class="{active: alwaysOnTop}" ref="aot" title="Toggle always on top" @click="toggleaot">P</button>
<button title="Close CSS Editor" @click="close">X</button> <button title="Close CSS Editor" @click="close">X</button>
</div> </div>
</div> </div>
@ -16,19 +16,16 @@
<div class="valign">Loading Please Wait...</div> <div class="valign">Loading Please Wait...</div>
</div> </div>
<div id="editor" class="editor"> <div id="editor" class="editor">
<codemirror <codemirror ref="mycm" :options="cmOptions" @input="cmOnChange" />
ref="mycm"
:options="cmOptions"
@input="cmOnChange"
/>
</div> </div>
<div class="parser-error" v-if="error">{{ error.formatted }}</div>
<div class="tools"> <div class="tools">
<div class="flex-row"> <div class="flex-row">
<button @click="save">Save</button> <button @click="save">Save</button>
<button @click="update">Update</button> <button @click="update">Update</button>
<div class="flex-spacer"></div> <div class="flex-spacer"></div>
<div id="chkboxLiveUpdate"> <div id="chkboxLiveUpdate">
<input type="checkbox" @click="toggleLiveUpdate" :checked="liveUpdate"><span>Live Update</span> <input type="checkbox" @click="toggleLiveUpdate" :checked="liveUpdate" /><span>Live Update</span>
</div> </div>
</div> </div>
</div> </div>
@ -45,11 +42,9 @@
import '../../node_modules/codemirror/addon/dialog/dialog.js'; import '../../node_modules/codemirror/addon/dialog/dialog.js';
import '../../node_modules/codemirror/addon/hint/show-hint.js'; import '../../node_modules/codemirror/addon/hint/show-hint.js';
import BDIpc from './BDIpc';
const { remote } = window.require('electron'); const { remote } = window.require('electron');
const { BDIpc } = require('./BDIpc');
function sendToDiscord(channel, message) {
BDIpc.send('bd-sendToDiscord', { channel, message });
}
const ExcludedIntelliSenseTriggerKeys = { const ExcludedIntelliSenseTriggerKeys = {
'8': 'backspace', '8': 'backspace',
@ -103,49 +98,7 @@
'222': 'quote' '222': 'quote'
}; };
/*Methods*/
function save() {
const css = this.codemirror.getValue();
sendToDiscord('save-css', css);
}
function update() {
const css = this.codemirror.getValue();
sendToDiscord('update-css', css);
}
function toggleaot() {
this.alwaysOnTop = !this.alwaysOnTop;
remote.getCurrentWindow().setAlwaysOnTop(this.alwaysOnTop);
}
function close() {
window.close();
}
function setCss(css) {
this.loading = false;
this.codemirror.setValue(css || '');
}
function cmOnChange(value) {
if(this.liveUpdate) sendToDiscord('update-css', value);
}
function cmOnKeyUp(editor, event) {
if (event.ctrlKey) return;
if (ExcludedIntelliSenseTriggerKeys[event.keyCode]) return;
cmCommands.autocomplete(editor, null, { completeSingle: false });
}
function toggleLiveUpdate(e) {
this.liveUpdate = !this.liveUpdate;
}
const methods = { save, update, toggleaot, close, setCss, cmOnChange, cmOnKeyUp, toggleLiveUpdate };
export default { export default {
methods,
data() { data() {
return { return {
loading: true, loading: true,
@ -155,7 +108,7 @@
cmOptions: { cmOptions: {
indentUnit: 4, indentUnit: 4,
tabSize: 4, tabSize: 4,
mode: 'css', mode: 'text/x-scss',
lineNumbers: true, lineNumbers: true,
theme: 'material', theme: 'material',
scrollbarStyle: 'overlay', scrollbarStyle: 'overlay',
@ -165,7 +118,8 @@
dialog: { dialog: {
'position': 'bottom' 'position': 'bottom'
} }
} },
error: null
} }
}, },
computed: { computed: {
@ -176,18 +130,57 @@
return this.$refs.mycm; return this.$refs.mycm;
} }
}, },
mounted: function () { mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp); this.codemirror.on('keyup', this.cmOnKeyUp);
BDIpc.on('set-css', (_, data) => { BDIpc.on('set-scss', (_, data) => {
if (data.error) { if (data.error) {
console.log(data.error); console.log(data.error);
return; return;
} }
console.log(data); console.log(data);
this.setCss(data.css); this.setScss(data.scss);
}); });
BDIpc.send('get-css'); BDIpc.sendToDiscord('get-scss');
BDIpc.on('scss-error', (_, err) => {
this.error = err;
this.$forceUpdate();
if (err)
console.error('SCSS parse error:', err);
});
},
methods: {
save() {
const scss = this.codemirror.getValue();
BDIpc.sendToDiscord('save-scss', scss);
},
update() {
const scss = this.codemirror.getValue();
BDIpc.sendToDiscord('update-scss', scss);
},
toggleaot() {
this.alwaysOnTop = !this.alwaysOnTop;
remote.getCurrentWindow().setAlwaysOnTop(this.alwaysOnTop);
},
close() {
window.close();
},
setScss(scss) {
this.loading = false;
this.codemirror.setValue(scss || '');
},
cmOnChange(value) {
if(this.liveUpdate) BDIpc.sendToDiscord('update-scss', value);
},
cmOnKeyUp(editor, event) {
if (event.ctrlKey) return;
if (ExcludedIntelliSenseTriggerKeys[event.keyCode]) return;
cmCommands.autocomplete(editor, null, { completeSingle: false });
},
toggleLiveUpdate(e) {
this.liveUpdate = !this.liveUpdate;
}
} }
} }
</script> </script>

View File

@ -1,15 +1,21 @@
const styles = require('./styles/index.scss'); // const styles = require('./styles/index.scss');
import Vue from 'vue'; import Vue from 'vue';
import VueCodemirror from 'vue-codemirror';
import Editor from './Editor.vue'; import Editor from './Editor.vue';
import VueCodemirror from 'vue-codemirror' import styles from './styles/index.scss';
Vue.use(VueCodemirror, {}); Vue.use(VueCodemirror, {});
window.cmCommands = VueCodemirror.CodeMirror.commands; window.cmCommands = VueCodemirror.CodeMirror.commands;
new Vue({ const mount = document.createElement('div');
el: '#root', mount.classList.add('container');
document.body.appendChild(mount);
const vue = new Vue({
el: mount,
components: { Editor }, components: { Editor },
template: '<Editor/>' template: '<Editor/>'
}); });

View File

@ -1,3 +1,13 @@
.parser-error {
padding: 4px 6px;
background: #292b2f;
border-top: 1px solid hsla(218,5%,47%,.3);
color: #d84040;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
white-space: pre-wrap;
font-size: 12px;
}
.tools { .tools {
height: 36px; height: 36px;
background: #292b2f; background: #292b2f;
@ -24,16 +34,18 @@
cursor: pointer; cursor: pointer;
border: 0; border: 0;
margin-right: 4px; margin-right: 4px;
flex: 0 0 auto;
&:hover { &:hover {
background: #44474e; background: #44474e;
color: #FFF; color: #fff;
} }
} }
#chkboxLiveUpdate { #chkboxLiveUpdate {
padding: 3px 10px; padding: 3px 10px;
line-height: 22px; line-height: 22px;
flex: 0 0 auto;
input[type="checkbox"] { input[type="checkbox"] {
margin: 0 6px 0 0; margin: 0 6px 0 0;

View File

@ -26,4 +26,4 @@ module.exports = {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js') vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
} }
} }
}; };

View File

@ -49,6 +49,7 @@
"build": "npm run build --prefix client && npm run build --prefix core && npm run build --prefix csseditor", "build": "npm run build --prefix client && npm run build --prefix core && npm run build --prefix csseditor",
"watch_client": "npm run watch --prefix client", "watch_client": "npm run watch --prefix client",
"watch_core": "npm run watch --prefix core", "watch_core": "npm run watch --prefix core",
"watch_csseditor": "npm run watch --prefix csseditor",
"lint": "eslint -f unix client/src core/src csseditor/src", "lint": "eslint -f unix client/src core/src csseditor/src",
"test": "npm run build && npm run lint", "test": "npm run build && npm run lint",
"build_node-sass": "node scripts/build-node-sass.js" "build_node-sass": "node scripts/build-node-sass.js"