camunda-modeler
Version:
Camunda Modeler for BPMN, DMN and CMMN, based on bpmn.io
403 lines (301 loc) • 8.8 kB
JavaScript
'use strict';
var debug = require('debug')('multi-editor-tab');
var inherits = require('inherits');
var h = require('vdom/h');
var isUnsaved = require('util/file/is-unsaved'),
replaceFileExt = require('util/file/replace-file-ext'),
ensureOpts = require('util/ensure-opts');
var Tab = require('base/components/tab');
var assign = require('lodash/object/assign'),
find = require('lodash/collection/find');
/**
* A tab holding a number of editors for a given file.
*
* @param {Object} options
*/
function MultiEditorTab(options) {
ensureOpts([
// externals / communication structure
// communicate via events not this.emit :'-(
'events',
'dialog',
// editor tab definition (file is optional)
'id',
'editorDefinitions'
], options);
Tab.call(this, options);
this.editors = this.createEditors(options);
this.setEditor(this.editors[0]);
// initialize with passed file, if any
if (options.file) {
this.setFile(options.file);
}
this.on('focus', () => {
if (!this.activeEditor && this.editors.length) {
this.showEditor(this.editors[0]);
} else {
this.activeEditor.update();
}
this.activeEditor.emit('focus');
});
this.on('destroy', () => {
if (this.editors) {
this.editors.forEach(function(editor) {
editor.destroy();
});
}
this._globalListeners.forEach((gl) => {
this.events.removeListener(gl.eventName, gl.callback);
});
this._globalListeners = [];
});
}
inherits(MultiEditorTab, Tab);
module.exports = MultiEditorTab;
/**
* Export the tab contents as the given type.
*
* Type can be any of png|svg|... depending on what
* the currently active underlying editor supports.
*
* @param {String} type
* @param {Function} done to be invoked with (err, exportedFile)
*/
MultiEditorTab.prototype.exportAs = function(type, done) {
var activeEditor = this.activeEditor;
if (!activeEditor.exportAs) {
return done(unsupportedExportAs());
}
activeEditor.exportAs(type, (err, newFile) => {
if (err) {
this.logger.error('%s', err.message);
return done(err);
}
done(null, assign({}, newFile, withExtension(this.file, type)));
});
};
/**
* Show a contained editor by editor id or direct reference.
*
* @param {String|BaseEditor} editor
*/
MultiEditorTab.prototype.showEditor = function(editor) {
if (typeof editor === 'string') {
editor = this.getEditor(editor);
}
var newEditor = editor,
oldEditor = this.activeEditor;
// no need to switch editors, if same
if (newEditor === oldEditor) {
return;
}
debug('[#showEditor] %s', newEditor.id);
// export old editor contents
oldEditor.saveXML((err, xml) => {
if (err) {
debug('[#showEditor] editor export error %s', err);
this.dialog.exportError(err, function() {});
return;
}
this.switchEditor(newEditor, xml);
});
};
MultiEditorTab.prototype.switchEditor = function(newEditor, xml) {
// set new editor
this.setEditor(newEditor);
// sync XML contents
newEditor.setXML(xml);
};
/**
* Get an editor by editor id.
*
* @param {String|BaseEditor} id
*
* @return {BaseEditor}
*/
MultiEditorTab.prototype.getEditor = function(id) {
var editor;
if (typeof id === 'string') {
editor = find(this.editors, { id: this.id + '#' + id });
} else {
editor = id;
}
if (!editor) {
throw new Error('no editor ' + id);
}
return editor;
};
// TODO(nikku): <REALLY BAD NAME....> we are going to set the _ACTIVE_ editor here
MultiEditorTab.prototype.setEditor = function(editor) {
this.activeEditor = editor;
this.emit('focus');
this.events.emit('changed');
};
/**
* Create and wire the editors this tab consists of.
*
* @param {Object} options
*
* @return {Array<Object>} editors
*/
MultiEditorTab.prototype.createEditors = function(options) {
debug('create editors', options.editorDefinitions);
this._globalListeners = [];
var editors = options.editorDefinitions.map((definition) => {
var id = definition.id,
opts = assign({}, options, {
id: options.id + '#' + id,
shortId: id,
label: definition.label
});
var EditorComponent = definition.component;
var editor = new EditorComponent(opts);
if (definition.isFallback) {
this.fallbackEditor = editor;
}
// handle import errors
editor.on('shown', (context) => {
var dialog = this.dialog;
// don't do anything if the current editor is the fallback editor
if (editor === this.fallbackEditor) {
return;
}
// context can be undefined here if lastImport is undefined
var error = context ? context.error : null;
if (error) {
this.logger.error('failed to import content for file "%s"', this.file.name);
this.logger.error('%s', error.message);
// show import error dialog
dialog.importError(this.file.name, error.message, (err, answer) => {
if (err) {
return;
}
debug('reset to fallback editor');
// switch to fallback editor
this.setEditor(this.fallbackEditor);
});
}
});
editor.on('layout:changed', this.events.composeEmitter('layout:update'));
editor.on('state-updated', (state) => {
this.dirty = this.isUnsaved() || state.dirty;
var newState = assign({
diagramType: this.getDiagramType(),
save: true,
closable: true
}, state, { dirty: this.dirty });
this.events.emit('tools:state-changed', this, newState);
});
editor.on('changed', this.events.composeEmitter('changed'));
editor.on('log:toggle', this.events.composeEmitter('log:toggle'));
editor.on('context-menu:open', this.events.composeEmitter('context-menu:open'));
/**
* messages = [ [ category, message ]* ]
*/
editor.on('log', (messages) => {
messages.forEach((m) => {
this.logger[m[0]](m[1]);
});
});
this._globalListeners.push({
eventName: 'window:resized',
callback: function() {
editor.emit('window:resized');
}
});
this._globalListeners.push({
eventName: 'layout:update',
callback: function() {
editor.emit('layout:update');
}
});
return editor;
});
this._globalListeners.forEach((gl) => {
this.events.on(gl.eventName, gl.callback);
});
return editors;
};
/**
* Save the tab, calling back with (err, file).
*
* @param {Function} done
*/
MultiEditorTab.prototype.save = function(done) {
var activeEditor = this.activeEditor;
if (!activeEditor.saveXML) {
return done(unsupportedSave());
}
activeEditor.saveXML((err, xml) => {
if (err) {
return done(err);
}
return done(null, assign({}, this.file, { contents: xml }));
});
};
MultiEditorTab.prototype.setFile = function(file) {
this.file = file;
this.label = file.name;
this.title = file.path;
this.dirty = isUnsaved(file);
this.editors.forEach(function(editor) {
editor.setXML(file.contents, {});
});
this.events.emit('changed');
};
MultiEditorTab.prototype.isUnsaved = function() {
return isUnsaved(this.file);
};
MultiEditorTab.prototype.getDiagramType = function() {
return this.file && this.file.fileType;
};
MultiEditorTab.prototype.triggerAction = function(action, options) {
if (this.activeEditor.triggerAction) {
this.activeEditor.triggerAction(action, options);
}
};
MultiEditorTab.prototype.render = function() {
var compose = this.compose;
return (
<div className="multi-editor-tab tabbed">
<div className="content">
{ h(this.activeEditor) }
</div>
<div className="tabs">
{
this.editors.map((editor) => {
return (
<div className={ 'tab ' + (this.activeEditor === editor ? 'active' : '') }
tabIndex="0"
ref={ editor.shortId + '-switch' }
onClick={ compose('showEditor', editor) }>
{ editor.label }
</div>
);
})
}
</div>
</div>
);
};
function unsupportedExportAs() {
return new Error('<exportAs> not supported for the current tab');
}
function unsupportedSave() {
return new Error('<save> not supported for current tab');
}
/**
* Returns a copy of the file with the given extension.
*
* @param {FileDescriptor} file
* @param {String} extension
*
* @return {FileDescriptor}
*/
function withExtension(file, extension) {
return {
name: replaceFileExt(file.name, extension),
path: !isUnsaved(file.path) ? replaceFileExt(file.path, extension) : file.path,
fileType: extension
};
}