227 lines
5.6 KiB
JavaScript
227 lines
5.6 KiB
JavaScript
define(['codemirror'], (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,
|
|
defaultCode = '',
|
|
}) {
|
|
window.devicePixelRatio = 1;
|
|
this.canvas = makeNode('canvas');
|
|
this.context = this.canvas.getContext('2d');
|
|
|
|
this.parser = parser;
|
|
this.generator = generator;
|
|
this.renderer = renderer;
|
|
this.defaultCode = defaultCode;
|
|
this.minScale = 1.5;
|
|
|
|
this.debounced = null;
|
|
this.latestSeq = null;
|
|
this.renderedSeq = null;
|
|
this.canvasDirty = true;
|
|
this.svgDirty = true;
|
|
this.latestPNG = null;
|
|
this.latestSVG = null;
|
|
this.updatingPNG = null;
|
|
|
|
this._downloadSVGClick = this._downloadSVGClick.bind(this);
|
|
this._downloadPNGClick = this._downloadPNGClick.bind(this);
|
|
this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
|
|
}
|
|
|
|
build(container) {
|
|
this.codePane = makeNode('div', {'class': 'pane-code'});
|
|
this.viewPane = makeNode('div', {'class': 'pane-view'});
|
|
this.viewPaneInner = makeNode('div', {'class': 'pane-view-inner'});
|
|
|
|
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);
|
|
|
|
this.options = makeNode('div', {'class': 'options'});
|
|
this.options.appendChild(this.downloadPNG);
|
|
this.options.appendChild(this.downloadSVG);
|
|
this.viewPane.appendChild(this.options);
|
|
this.viewPane.appendChild(this.viewPaneInner);
|
|
|
|
container.appendChild(this.codePane);
|
|
container.appendChild(this.viewPane);
|
|
|
|
this.code = new CodeMirror(this.codePane, {
|
|
value: this.defaultCode,
|
|
mode: '',
|
|
});
|
|
this.viewPaneInner.appendChild(this.renderer.svg());
|
|
|
|
this.code.on('change', () => this.update(false));
|
|
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.canvasDirty = true;
|
|
this.svgDirty = true;
|
|
this.renderedSeq = sequence;
|
|
this.renderer.render(sequence);
|
|
this.updateMinSize(this.renderer.width, this.renderer.height);
|
|
}
|
|
|
|
update(immediate = true) {
|
|
const src = this.code.getDoc().getValue();
|
|
let sequence = null;
|
|
try {
|
|
const parsed = this.parser.parse(src);
|
|
sequence = this.generator.generate(parsed);
|
|
} catch(e) {
|
|
// TODO
|
|
// console.log(e);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
getSVGData() {
|
|
this.forceRender();
|
|
if(!this.svgDirty) {
|
|
return this.latestSVG;
|
|
}
|
|
this.svgDirty = false;
|
|
const src = this.renderer.svg().outerHTML;
|
|
const blob = new Blob([src], {type: 'image/svg+xml'});
|
|
if(this.latestSVG) {
|
|
URL.revokeObjectURL(this.latestSVG);
|
|
}
|
|
this.latestSVG = URL.createObjectURL(blob);
|
|
return this.latestSVG;
|
|
}
|
|
|
|
getPNGData(callback) {
|
|
this.forceRender();
|
|
if(!this.canvasDirty) {
|
|
// TODO: this could cause issues if getPNGData is called
|
|
// while another update is ongoing. Needs a more robust fix
|
|
callback(this.latestPNG);
|
|
return;
|
|
}
|
|
this.canvasDirty = false;
|
|
const width = this.renderer.width * PNG_RESOLUTION;
|
|
const height = this.renderer.height * PNG_RESOLUTION;
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
const img = new Image(width, height);
|
|
img.addEventListener('load', () => {
|
|
this.context.drawImage(img, 0, 0);
|
|
this.canvas.toBlob((blob) => {
|
|
if(this.latestPNG) {
|
|
URL.revokeObjectURL(this.latestPNG);
|
|
}
|
|
this.latestPNG = URL.createObjectURL(blob);
|
|
callback(this.latestPNG);
|
|
}, 'image/png');
|
|
}, {once: true});
|
|
img.src = this.getSVGData();
|
|
}
|
|
|
|
updatePNGLink() {
|
|
const nonce = this.updatingPNG = {};
|
|
this.getPNGData((data) => {
|
|
if(this.updatingPNG === nonce) {
|
|
this.downloadPNG.setAttribute('href', data);
|
|
this.updatingPNG = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
_downloadPNGFocus() {
|
|
this.forceRender();
|
|
if(this.canvasDirty) {
|
|
this.updatePNGLink();
|
|
}
|
|
}
|
|
|
|
_downloadPNGClick(e) {
|
|
if(this.updatingPNG) {
|
|
e.preventDefault();
|
|
} else if(this.canvasDirty) {
|
|
e.preventDefault();
|
|
this.updatePNGLink();
|
|
}
|
|
}
|
|
|
|
_downloadSVGClick() {
|
|
this.downloadSVG.setAttribute('href', this.getSVGData());
|
|
}
|
|
};
|
|
});
|