465 lines
9.6 KiB
JavaScript
465 lines
9.6 KiB
JavaScript
import {Factory as BasicThemeFactory} from './themes/Basic.mjs';
|
|
import {Factory as ChunkyThemeFactory} from './themes/Chunky.mjs';
|
|
import EventObject from '../core/EventObject.mjs';
|
|
import Exporter from './Exporter.mjs';
|
|
import Generator from './Generator.mjs';
|
|
import {Factory as MonospaceThemeFactory} from './themes/Monospace.mjs';
|
|
import Parser from './Parser.mjs';
|
|
import Renderer from './Renderer.mjs';
|
|
import {Factory as SketchThemeFactory} from './themes/Sketch.mjs';
|
|
import {getHints} from './CodeMirrorHints.mjs';
|
|
|
|
const themes = [
|
|
new BasicThemeFactory(),
|
|
new MonospaceThemeFactory(),
|
|
new ChunkyThemeFactory(),
|
|
new SketchThemeFactory(SketchThemeFactory.RIGHT),
|
|
new SketchThemeFactory(SketchThemeFactory.LEFT),
|
|
];
|
|
|
|
const SharedParser = new Parser();
|
|
const SharedGenerator = new Generator();
|
|
const CMMode = SharedParser.getCodeMirrorMode();
|
|
|
|
function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') {
|
|
const cm = CodeMirror || window.CodeMirror;
|
|
cm.defineMode(modeName, () => CMMode);
|
|
cm.registerHelper('hint', modeName, getHints);
|
|
}
|
|
|
|
function addTheme(theme) {
|
|
themes.push(theme);
|
|
}
|
|
|
|
function extractCodeFromSVG(svg) {
|
|
const dom = new DOMParser().parseFromString(svg, 'image/svg+xml');
|
|
const meta = dom.querySelector('metadata');
|
|
if(!meta) {
|
|
return '';
|
|
}
|
|
return meta.textContent;
|
|
}
|
|
|
|
function renderAll(diagrams) {
|
|
const errors = [];
|
|
function storeError(sd, e) {
|
|
errors.push(e);
|
|
}
|
|
|
|
diagrams.forEach((diagram) => {
|
|
diagram.addEventListener('error', storeError);
|
|
diagram.optimisedRenderPreReflow();
|
|
});
|
|
diagrams.forEach((diagram) => {
|
|
diagram.optimisedRenderReflow();
|
|
});
|
|
diagrams.forEach((diagram) => {
|
|
diagram.optimisedRenderPostReflow();
|
|
diagram.removeEventListener('error', storeError);
|
|
});
|
|
|
|
if(errors.length > 0) {
|
|
throw errors;
|
|
}
|
|
}
|
|
|
|
function pickDocument(container) {
|
|
if(container) {
|
|
return container.ownerDocument || null;
|
|
} else if(typeof window === 'undefined') {
|
|
return null;
|
|
} else {
|
|
return window.document;
|
|
}
|
|
}
|
|
|
|
export default class SequenceDiagram extends EventObject {
|
|
/* eslint-disable complexity */ // Just some defaults
|
|
constructor(code = null, options = {}) {
|
|
/* eslint-enable complexity */
|
|
super();
|
|
|
|
let opts = null;
|
|
if(code && typeof code === 'object') {
|
|
opts = code;
|
|
this.code = opts.code;
|
|
} else {
|
|
opts = options;
|
|
this.code = code;
|
|
}
|
|
|
|
Object.assign(this, {
|
|
exporter: new Exporter(),
|
|
generator: SharedGenerator,
|
|
isInteractive: false,
|
|
latestProcessed: null,
|
|
parser: SharedParser,
|
|
registerCodeMirrorMode,
|
|
renderer: new Renderer(Object.assign({
|
|
document: pickDocument(opts.container),
|
|
themes,
|
|
}, opts)),
|
|
textSizerFactory: opts.textSizerFactory || null,
|
|
});
|
|
|
|
this.renderer.addEventForwarding(this);
|
|
|
|
if(opts.container) {
|
|
opts.container.appendChild(this.dom());
|
|
}
|
|
if(opts.interactive) {
|
|
this.addInteractivity();
|
|
}
|
|
if(typeof this.code === 'string' && opts.render !== false) {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
clone(options = {}) {
|
|
const reference = (options.container || this.renderer.dom());
|
|
|
|
return new SequenceDiagram(Object.assign({
|
|
code: this.code,
|
|
components: this.renderer.components,
|
|
container: null,
|
|
document: reference.ownerDocument,
|
|
interactive: this.isInteractive,
|
|
namespace: null,
|
|
textSizerFactory: this.textSizerFactory,
|
|
themes: this.renderer.getThemes(),
|
|
}, options));
|
|
}
|
|
|
|
set(code = '', {render = true} = {}) {
|
|
if(this.code === code) {
|
|
return;
|
|
}
|
|
|
|
this.code = code;
|
|
if(render) {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
process(code) {
|
|
const parsed = this.parser.parse(code);
|
|
return this.generator.generate(parsed);
|
|
}
|
|
|
|
addTheme(theme) {
|
|
this.renderer.addTheme(theme);
|
|
}
|
|
|
|
setHighlight(line) {
|
|
this.renderer.setHighlight(line);
|
|
}
|
|
|
|
isCollapsed(line) {
|
|
return this.renderer.isCollapsed(line);
|
|
}
|
|
|
|
setCollapsed(line, collapsed = true, {render = true} = {}) {
|
|
if(!this.renderer.setCollapsed(line, collapsed)) {
|
|
return false;
|
|
}
|
|
if(render && this.latestProcessed) {
|
|
this.render(this.latestProcessed);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
collapse(line, options) {
|
|
return this.setCollapsed(line, true, options);
|
|
}
|
|
|
|
expand(line, options) {
|
|
return this.setCollapsed(line, false, options);
|
|
}
|
|
|
|
toggleCollapsed(line, options) {
|
|
return this.setCollapsed(line, !this.isCollapsed(line), options);
|
|
}
|
|
|
|
expandAll(options) {
|
|
return this.setCollapsed(null, false, options);
|
|
}
|
|
|
|
getThemeNames() {
|
|
return this.renderer.getThemeNames();
|
|
}
|
|
|
|
getThemes() {
|
|
return this.renderer.getThemes();
|
|
}
|
|
|
|
getSVGSynchronous() {
|
|
return this.exporter.getSVGURL(this.renderer);
|
|
}
|
|
|
|
getSVG() {
|
|
return Promise.resolve({
|
|
latest: true,
|
|
url: this.getSVGSynchronous(),
|
|
});
|
|
}
|
|
|
|
getCanvas({resolution = 1, size = null} = {}) {
|
|
if(size) {
|
|
this.renderer.width = size.width;
|
|
this.renderer.height = size.height;
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.exporter.getCanvas(this.renderer, resolution, resolve);
|
|
});
|
|
}
|
|
|
|
getPNG({resolution = 1, size = null} = {}) {
|
|
if(size) {
|
|
this.renderer.width = size.width;
|
|
this.renderer.height = size.height;
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.exporter.getPNGURL(
|
|
this.renderer,
|
|
resolution,
|
|
(url, latest) => {
|
|
resolve({latest, url});
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
getSize() {
|
|
return {
|
|
height: this.renderer.height,
|
|
width: this.renderer.width,
|
|
};
|
|
}
|
|
|
|
_revertParent(state) {
|
|
const dom = this.renderer.dom();
|
|
if(dom.parentNode !== state.originalParent) {
|
|
dom.parentNode.removeChild(dom);
|
|
if(state.originalParent) {
|
|
state.originalParent.appendChild(dom);
|
|
}
|
|
}
|
|
}
|
|
|
|
_sendRenderError(e) {
|
|
this._revertParent(this.renderState);
|
|
this.renderState.error = true;
|
|
this.trigger('error', [this, e]);
|
|
}
|
|
|
|
optimisedRenderPreReflow(processed = null) {
|
|
const dom = this.renderer.dom();
|
|
this.renderState = {
|
|
error: false,
|
|
originalParent: dom.parentNode,
|
|
processed,
|
|
};
|
|
const state = this.renderState;
|
|
|
|
if(!dom.isConnected) {
|
|
if(state.originalParent) {
|
|
state.originalParent.removeChild(dom);
|
|
}
|
|
dom.ownerDocument.body.appendChild(dom);
|
|
}
|
|
|
|
try {
|
|
if(!state.processed) {
|
|
state.processed = this.process(this.code);
|
|
}
|
|
this.renderer.optimisedRenderPreReflow(state.processed);
|
|
} catch(e) {
|
|
this._sendRenderError(e);
|
|
}
|
|
}
|
|
|
|
optimisedRenderReflow() {
|
|
try {
|
|
if(!this.renderState.error) {
|
|
this.renderer.optimisedRenderReflow();
|
|
}
|
|
} catch(e) {
|
|
this._sendRenderError(e);
|
|
}
|
|
}
|
|
|
|
optimisedRenderPostReflow() {
|
|
const state = this.renderState;
|
|
|
|
try {
|
|
if(!state.error) {
|
|
this.renderer.optimisedRenderPostReflow(state.processed);
|
|
}
|
|
} catch(e) {
|
|
this._sendRenderError(e);
|
|
}
|
|
|
|
this.renderState = null;
|
|
|
|
if(!state.error) {
|
|
this._revertParent(state);
|
|
this.latestProcessed = state.processed;
|
|
this.trigger('render', [this]);
|
|
}
|
|
}
|
|
|
|
render(processed = null) {
|
|
let latestError = null;
|
|
function storeError(sd, e) {
|
|
latestError = e;
|
|
}
|
|
this.addEventListener('error', storeError);
|
|
|
|
this.optimisedRenderPreReflow(processed);
|
|
this.optimisedRenderReflow();
|
|
this.optimisedRenderPostReflow();
|
|
|
|
this.removeEventListener('error', storeError);
|
|
if(latestError) {
|
|
throw latestError;
|
|
}
|
|
}
|
|
|
|
setContainer(node = null) {
|
|
const dom = this.dom();
|
|
if(dom.parentNode) {
|
|
dom.parentNode.removeChild(dom);
|
|
}
|
|
if(node) {
|
|
node.appendChild(dom);
|
|
}
|
|
}
|
|
|
|
addInteractivity() {
|
|
if(this.isInteractive) {
|
|
return;
|
|
}
|
|
this.isInteractive = true;
|
|
|
|
this.addEventListener('click', (element) => {
|
|
this.toggleCollapsed(element.ln);
|
|
});
|
|
}
|
|
|
|
extractCodeFromSVG(svg) {
|
|
return extractCodeFromSVG(svg);
|
|
}
|
|
|
|
renderAll(diagrams) {
|
|
return renderAll(diagrams);
|
|
}
|
|
|
|
dom() {
|
|
return this.renderer.dom();
|
|
}
|
|
}
|
|
|
|
function datasetBoolean(value) {
|
|
return typeof value !== 'undefined' && value !== 'false';
|
|
}
|
|
|
|
function parseTagOptions(element) {
|
|
return {
|
|
interactive: datasetBoolean(element.dataset.sdInteractive),
|
|
namespace: element.dataset.sdNamespace || null,
|
|
};
|
|
}
|
|
|
|
function convertOne(element, code = null, options = {}) {
|
|
if(element.tagName === 'svg') {
|
|
return null;
|
|
}
|
|
|
|
const tagOptions = parseTagOptions(element);
|
|
|
|
const diagram = new SequenceDiagram(
|
|
(code === null) ? element.textContent : code,
|
|
Object.assign(tagOptions, options)
|
|
);
|
|
const newElement = diagram.dom();
|
|
const attrs = element.attributes;
|
|
for(let i = 0; i < attrs.length; ++ i) {
|
|
newElement.setAttribute(
|
|
attrs[i].nodeName,
|
|
attrs[i].nodeValue
|
|
);
|
|
}
|
|
element.parentNode.replaceChild(newElement, element);
|
|
return diagram;
|
|
}
|
|
|
|
function convert(elements, code = null, options = {}) {
|
|
let c = null;
|
|
let opts = null;
|
|
if(code && typeof code === 'object') {
|
|
opts = code;
|
|
c = opts.code;
|
|
} else {
|
|
opts = options;
|
|
c = code;
|
|
}
|
|
|
|
if(Array.isArray(elements)) {
|
|
const nodrawOpts = Object.assign({}, opts, {render: false});
|
|
const diagrams = elements.map((el) => convertOne(el, c, nodrawOpts));
|
|
if(opts.render !== false) {
|
|
renderAll(diagrams);
|
|
}
|
|
return diagrams;
|
|
} else {
|
|
return convertOne(elements, c, opts);
|
|
}
|
|
}
|
|
|
|
function convertAll(root = null, className = 'sequence-diagram') {
|
|
let r = null;
|
|
let cls = null;
|
|
if(typeof root === 'string') {
|
|
r = null;
|
|
cls = root;
|
|
} else {
|
|
r = root;
|
|
cls = className;
|
|
}
|
|
|
|
let elements = null;
|
|
if(r && typeof r.length !== 'undefined') {
|
|
elements = r;
|
|
} else {
|
|
elements = (r || window.document).getElementsByClassName(cls);
|
|
}
|
|
|
|
// Convert from "live" collection to static to avoid infinite loops:
|
|
const els = [];
|
|
for(let i = 0; i < elements.length; ++ i) {
|
|
els.push(elements[i]);
|
|
}
|
|
|
|
// Convert elements
|
|
convert(els);
|
|
}
|
|
|
|
function getDefaultThemeNames() {
|
|
return themes.map((theme) => theme.name);
|
|
}
|
|
|
|
Object.assign(SequenceDiagram, {
|
|
Exporter,
|
|
Generator,
|
|
Parser,
|
|
Renderer,
|
|
addTheme,
|
|
convert,
|
|
convertAll,
|
|
extractCodeFromSVG,
|
|
getDefaultThemeNames,
|
|
registerCodeMirrorMode,
|
|
renderAll,
|
|
themes,
|
|
});
|