whiteboard-app
Version:
Whiteboard - a slide-based activity presentation system
318 lines (280 loc) • 9.54 kB
JavaScript
const fs = require('fs');
const pathlib = require('path');
const glob = require('glob');
const { ModuleBase } = require('elmoed');
const { glyphIcon } = require('../utils');
function splitText(infoText) {
const text = infoText.trim();
if (text.includes('\n')) {
// Split by newline
return text.split('\n').map(s => s.trim());
} else if (text.includes(',')) {
// Split by comma
return text.split(',').map(s => s.trim());
} else if (text.length > 0) {
// Just return single element
return [text];
}
// Empty string = empty list
return [];
}
const DEFAULT_FONT_SIZE = 28;
const DEFAULT_THEME = 'light';
const _makeDefaultEmpty = () => ({
path: 'NEW',
text: '',
edited: false,
_default: true,
});
class Editor extends ModuleBase {
constructor(...args) {
super(...args);
this.tabs = null;
this.activeFilePath = null;
this.font_size = DEFAULT_FONT_SIZE;
this.theme = DEFAULT_THEME;
}
load(callback, text) {
// Already loaded
if (this.tabs !== null) {
callback();
return;
}
// Converts the text into a list of files, some of which might
// be globs. These it attempts to resolve and put all in paths.
const fileList = splitText(text);
const globs = fileList.filter(glob.hasMagic);
const normalPaths = fileList.filter(path => !glob.hasMagic(path));
const globbedPaths = globs.map(
gpath => glob.sync(gpath.trim(), { nodir: true }));
const allPaths = Array.prototype.concat.apply(normalPaths, globbedPaths);
this.setupEvents();
this.tabs = [];
this.addTabs(allPaths, callback);
}
getRelativePath(path) {
const basepath = pathlib.dirname(this.getRealPath());
return pathlib.relative(basepath, path);
}
serialized() {
const paths = this.tabs.map(tab => this.getRelativePath(tab.path));
let sep = ',';
if (paths.join().length > 80) {
// Too many files go on separate lines
sep = '\n';
}
return paths.join(sep);
}
addTabs(paths, callback) {
// "empty state" of just a single empty placeholder tab
if (this.tabs.length === 1
&& this.tabs[0]._default
&& this.tabs[0].text.length < 1) {
this.activeFilePath = null; // reset active file
this.tabs = [];
}
// ////////////////////
// TODO: For some reason, this is buggy on macOS, and I can't test it,
// so I'm just adding extra chdir here to try to fix it
const basepath = pathlib.dirname(this.getRealPath());
process.chdir(basepath);
// Hack, DELETEME when properly fixed
// ////////////////////
// Remove existing versions of tabs (in the case of dupes)
// TODO: make async
this.tabs = this.tabs.concat(paths.map((partialPath) => {
const path = pathlib.resolve(partialPath.trim());
let text = '';
if (fs.existsSync(path)) {
text = fs.readFileSync(path).toString();
}
return { path, text, edited: false };
}));
// Ensure we never have an empty tab list
if (this.tabs.length === 0) {
this.tabs = [_makeDefaultEmpty()];
}
// If nothing is open, auto-open first thing
if (this.activeFilePath === null) {
this.activeFilePath = this.tabs[0].path;
}
callback();
}
setupEvents() {
this.on('change_tab', (event, updatedText, currentPath, newPath) => {
if (currentPath !== this.activeFilePath) {
// Shouldn't happen any more, but just in case leaving
// this in here
this.activeFilePath = currentPath;
}
this.setText(this.activeFilePath, updatedText);
this.activeFilePath = newPath;
this.update();
});
this.on('save', (event, updatedText) => {
this.saveText(this.activeFilePath, updatedText);
});
this.on('saveas', (event, updatedText) => {
this.changeActiveFilePath(() => {
this.saveText(this.activeFilePath, updatedText);
this.update();
});
});
}
saveText(path, newText) {
// Saves the given path with the new text
const activeFile = this.getFile(path);
activeFile.edited = false;
activeFile.text = newText;
fs.writeFile(activeFile.path, newText, (err) => {
if (err) {
console.error('error writing file: ', err);
}
});
}
closeTab() {
// Saves the given path with the new text
this.tabs = this.tabs.filter(({ path }) => path !== this.activeFilePath);
if (this.tabs.length === 0) {
this.tabs = [_makeDefaultEmpty()];
}
this.activeFilePath = this.tabs[0].path;
this.update();
}
openFiles() {
const { dialog } = this.manager.electron;
dialog.showOpenDialog({
title: 'Open new tab(s)...',
properties: ['openFile', 'multiSelections'],
}, (filePaths) => {
if (!filePaths) {
return; // Canceled
}
this.addTabs(filePaths, () => this.update());
});
}
changeActiveFilePath(callback) {
const { dialog } = this.manager.electron;
const opts = {};
// Hacky, check if this is a newly created file, if so, give no default
// path
if (this.activeFilePath !== 'NEW' &&
this.activeFilePath !== 'new editor') {
opts.defaultPath = this.activeFilePath;
}
dialog.showSaveDialog(opts, (newPath) => {
if (!newPath) {
return; // Canceled
}
// Reassigns the path
const activeFile = this.getFile(this.activeFilePath);
activeFile.path = newPath;
this.activeFilePath = newPath;
callback();
});
}
getContextMenu() {
const fontSizeSubmenu = number => ({
label: `${number} pt`,
type: 'radio',
click: () => {
this.font_size = number;
this.update();
},
checked: this.font_size === number,
});
const themeSubmenu = (themeLabel, themeName) => ({
label: themeLabel,
type: 'radio',
click: () => {
this.theme = themeName;
this.update();
},
checked: this.theme === themeName,
});
return [
{
label: 'Font Size',
icon: glyphIcon('zoom-in'),
submenu: [
fontSizeSubmenu(11),
fontSizeSubmenu(14),
fontSizeSubmenu(18),
fontSizeSubmenu(24),
fontSizeSubmenu(28),
fontSizeSubmenu(32),
],
},
{
label: 'Theme',
icon: glyphIcon('color-picker'),
submenu: [
themeSubmenu('Light', 'light'),
themeSubmenu('Dark', 'dark'),
],
},
{
label: 'Save',
icon: glyphIcon('floppy-disk'),
accelerator: 'CommandOrControl+S',
click: () => this.send('trigger_save'),
},
{
label: 'Save as...',
icon: glyphIcon('floppy-disk'),
accelerator: 'CommandOrControl+Shift+S',
click: () => this.send('trigger_save_as'),
},
{
label: 'Open file(s)...',
icon: glyphIcon('folder-open'),
accelerator: 'CommandOrControl+O',
click: () => this.openFiles(),
},
{
label: 'Close tab',
icon: glyphIcon('delete'),
accelerator: 'CommandOrControl+W',
click: () => this.closeTab(),
},
];
}
setText(path, newText) {
const activeFile = this.getFile(path);
activeFile.edited = true;
activeFile.text = newText;
}
getFile(path) {
const file = this.tabs.find(tab => tab.path === path);
if (!file) {
console.error('Cannot find file ', path);
}
return file;
}
getActiveFileText() {
return this.getFile(this.activeFilePath).text;
}
includeInsertMenu() {
return true;
}
static getPNGIconPath() {
return glyphIcon('code');
}
static getIconicPreview() {
return '<img src="svg/si-glyph-code.svg"/>';
}
getProps() {
const tabs = this.tabs
.map(file => ({
title: pathlib.basename(file.path),
path: file.path,
active: file.path === this.activeFilePath,
}));
const path = this.activeFilePath;
const text = this.getActiveFileText();
const { font_size } = this;
const { theme } = this;
return { path, tabs, text, font_size, theme };
}
}
module.exports = Editor;