SequenceDiagram/scripts/interface/Interface.js

354 lines
8.2 KiB
JavaScript

define([
'cm/lib/codemirror',
'cm/addon/hint/show-hint',
'cm/addon/edit/trailingspace',
'cm/addon/comment/comment',
], (CodeMirror) => {
'use strict';
const DELAY_AGENTCHANGE = 500;
const DELAY_STAGECHANGE = 250;
const PNG_RESOLUTION = 4;
function makeText(text = '') {
return document.createTextNode(text);
}
function makeNode(type, attrs = {}) {
const o = document.createElement(type);
for(let k in attrs) {
if(attrs.hasOwnProperty(k)) {
o.setAttribute(k, attrs[k]);
}
}
return o;
}
function on(element, events, fn) {
events.forEach((event) => element.addEventListener(event, fn));
}
return class Interface {
constructor({
parser,
generator,
renderer,
exporter,
defaultCode = '',
localStorage = '',
}) {
this.parser = parser;
this.generator = generator;
this.renderer = renderer;
this.exporter = exporter;
this.defaultCode = defaultCode;
this.localStorage = localStorage;
this.minScale = 1.5;
this.debounced = null;
this.latestSeq = null;
this.renderedSeq = null;
this.pngDirty = true;
this.updatingPNG = false;
this.marker = null;
this._downloadSVGClick = this._downloadSVGClick.bind(this);
this._downloadPNGClick = this._downloadPNGClick.bind(this);
this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
}
buildOptionsLinks() {
const githubLink = makeNode('a', {
'class': 'github',
'href': 'https://github.com/davidje13/SequenceDiagram',
'target': '_blank',
});
githubLink.appendChild(makeText('GitHub'));
const options = makeNode('div', {'class': 'options links'});
options.appendChild(githubLink);
return options;
}
buildOptionsDownloads() {
this.downloadPNG = makeNode('a', {
'href': '#',
'download': 'SequenceDiagram.png',
});
this.downloadPNG.appendChild(makeText('Download PNG'));
on(this.downloadPNG, [
'focus',
'mouseover',
'mousedown',
], this._downloadPNGFocus);
on(this.downloadPNG, ['click'], this._downloadPNGClick);
this.downloadSVG = makeNode('a', {
'href': '#',
'download': 'SequenceDiagram.svg',
});
this.downloadSVG.appendChild(makeText('SVG'));
on(this.downloadSVG, ['click'], this._downloadSVGClick);
const options = makeNode('div', {'class': 'options downloads'});
options.appendChild(this.downloadPNG);
options.appendChild(this.downloadSVG);
return options;
}
buildEditor(container) {
const value = this.loadCode() || this.defaultCode;
CodeMirror.defineMode(
'sequence',
() => this.parser.getCodeMirrorMode()
);
CodeMirror.registerHelper(
'hint',
'sequence',
this.parser.getCodeMirrorHints()
);
const code = new CodeMirror(container, {
value,
mode: 'sequence',
globals: {
themes: this.renderer.getThemeNames(),
},
lineNumbers: true,
showTrailingSpace: true,
extraKeys: {
'Tab': (cm) => cm.execCommand('indentMore'),
'Shift-Tab': (cm) => cm.execCommand('indentLess'),
'Cmd-/': (cm) => cm.toggleComment({padding: ''}),
'Ctrl-/': (cm) => cm.toggleComment({padding: ''}),
'Ctrl-Space': 'autocomplete',
'Ctrl-Enter': 'autocomplete',
'Cmd-Enter': 'autocomplete',
},
});
let lastKey = 0;
code.on('keydown', (cm, event) => {
lastKey = event.keyCode;
});
code.on('endCompletion', () => {
lastKey = 0;
});
code.on('change', (cm) => {
if(cm.state.completionActive) {
return;
}
if(lastKey === 13 || lastKey === 8) {
return;
}
lastKey = 0;
CodeMirror.commands.autocomplete(cm, null, {
completeSingle: false,
});
});
return code;
}
registerListeners() {
this.code.on('change', () => this.update(false));
this.renderer.addEventListener('mouseover', (element) => {
if(this.marker) {
this.marker.clear();
}
if(element.ln !== undefined) {
this.marker = this.code.markText(
{line: element.ln, ch: 0},
{line: element.ln + 1, ch: 0},
{
className: 'hover',
inclusiveLeft: false,
inclusiveRight: false,
clearOnEnter: true,
}
);
}
});
this.renderer.addEventListener('mouseout', () => {
if(this.marker) {
this.marker.clear();
this.marker = null;
}
});
this.renderer.addEventListener('click', (element) => {
if(this.marker) {
this.marker.clear();
this.marker = null;
}
if(element.ln !== undefined) {
this.code.setSelection(
{line: element.ln, ch: 0},
{line: element.ln + 1, ch: 0},
{origin: '+focus', bias: -1}
);
this.code.focus();
}
});
}
build(container) {
const codePane = makeNode('div', {'class': 'pane-code'});
const viewPane = makeNode('div', {'class': 'pane-view'});
this.errorPane = makeNode('div', {'class': 'pane-error'});
this.errorText = makeText();
this.errorPane.appendChild(this.errorText);
const viewPaneScroller = makeNode('div', {
'class': 'pane-view-scroller',
});
this.viewPaneInner = makeNode('div', {
'class': 'pane-view-inner',
});
viewPane.appendChild(viewPaneScroller);
viewPaneScroller.appendChild(this.viewPaneInner);
viewPane.appendChild(this.buildOptionsLinks());
viewPane.appendChild(this.buildOptionsDownloads());
container.appendChild(codePane);
container.appendChild(this.errorPane);
container.appendChild(viewPane);
this.code = this.buildEditor(codePane);
this.viewPaneInner.appendChild(this.renderer.svg());
this.registerListeners();
this.update();
}
updateMinSize(width, height) {
const style = this.viewPaneInner.style;
style.minWidth = Math.ceil(width * this.minScale) + 'px';
style.minHeight = Math.ceil(height * this.minScale) + 'px';
}
redraw(sequence) {
clearTimeout(this.debounced);
this.debounced = null;
this.pngDirty = true;
this.renderedSeq = sequence;
this.renderer.render(sequence);
this.updateMinSize(this.renderer.width, this.renderer.height);
}
saveCode(src) {
if(!this.localStorage) {
return;
}
try {
window.localStorage.setItem(this.localStorage, src);
} catch(e) {
// ignore
}
}
loadCode() {
if(!this.localStorage) {
return '';
}
try {
return window.localStorage.getItem(this.localStorage) || '';
} catch(e) {
return '';
}
}
markError(error) {
if(typeof error === 'object' && error.message) {
this.errorText.nodeValue = error.message;
} else {
this.errorText.nodeValue = error;
}
this.errorPane.setAttribute('class', 'pane-error error');
}
markOK() {
this.errorText.nodeValue = 'All OK';
this.errorPane.setAttribute('class', 'pane-error ok');
}
update(immediate = true) {
const src = this.code.getDoc().getValue();
this.saveCode(src);
let sequence = null;
try {
const parsed = this.parser.parse(src);
sequence = this.generator.generate(parsed);
} catch(e) {
this.markError(e);
return;
}
this.markOK();
let delay = 0;
if(!immediate && this.renderedSeq) {
const old = this.renderedSeq;
if(sequence.agents.length !== old.agents.length) {
delay = DELAY_AGENTCHANGE;
} else if(sequence.stages.length !== old.stages.length) {
delay = DELAY_STAGECHANGE;
}
}
if(delay <= 0) {
this.redraw(sequence);
} else {
clearTimeout(this.debounced);
this.latestSeq = sequence;
this.debounced = setTimeout(() => this.redraw(sequence), delay);
}
}
forceRender() {
if(this.debounced) {
clearTimeout(this.debounced);
this.redraw(this.latestSeq);
}
}
updatePNGLink() {
this.forceRender();
if(this.updatingPNG || !this.pngDirty) {
return false;
}
this.pngDirty = false;
this.updatingPNG = true;
this.exporter.getPNGURL(
this.renderer,
PNG_RESOLUTION,
(url, latest) => {
if(latest) {
this.downloadPNG.setAttribute('href', url);
this.updatingPNG = false;
}
}
);
return true;
}
_downloadPNGFocus() {
this.updatePNGLink();
}
_downloadPNGClick(e) {
if(this.updatingPNG) {
e.preventDefault();
} else if(this.updatePNGLink()) {
e.preventDefault();
}
}
_downloadSVGClick() {
this.forceRender();
const url = this.exporter.getSVGURL(this.renderer);
this.downloadSVG.setAttribute('href', url);
}
};
});