Significantly reduce reflows for faster page loads
This commit is contained in:
parent
a0de2914a3
commit
3d89dc3548
|
@ -3539,27 +3539,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
});
|
||||
}
|
||||
|
||||
function measureLine(tester, line) {
|
||||
if(!line.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labelKey = JSON.stringify(line);
|
||||
const knownWidth = tester.widths.get(labelKey);
|
||||
if(knownWidth !== undefined) {
|
||||
return knownWidth;
|
||||
}
|
||||
|
||||
// getComputedTextLength forces a reflow, so only call it if nothing
|
||||
// else can tell us the length
|
||||
|
||||
svg.empty(tester.node);
|
||||
populateSvgTextLine(tester.node, line);
|
||||
const width = tester.node.getComputedTextLength();
|
||||
tester.widths.set(labelKey, width);
|
||||
return width;
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
|
@ -3676,35 +3655,68 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
});
|
||||
this.container = container;
|
||||
this.cache = new Map();
|
||||
this.nodes = null;
|
||||
}
|
||||
|
||||
_expectMeasure({attrs, formatted}) {
|
||||
if(!formatted.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let attrCache = this.cache.get(attrKey);
|
||||
if(!attrCache) {
|
||||
attrCache = {
|
||||
attrs,
|
||||
lines: new Map(),
|
||||
};
|
||||
this.cache.set(attrKey, attrCache);
|
||||
}
|
||||
|
||||
formatted.forEach((line) => {
|
||||
if(!line.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelKey = JSON.stringify(line);
|
||||
if(!attrCache.lines.has(labelKey)) {
|
||||
attrCache.lines.set(labelKey, {
|
||||
formatted: line,
|
||||
width: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return attrCache;
|
||||
}
|
||||
|
||||
_measureHeight({attrs, formatted}) {
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
}
|
||||
|
||||
_measureWidth({attrs, formatted}) {
|
||||
if(!formatted.length) {
|
||||
_measureLine(attrCache, line) {
|
||||
if(!line.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let tester = this.cache.get(attrKey);
|
||||
if(!tester) {
|
||||
const node = svg.make('text', attrs);
|
||||
this.testers.appendChild(node);
|
||||
tester = {
|
||||
node,
|
||||
widths: new Map(),
|
||||
};
|
||||
this.cache.set(attrKey, tester);
|
||||
const labelKey = JSON.stringify(line);
|
||||
const cache = attrCache.lines.get(labelKey);
|
||||
if(cache.width === null) {
|
||||
window.console.warn('Performing unexpected measurement', line);
|
||||
this.performMeasurements();
|
||||
}
|
||||
return cache.width;
|
||||
}
|
||||
|
||||
if(!this.testers.parentNode) {
|
||||
this.container.appendChild(this.testers);
|
||||
_measureWidth(opts) {
|
||||
if(!opts.formatted.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (formatted
|
||||
.map((line) => measureLine(tester, line))
|
||||
const attrCache = this._expectMeasure(opts);
|
||||
|
||||
return (opts.formatted
|
||||
.map((line) => this._measureLine(attrCache, line))
|
||||
.reduce((a, b) => Math.max(a, b), 0)
|
||||
);
|
||||
}
|
||||
|
@ -3723,6 +3735,52 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
return {attrs, formatted};
|
||||
}
|
||||
|
||||
expectMeasure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
this._expectMeasure(opts);
|
||||
}
|
||||
|
||||
performMeasurementsPre() {
|
||||
this.nodes = [];
|
||||
this.cache.forEach(({attrs, lines}) => {
|
||||
lines.forEach((cacheLine) => {
|
||||
if(cacheLine.width === null) {
|
||||
const node = svg.make('text', attrs);
|
||||
populateSvgTextLine(node, cacheLine.formatted);
|
||||
this.testers.appendChild(node);
|
||||
this.nodes.push({node, cacheLine});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if(this.nodes.length) {
|
||||
this.container.appendChild(this.testers);
|
||||
}
|
||||
}
|
||||
|
||||
performMeasurementsAct() {
|
||||
this.nodes.forEach(({node, cacheLine}) => {
|
||||
cacheLine.width = node.getComputedTextLength();
|
||||
});
|
||||
}
|
||||
|
||||
performMeasurementsPost() {
|
||||
if(this.nodes.length) {
|
||||
this.container.removeChild(this.testers);
|
||||
svg.empty(this.testers);
|
||||
}
|
||||
this.nodes = null;
|
||||
}
|
||||
|
||||
performMeasurements() {
|
||||
// getComputedTextLength forces a reflow, so we try to batch as
|
||||
// many measurements as possible into a single DOM change
|
||||
|
||||
this.performMeasurementsPre();
|
||||
this.performMeasurementsAct();
|
||||
this.performMeasurementsPost();
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
return {
|
||||
|
@ -3737,15 +3795,8 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
}
|
||||
|
||||
resetCache() {
|
||||
svg.empty(this.testers);
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
detach() {
|
||||
if(this.testers.parentNode) {
|
||||
this.container.removeChild(this.testers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SVGTextBlock.SizeTester = SizeTester;
|
||||
|
@ -4010,6 +4061,16 @@ define('sequence/components/BaseComponent',[],() => {
|
|||
this.makeState(state);
|
||||
}
|
||||
|
||||
prepareMeasurements(/*stage, {
|
||||
renderer,
|
||||
theme,
|
||||
agentInfos,
|
||||
textSizer,
|
||||
state,
|
||||
components,
|
||||
}*/) {
|
||||
}
|
||||
|
||||
separationPre(/*stage, {
|
||||
renderer,
|
||||
theme,
|
||||
|
@ -4128,6 +4189,13 @@ define('sequence/components/Block',[
|
|||
'use strict';
|
||||
|
||||
class BlockSplit extends BaseComponent {
|
||||
prepareMeasurements({left, tag, label}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.type).section;
|
||||
env.textSizer.expectMeasure(config.tag.labelAttrs, tag);
|
||||
env.textSizer.expectMeasure(config.label.labelAttrs, label);
|
||||
}
|
||||
|
||||
separation({left, right, tag, label}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.type).section;
|
||||
|
@ -4242,8 +4310,14 @@ define('sequence/components/Block',[
|
|||
return blockInfo;
|
||||
}
|
||||
|
||||
prepareMeasurements(stage, env) {
|
||||
this.storeBlockInfo(stage, env);
|
||||
super.prepareMeasurements(stage, env);
|
||||
}
|
||||
|
||||
separationPre(stage, env) {
|
||||
this.storeBlockInfo(stage, env);
|
||||
super.separationPre(stage, env);
|
||||
}
|
||||
|
||||
separation(stage, env) {
|
||||
|
@ -4379,16 +4453,23 @@ define('sequence/components/Parallel',[
|
|||
}
|
||||
|
||||
class Parallel extends BaseComponent {
|
||||
separationPre(stage, env) {
|
||||
stage.stages.forEach((subStage) => {
|
||||
env.components.get(subStage.type).separationPre(subStage, env);
|
||||
invokeChildren(stage, env, methodName) {
|
||||
return stage.stages.map((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
return component[methodName](subStage, env);
|
||||
});
|
||||
}
|
||||
|
||||
prepareMeasurements(stage, env) {
|
||||
this.invokeChildren(stage, env, 'prepareMeasurements');
|
||||
}
|
||||
|
||||
separationPre(stage, env) {
|
||||
this.invokeChildren(stage, env, 'separationPre');
|
||||
}
|
||||
|
||||
separation(stage, env) {
|
||||
stage.stages.forEach((subStage) => {
|
||||
env.components.get(subStage.type).separation(subStage, env);
|
||||
});
|
||||
this.invokeChildren(stage, env, 'separation');
|
||||
}
|
||||
|
||||
renderPre(stage, env) {
|
||||
|
@ -4398,11 +4479,9 @@ define('sequence/components/Parallel',[
|
|||
asynchronousY: null,
|
||||
};
|
||||
|
||||
return stage.stages.map((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
const subResult = component.renderPre(subStage, env);
|
||||
return BaseComponent.cleanRenderPreResult(subResult);
|
||||
}).reduce(mergeResults, baseResults);
|
||||
return this.invokeChildren(stage, env, 'renderPre')
|
||||
.map((r) => BaseComponent.cleanRenderPreResult(r))
|
||||
.reduce(mergeResults, baseResults);
|
||||
}
|
||||
|
||||
render(stage, env) {
|
||||
|
@ -4424,24 +4503,19 @@ define('sequence/components/Parallel',[
|
|||
}
|
||||
|
||||
renderHidden(stage, env) {
|
||||
stage.stages.forEach((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
component.renderHidden(subStage, env);
|
||||
});
|
||||
this.invokeChildren(stage, env, 'renderHidden');
|
||||
}
|
||||
|
||||
shouldHide(stage, env) {
|
||||
const result = {
|
||||
const baseResults = {
|
||||
self: false,
|
||||
nest: 0,
|
||||
};
|
||||
stage.stages.forEach((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
const hide = component.shouldHide(subStage, env) || {};
|
||||
result.self = (result.self || Boolean(hide.self));
|
||||
result.nest += (hide.nest || 0);
|
||||
});
|
||||
return result;
|
||||
return this.invokeChildren(stage, env, 'shouldHide')
|
||||
.reduce((result, {self = false, nest = 0} = {}) => ({
|
||||
self: result.self || Boolean(self),
|
||||
nest: result.nest + nest,
|
||||
}), baseResults);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4514,6 +4588,11 @@ define('sequence/components/AgentCap',[
|
|||
return config || env.theme.agentCap.box;
|
||||
}
|
||||
|
||||
prepareMeasurements({formattedLabel, options}, env) {
|
||||
const config = this.getConfig(options, env);
|
||||
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
|
||||
}
|
||||
|
||||
separation({formattedLabel, options}, env) {
|
||||
const config = this.getConfig(options, env);
|
||||
const width = (
|
||||
|
@ -4572,6 +4651,9 @@ define('sequence/components/AgentCap',[
|
|||
}
|
||||
|
||||
class CapCross {
|
||||
prepareMeasurements() {
|
||||
}
|
||||
|
||||
separation(agentInfo, env) {
|
||||
const config = env.theme.agentCap.cross;
|
||||
return {
|
||||
|
@ -4616,6 +4698,11 @@ define('sequence/components/AgentCap',[
|
|||
}
|
||||
|
||||
class CapBar {
|
||||
prepareMeasurements({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
|
||||
}
|
||||
|
||||
separation({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const width = (
|
||||
|
@ -4672,6 +4759,9 @@ define('sequence/components/AgentCap',[
|
|||
}
|
||||
|
||||
class CapFade {
|
||||
prepareMeasurements() {
|
||||
}
|
||||
|
||||
separation({currentRad}) {
|
||||
return {
|
||||
left: currentRad,
|
||||
|
@ -4733,6 +4823,9 @@ define('sequence/components/AgentCap',[
|
|||
}
|
||||
|
||||
class CapNone {
|
||||
prepareMeasurements() {
|
||||
}
|
||||
|
||||
separation({currentRad}) {
|
||||
return {
|
||||
left: currentRad,
|
||||
|
@ -4781,6 +4874,14 @@ define('sequence/components/AgentCap',[
|
|||
this.begin = begin;
|
||||
}
|
||||
|
||||
prepareMeasurements({mode, agentIDs}, env) {
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
const cap = AGENT_CAPS[mode];
|
||||
cap.prepareMeasurements(agentInfo, env, this.begin);
|
||||
});
|
||||
}
|
||||
|
||||
separationPre({mode, agentIDs}, env) {
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
|
@ -5011,6 +5112,15 @@ define('sequence/components/Connect',[
|
|||
];
|
||||
|
||||
class Connect extends BaseComponent {
|
||||
prepareMeasurements({agentIDs, label}, env) {
|
||||
const config = env.theme.connect;
|
||||
const loopback = (agentIDs[0] === agentIDs[1]);
|
||||
const labelAttrs = (loopback ?
|
||||
config.label.loopbackAttrs : config.label.attrs);
|
||||
|
||||
env.textSizer.expectMeasure(labelAttrs, label);
|
||||
}
|
||||
|
||||
separationPre({agentIDs}, env) {
|
||||
const r = env.theme.connect.source.radius;
|
||||
agentIDs.forEach((id) => {
|
||||
|
@ -5029,15 +5139,17 @@ define('sequence/components/Connect',[
|
|||
const lArrow = ARROWHEADS[options.left];
|
||||
const rArrow = ARROWHEADS[options.right];
|
||||
|
||||
let labelWidth = (
|
||||
env.textSizer.measure(config.label.attrs, label).width
|
||||
);
|
||||
const loopback = (agentIDs[0] === agentIDs[1]);
|
||||
const labelAttrs = (loopback ?
|
||||
config.label.loopbackAttrs : config.label.attrs);
|
||||
|
||||
let labelWidth = env.textSizer.measure(labelAttrs, label).width;
|
||||
if(labelWidth > 0) {
|
||||
labelWidth += config.label.padding * 2;
|
||||
}
|
||||
|
||||
const info1 = env.agentInfos.get(agentIDs[0]);
|
||||
if(agentIDs[0] === agentIDs[1]) {
|
||||
if(loopback) {
|
||||
env.addSpacing(agentIDs[0], {
|
||||
left: 0,
|
||||
right: (
|
||||
|
@ -5391,6 +5503,8 @@ define('sequence/components/Connect',[
|
|||
}
|
||||
|
||||
class ConnectDelayEnd extends Connect {
|
||||
prepareMeasurements() {}
|
||||
|
||||
separationPre() {}
|
||||
|
||||
separation() {}
|
||||
|
@ -5453,6 +5567,11 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base
|
|||
}
|
||||
|
||||
class NoteComponent extends BaseComponent {
|
||||
prepareMeasurements({mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
env.textSizer.expectMeasure(config.labelAttrs, label);
|
||||
}
|
||||
|
||||
renderPre({agentIDs}) {
|
||||
return {agentIDs};
|
||||
}
|
||||
|
@ -5713,6 +5832,11 @@ define('sequence/components/Divider',[
|
|||
'use strict';
|
||||
|
||||
class Divider extends BaseComponent {
|
||||
prepareMeasurements({mode, formattedLabel}, env) {
|
||||
const config = env.theme.getDivider(mode);
|
||||
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
|
||||
}
|
||||
|
||||
separation({mode, formattedLabel}, env) {
|
||||
const config = env.theme.getDivider(mode);
|
||||
|
||||
|
@ -5905,6 +6029,8 @@ define('sequence/Renderer',[
|
|||
|
||||
_bindMethods() {
|
||||
this.separationStage = this.separationStage.bind(this);
|
||||
this.prepareMeasurementsStage =
|
||||
this.prepareMeasurementsStage.bind(this);
|
||||
this.renderStage = this.renderStage.bind(this);
|
||||
this.addThemeDef = this.addThemeDef.bind(this);
|
||||
this.addDef = this.addDef.bind(this);
|
||||
|
@ -6100,6 +6226,24 @@ define('sequence/Renderer',[
|
|||
});
|
||||
}
|
||||
|
||||
prepareMeasurementsStage(stage) {
|
||||
const env = {
|
||||
renderer: this,
|
||||
theme: this.theme,
|
||||
agentInfos: this.agentInfos,
|
||||
textSizer: this.sizer,
|
||||
state: this.state,
|
||||
components: this.components,
|
||||
};
|
||||
|
||||
const component = this.components.get(stage.type);
|
||||
if(!component) {
|
||||
throw new Error('Unknown component: ' + stage.type);
|
||||
}
|
||||
|
||||
component.prepareMeasurements(stage, env);
|
||||
}
|
||||
|
||||
checkAgentRange(agentIDs, topY = 0) {
|
||||
if(agentIDs.length === 0) {
|
||||
return topY;
|
||||
|
@ -6273,7 +6417,7 @@ define('sequence/Renderer',[
|
|||
});
|
||||
}
|
||||
|
||||
buildAgentInfos(agents, stages) {
|
||||
buildAgentInfos(agents) {
|
||||
this.agentInfos = new Map();
|
||||
agents.forEach((agent, index) => {
|
||||
this.agentInfos.set(agent.id, {
|
||||
|
@ -6293,11 +6437,6 @@ define('sequence/Renderer',[
|
|||
separations: new Map(),
|
||||
});
|
||||
});
|
||||
|
||||
this.visibleAgentIDs = ['[', ']'];
|
||||
stages.forEach(this.separationStage);
|
||||
|
||||
this.positionAgents();
|
||||
}
|
||||
|
||||
updateBounds(stagesHeight) {
|
||||
|
@ -6429,16 +6568,18 @@ define('sequence/Renderer',[
|
|||
return this._setCollapsed(line, collapsed);
|
||||
}
|
||||
|
||||
render(sequence) {
|
||||
const prevHighlight = this.currentHighlight;
|
||||
_switchTheme(name) {
|
||||
const oldTheme = this.theme;
|
||||
this.theme = this.getThemeNamed(name);
|
||||
this.theme.reset();
|
||||
|
||||
this.theme = this.getThemeNamed(sequence.meta.theme);
|
||||
return (this.theme !== oldTheme);
|
||||
}
|
||||
|
||||
const themeChanged = (this.theme !== oldTheme);
|
||||
optimisedRenderPreReflow(sequence) {
|
||||
const themeChanged = this._switchTheme(sequence.meta.theme);
|
||||
this._reset(themeChanged);
|
||||
|
||||
this.theme.reset();
|
||||
this.metaCode.nodeValue = sequence.meta.code;
|
||||
this.theme.addDefs(this.addThemeDef);
|
||||
|
||||
|
@ -6446,21 +6587,47 @@ define('sequence/Renderer',[
|
|||
attrs: this.theme.titleAttrs,
|
||||
formatted: sequence.meta.title,
|
||||
});
|
||||
this.sizer.expectMeasure(this.title);
|
||||
|
||||
this.minX = 0;
|
||||
this.maxX = 0;
|
||||
this.buildAgentInfos(sequence.agents, sequence.stages);
|
||||
|
||||
this.buildAgentInfos(sequence.agents);
|
||||
|
||||
sequence.stages.forEach(this.prepareMeasurementsStage);
|
||||
this._resetState();
|
||||
this.sizer.performMeasurementsPre();
|
||||
}
|
||||
|
||||
optimisedRenderReflow() {
|
||||
this.sizer.performMeasurementsAct();
|
||||
}
|
||||
|
||||
optimisedRenderPostReflow(sequence) {
|
||||
this.visibleAgentIDs = ['[', ']'];
|
||||
sequence.stages.forEach(this.separationStage);
|
||||
this._resetState();
|
||||
|
||||
this.positionAgents();
|
||||
|
||||
sequence.stages.forEach(this.renderStage);
|
||||
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
||||
|
||||
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
||||
this.updateBounds(stagesHeight);
|
||||
|
||||
this.sizer.resetCache();
|
||||
this.sizer.detach();
|
||||
const prevHighlight = this.currentHighlight;
|
||||
this.currentHighlight = -1;
|
||||
this.setHighlight(prevHighlight);
|
||||
|
||||
this.sizer.performMeasurementsPost();
|
||||
this.sizer.resetCache();
|
||||
}
|
||||
|
||||
render(sequence) {
|
||||
this.optimisedRenderPreReflow(sequence);
|
||||
this.optimisedRenderReflow();
|
||||
this.optimisedRenderPostReflow(sequence);
|
||||
}
|
||||
|
||||
getThemeNames() {
|
||||
|
@ -9434,8 +9601,8 @@ define('sequence/themes/Sketch',[
|
|||
// but this fails when exporting as SVG / PNG (svg tags must
|
||||
// have no external dependencies).
|
||||
// const url = 'https://fonts.googleapis.com/css?family=' + FONT;
|
||||
// style.innerText = '@import url("' + url + '")';
|
||||
style.innerText = (
|
||||
// style.textContent = '@import url("' + url + '")';
|
||||
style.textContent = (
|
||||
'@font-face{' +
|
||||
'font-family:"' + Handlee.name + '";' +
|
||||
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
|
||||
|
@ -9963,6 +10130,29 @@ define('sequence/SequenceDiagram',[
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class SequenceDiagram extends EventObject {
|
||||
constructor(code = null, options = {}) {
|
||||
super();
|
||||
|
@ -9988,7 +10178,7 @@ define('sequence/SequenceDiagram',[
|
|||
if(options.interactive) {
|
||||
this.addInteractivity();
|
||||
}
|
||||
if(typeof this.code === 'string') {
|
||||
if(typeof this.code === 'string' && options.render !== false) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
@ -10005,14 +10195,16 @@ define('sequence/SequenceDiagram',[
|
|||
}, options));
|
||||
}
|
||||
|
||||
set(code = '') {
|
||||
set(code = '', {render = true} = {}) {
|
||||
if(this.code === code) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.code = code;
|
||||
if(render) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
process(code) {
|
||||
const parsed = this.parser.parse(code);
|
||||
|
@ -10109,29 +10301,92 @@ define('sequence/SequenceDiagram',[
|
|||
};
|
||||
}
|
||||
|
||||
render(processed = null) {
|
||||
_revertParent(state) {
|
||||
const dom = this.renderer.svg();
|
||||
const originalParent = dom.parentNode;
|
||||
if(dom.parentNode !== state.originalParent) {
|
||||
document.body.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.svg();
|
||||
this.renderState = {
|
||||
originalParent: dom.parentNode,
|
||||
processed,
|
||||
error: false,
|
||||
};
|
||||
const state = this.renderState;
|
||||
|
||||
if(!document.body.contains(dom)) {
|
||||
if(originalParent) {
|
||||
originalParent.removeChild(dom);
|
||||
if(state.originalParent) {
|
||||
state.originalParent.removeChild(dom);
|
||||
}
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
try {
|
||||
if(!processed) {
|
||||
processed = this.process(this.code);
|
||||
if(!state.processed) {
|
||||
state.processed = this.process(this.code);
|
||||
}
|
||||
this.renderer.render(processed);
|
||||
this.latestProcessed = processed;
|
||||
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]);
|
||||
} finally {
|
||||
if(dom.parentNode !== originalParent) {
|
||||
document.body.removeChild(dom);
|
||||
if(originalParent) {
|
||||
originalParent.appendChild(dom);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10160,6 +10415,10 @@ define('sequence/SequenceDiagram',[
|
|||
return extractCodeFromSVG(svg);
|
||||
}
|
||||
|
||||
renderAll(diagrams) {
|
||||
return renderAll(diagrams);
|
||||
}
|
||||
|
||||
dom() {
|
||||
return this.renderer.svg();
|
||||
}
|
||||
|
@ -10176,16 +10435,13 @@ define('sequence/SequenceDiagram',[
|
|||
};
|
||||
}
|
||||
|
||||
function convert(element, code = null, options = {}) {
|
||||
function convertOne(element, code = null, options = {}) {
|
||||
if(element.tagName === 'svg') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(code === null) {
|
||||
code = element.innerText;
|
||||
} else if(typeof code === 'object') {
|
||||
options = code;
|
||||
code = options.code;
|
||||
code = element.textContent;
|
||||
}
|
||||
|
||||
const tagOptions = parseTagOptions(element);
|
||||
|
@ -10207,6 +10463,24 @@ define('sequence/SequenceDiagram',[
|
|||
return diagram;
|
||||
}
|
||||
|
||||
function convert(elements, code = null, options = {}) {
|
||||
if(code && typeof code === 'object') {
|
||||
options = code;
|
||||
code = options.code;
|
||||
}
|
||||
|
||||
if(Array.isArray(elements)) {
|
||||
const opts = Object.assign({}, options, {render: false});
|
||||
const diagrams = elements.map((el) => convertOne(el, code, opts));
|
||||
if(options.render !== false) {
|
||||
renderAll(diagrams);
|
||||
}
|
||||
return diagrams;
|
||||
} else {
|
||||
return convertOne(elements, code, options);
|
||||
}
|
||||
}
|
||||
|
||||
function convertAll(root = null, className = 'sequence-diagram') {
|
||||
if(typeof root === 'string') {
|
||||
className = root;
|
||||
|
@ -10218,13 +10492,15 @@ define('sequence/SequenceDiagram',[
|
|||
} else {
|
||||
elements = (root || document).getElementsByClassName(className);
|
||||
}
|
||||
|
||||
// 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
|
||||
els.forEach((el) => convert(el));
|
||||
convert(els);
|
||||
}
|
||||
|
||||
return Object.assign(SequenceDiagram, {
|
||||
|
@ -10237,6 +10513,7 @@ define('sequence/SequenceDiagram',[
|
|||
addTheme,
|
||||
registerCodeMirrorMode,
|
||||
extractCodeFromSVG,
|
||||
renderAll,
|
||||
convert,
|
||||
convertAll,
|
||||
});
|
||||
|
|
File diff suppressed because one or more lines are too long
10
library.htm
10
library.htm
|
@ -61,24 +61,30 @@
|
|||
|
||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const diagrams = [];
|
||||
|
||||
// Example 1:
|
||||
(() => {
|
||||
const diagram = new SequenceDiagram();
|
||||
diagram.set('A -> B\nB -> A');
|
||||
diagram.set('A -> B\nB -> A', {render: false});
|
||||
diagram.dom().setAttribute('class', 'sequence-diagram');
|
||||
document.getElementById('hold1').appendChild(diagram.dom());
|
||||
diagram.setHighlight(1);
|
||||
diagrams.push(diagram);
|
||||
})();
|
||||
|
||||
// Snippets:
|
||||
const elements = document.getElementsByClassName('example');
|
||||
for(let i = 0; i < elements.length; ++ i) {
|
||||
const el = elements[i];
|
||||
const diagram = new SequenceDiagram(el.innerText);
|
||||
const diagram = new SequenceDiagram(el.textContent, {render: false});
|
||||
diagram.dom().setAttribute('class', 'example-diagram');
|
||||
el.parentNode.insertBefore(diagram.dom(), el);
|
||||
diagrams.push(diagram);
|
||||
}
|
||||
|
||||
SequenceDiagram.renderAll(diagrams);
|
||||
|
||||
CodeMirror.colorize();
|
||||
|
||||
}, {once: true});</script>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
const links = [];
|
||||
for(let i = 0; i < linkElements.length; ++ i) {
|
||||
links.push({
|
||||
label: linkElements[i].innerText,
|
||||
label: linkElements[i].textContent,
|
||||
href: linkElements[i].getAttribute('href'),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -289,7 +289,7 @@ define(['require'], (require) => {
|
|||
}
|
||||
|
||||
buildLibrary(container) {
|
||||
this.library.forEach((lib) => {
|
||||
const diagrams = this.library.map((lib) => {
|
||||
const holdInner = makeNode('div', {
|
||||
'title': lib.title || lib.code,
|
||||
});
|
||||
|
@ -301,17 +301,22 @@ define(['require'], (require) => {
|
|||
this.addCodeBlock.bind(this, lib.code)
|
||||
);
|
||||
container.appendChild(hold);
|
||||
try {
|
||||
this.diagram.clone({
|
||||
const diagram = this.diagram.clone({
|
||||
code: simplifyPreview(lib.preview || lib.code),
|
||||
container: holdInner,
|
||||
render: false,
|
||||
});
|
||||
} catch(e) {
|
||||
window.console.log('Failed to render preview', e);
|
||||
diagram.addEventListener('error', (sd, e) => {
|
||||
window.console.warn('Failed to render preview', e);
|
||||
hold.setAttribute('class', 'library-item broken');
|
||||
holdInner.appendChild(makeText(lib.code));
|
||||
}
|
||||
holdInner.textContent = lib.code;
|
||||
});
|
||||
return diagram;
|
||||
});
|
||||
|
||||
try {
|
||||
this.diagram.renderAll(diagrams);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
buildErrorReport() {
|
||||
|
|
|
@ -99,6 +99,8 @@ define([
|
|||
|
||||
_bindMethods() {
|
||||
this.separationStage = this.separationStage.bind(this);
|
||||
this.prepareMeasurementsStage =
|
||||
this.prepareMeasurementsStage.bind(this);
|
||||
this.renderStage = this.renderStage.bind(this);
|
||||
this.addThemeDef = this.addThemeDef.bind(this);
|
||||
this.addDef = this.addDef.bind(this);
|
||||
|
@ -294,6 +296,24 @@ define([
|
|||
});
|
||||
}
|
||||
|
||||
prepareMeasurementsStage(stage) {
|
||||
const env = {
|
||||
renderer: this,
|
||||
theme: this.theme,
|
||||
agentInfos: this.agentInfos,
|
||||
textSizer: this.sizer,
|
||||
state: this.state,
|
||||
components: this.components,
|
||||
};
|
||||
|
||||
const component = this.components.get(stage.type);
|
||||
if(!component) {
|
||||
throw new Error('Unknown component: ' + stage.type);
|
||||
}
|
||||
|
||||
component.prepareMeasurements(stage, env);
|
||||
}
|
||||
|
||||
checkAgentRange(agentIDs, topY = 0) {
|
||||
if(agentIDs.length === 0) {
|
||||
return topY;
|
||||
|
@ -467,7 +487,7 @@ define([
|
|||
});
|
||||
}
|
||||
|
||||
buildAgentInfos(agents, stages) {
|
||||
buildAgentInfos(agents) {
|
||||
this.agentInfos = new Map();
|
||||
agents.forEach((agent, index) => {
|
||||
this.agentInfos.set(agent.id, {
|
||||
|
@ -487,11 +507,6 @@ define([
|
|||
separations: new Map(),
|
||||
});
|
||||
});
|
||||
|
||||
this.visibleAgentIDs = ['[', ']'];
|
||||
stages.forEach(this.separationStage);
|
||||
|
||||
this.positionAgents();
|
||||
}
|
||||
|
||||
updateBounds(stagesHeight) {
|
||||
|
@ -623,16 +638,18 @@ define([
|
|||
return this._setCollapsed(line, collapsed);
|
||||
}
|
||||
|
||||
render(sequence) {
|
||||
const prevHighlight = this.currentHighlight;
|
||||
_switchTheme(name) {
|
||||
const oldTheme = this.theme;
|
||||
this.theme = this.getThemeNamed(name);
|
||||
this.theme.reset();
|
||||
|
||||
this.theme = this.getThemeNamed(sequence.meta.theme);
|
||||
return (this.theme !== oldTheme);
|
||||
}
|
||||
|
||||
const themeChanged = (this.theme !== oldTheme);
|
||||
optimisedRenderPreReflow(sequence) {
|
||||
const themeChanged = this._switchTheme(sequence.meta.theme);
|
||||
this._reset(themeChanged);
|
||||
|
||||
this.theme.reset();
|
||||
this.metaCode.nodeValue = sequence.meta.code;
|
||||
this.theme.addDefs(this.addThemeDef);
|
||||
|
||||
|
@ -640,21 +657,47 @@ define([
|
|||
attrs: this.theme.titleAttrs,
|
||||
formatted: sequence.meta.title,
|
||||
});
|
||||
this.sizer.expectMeasure(this.title);
|
||||
|
||||
this.minX = 0;
|
||||
this.maxX = 0;
|
||||
this.buildAgentInfos(sequence.agents, sequence.stages);
|
||||
|
||||
this.buildAgentInfos(sequence.agents);
|
||||
|
||||
sequence.stages.forEach(this.prepareMeasurementsStage);
|
||||
this._resetState();
|
||||
this.sizer.performMeasurementsPre();
|
||||
}
|
||||
|
||||
optimisedRenderReflow() {
|
||||
this.sizer.performMeasurementsAct();
|
||||
}
|
||||
|
||||
optimisedRenderPostReflow(sequence) {
|
||||
this.visibleAgentIDs = ['[', ']'];
|
||||
sequence.stages.forEach(this.separationStage);
|
||||
this._resetState();
|
||||
|
||||
this.positionAgents();
|
||||
|
||||
sequence.stages.forEach(this.renderStage);
|
||||
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
||||
|
||||
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
||||
this.updateBounds(stagesHeight);
|
||||
|
||||
this.sizer.resetCache();
|
||||
this.sizer.detach();
|
||||
const prevHighlight = this.currentHighlight;
|
||||
this.currentHighlight = -1;
|
||||
this.setHighlight(prevHighlight);
|
||||
|
||||
this.sizer.performMeasurementsPost();
|
||||
this.sizer.resetCache();
|
||||
}
|
||||
|
||||
render(sequence) {
|
||||
this.optimisedRenderPreReflow(sequence);
|
||||
this.optimisedRenderReflow();
|
||||
this.optimisedRenderPostReflow(sequence);
|
||||
}
|
||||
|
||||
getThemeNames() {
|
||||
|
|
|
@ -60,6 +60,29 @@ define([
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class SequenceDiagram extends EventObject {
|
||||
constructor(code = null, options = {}) {
|
||||
super();
|
||||
|
@ -85,7 +108,7 @@ define([
|
|||
if(options.interactive) {
|
||||
this.addInteractivity();
|
||||
}
|
||||
if(typeof this.code === 'string') {
|
||||
if(typeof this.code === 'string' && options.render !== false) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
@ -102,14 +125,16 @@ define([
|
|||
}, options));
|
||||
}
|
||||
|
||||
set(code = '') {
|
||||
set(code = '', {render = true} = {}) {
|
||||
if(this.code === code) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.code = code;
|
||||
if(render) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
process(code) {
|
||||
const parsed = this.parser.parse(code);
|
||||
|
@ -206,29 +231,92 @@ define([
|
|||
};
|
||||
}
|
||||
|
||||
render(processed = null) {
|
||||
_revertParent(state) {
|
||||
const dom = this.renderer.svg();
|
||||
const originalParent = dom.parentNode;
|
||||
if(dom.parentNode !== state.originalParent) {
|
||||
document.body.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.svg();
|
||||
this.renderState = {
|
||||
originalParent: dom.parentNode,
|
||||
processed,
|
||||
error: false,
|
||||
};
|
||||
const state = this.renderState;
|
||||
|
||||
if(!document.body.contains(dom)) {
|
||||
if(originalParent) {
|
||||
originalParent.removeChild(dom);
|
||||
if(state.originalParent) {
|
||||
state.originalParent.removeChild(dom);
|
||||
}
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
try {
|
||||
if(!processed) {
|
||||
processed = this.process(this.code);
|
||||
if(!state.processed) {
|
||||
state.processed = this.process(this.code);
|
||||
}
|
||||
this.renderer.render(processed);
|
||||
this.latestProcessed = processed;
|
||||
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]);
|
||||
} finally {
|
||||
if(dom.parentNode !== originalParent) {
|
||||
document.body.removeChild(dom);
|
||||
if(originalParent) {
|
||||
originalParent.appendChild(dom);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,6 +345,10 @@ define([
|
|||
return extractCodeFromSVG(svg);
|
||||
}
|
||||
|
||||
renderAll(diagrams) {
|
||||
return renderAll(diagrams);
|
||||
}
|
||||
|
||||
dom() {
|
||||
return this.renderer.svg();
|
||||
}
|
||||
|
@ -273,16 +365,13 @@ define([
|
|||
};
|
||||
}
|
||||
|
||||
function convert(element, code = null, options = {}) {
|
||||
function convertOne(element, code = null, options = {}) {
|
||||
if(element.tagName === 'svg') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(code === null) {
|
||||
code = element.innerText;
|
||||
} else if(typeof code === 'object') {
|
||||
options = code;
|
||||
code = options.code;
|
||||
code = element.textContent;
|
||||
}
|
||||
|
||||
const tagOptions = parseTagOptions(element);
|
||||
|
@ -304,6 +393,24 @@ define([
|
|||
return diagram;
|
||||
}
|
||||
|
||||
function convert(elements, code = null, options = {}) {
|
||||
if(code && typeof code === 'object') {
|
||||
options = code;
|
||||
code = options.code;
|
||||
}
|
||||
|
||||
if(Array.isArray(elements)) {
|
||||
const opts = Object.assign({}, options, {render: false});
|
||||
const diagrams = elements.map((el) => convertOne(el, code, opts));
|
||||
if(options.render !== false) {
|
||||
renderAll(diagrams);
|
||||
}
|
||||
return diagrams;
|
||||
} else {
|
||||
return convertOne(elements, code, options);
|
||||
}
|
||||
}
|
||||
|
||||
function convertAll(root = null, className = 'sequence-diagram') {
|
||||
if(typeof root === 'string') {
|
||||
className = root;
|
||||
|
@ -315,13 +422,15 @@ define([
|
|||
} else {
|
||||
elements = (root || document).getElementsByClassName(className);
|
||||
}
|
||||
|
||||
// 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
|
||||
els.forEach((el) => convert(el));
|
||||
convert(els);
|
||||
}
|
||||
|
||||
return Object.assign(SequenceDiagram, {
|
||||
|
@ -334,6 +443,7 @@ define([
|
|||
addTheme,
|
||||
registerCodeMirrorMode,
|
||||
extractCodeFromSVG,
|
||||
renderAll,
|
||||
convert,
|
||||
convertAll,
|
||||
});
|
||||
|
|
|
@ -20,6 +20,11 @@ define([
|
|||
return config || env.theme.agentCap.box;
|
||||
}
|
||||
|
||||
prepareMeasurements({formattedLabel, options}, env) {
|
||||
const config = this.getConfig(options, env);
|
||||
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
|
||||
}
|
||||
|
||||
separation({formattedLabel, options}, env) {
|
||||
const config = this.getConfig(options, env);
|
||||
const width = (
|
||||
|
@ -78,6 +83,9 @@ define([
|
|||
}
|
||||
|
||||
class CapCross {
|
||||
prepareMeasurements() {
|
||||
}
|
||||
|
||||
separation(agentInfo, env) {
|
||||
const config = env.theme.agentCap.cross;
|
||||
return {
|
||||
|
@ -122,6 +130,11 @@ define([
|
|||
}
|
||||
|
||||
class CapBar {
|
||||
prepareMeasurements({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
|
||||
}
|
||||
|
||||
separation({formattedLabel}, env) {
|
||||
const config = env.theme.agentCap.box;
|
||||
const width = (
|
||||
|
@ -178,6 +191,9 @@ define([
|
|||
}
|
||||
|
||||
class CapFade {
|
||||
prepareMeasurements() {
|
||||
}
|
||||
|
||||
separation({currentRad}) {
|
||||
return {
|
||||
left: currentRad,
|
||||
|
@ -239,6 +255,9 @@ define([
|
|||
}
|
||||
|
||||
class CapNone {
|
||||
prepareMeasurements() {
|
||||
}
|
||||
|
||||
separation({currentRad}) {
|
||||
return {
|
||||
left: currentRad,
|
||||
|
@ -287,6 +306,14 @@ define([
|
|||
this.begin = begin;
|
||||
}
|
||||
|
||||
prepareMeasurements({mode, agentIDs}, env) {
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
const cap = AGENT_CAPS[mode];
|
||||
cap.prepareMeasurements(agentInfo, env, this.begin);
|
||||
});
|
||||
}
|
||||
|
||||
separationPre({mode, agentIDs}, env) {
|
||||
agentIDs.forEach((id) => {
|
||||
const agentInfo = env.agentInfos.get(id);
|
||||
|
|
|
@ -9,6 +9,16 @@ define(() => {
|
|||
this.makeState(state);
|
||||
}
|
||||
|
||||
prepareMeasurements(/*stage, {
|
||||
renderer,
|
||||
theme,
|
||||
agentInfos,
|
||||
textSizer,
|
||||
state,
|
||||
components,
|
||||
}*/) {
|
||||
}
|
||||
|
||||
separationPre(/*stage, {
|
||||
renderer,
|
||||
theme,
|
||||
|
|
|
@ -12,6 +12,13 @@ define([
|
|||
'use strict';
|
||||
|
||||
class BlockSplit extends BaseComponent {
|
||||
prepareMeasurements({left, tag, label}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.type).section;
|
||||
env.textSizer.expectMeasure(config.tag.labelAttrs, tag);
|
||||
env.textSizer.expectMeasure(config.label.labelAttrs, label);
|
||||
}
|
||||
|
||||
separation({left, right, tag, label}, env) {
|
||||
const blockInfo = env.state.blocks.get(left);
|
||||
const config = env.theme.getBlock(blockInfo.type).section;
|
||||
|
@ -126,8 +133,14 @@ define([
|
|||
return blockInfo;
|
||||
}
|
||||
|
||||
prepareMeasurements(stage, env) {
|
||||
this.storeBlockInfo(stage, env);
|
||||
super.prepareMeasurements(stage, env);
|
||||
}
|
||||
|
||||
separationPre(stage, env) {
|
||||
this.storeBlockInfo(stage, env);
|
||||
super.separationPre(stage, env);
|
||||
}
|
||||
|
||||
separation(stage, env) {
|
||||
|
|
|
@ -110,6 +110,15 @@ define([
|
|||
];
|
||||
|
||||
class Connect extends BaseComponent {
|
||||
prepareMeasurements({agentIDs, label}, env) {
|
||||
const config = env.theme.connect;
|
||||
const loopback = (agentIDs[0] === agentIDs[1]);
|
||||
const labelAttrs = (loopback ?
|
||||
config.label.loopbackAttrs : config.label.attrs);
|
||||
|
||||
env.textSizer.expectMeasure(labelAttrs, label);
|
||||
}
|
||||
|
||||
separationPre({agentIDs}, env) {
|
||||
const r = env.theme.connect.source.radius;
|
||||
agentIDs.forEach((id) => {
|
||||
|
@ -128,15 +137,17 @@ define([
|
|||
const lArrow = ARROWHEADS[options.left];
|
||||
const rArrow = ARROWHEADS[options.right];
|
||||
|
||||
let labelWidth = (
|
||||
env.textSizer.measure(config.label.attrs, label).width
|
||||
);
|
||||
const loopback = (agentIDs[0] === agentIDs[1]);
|
||||
const labelAttrs = (loopback ?
|
||||
config.label.loopbackAttrs : config.label.attrs);
|
||||
|
||||
let labelWidth = env.textSizer.measure(labelAttrs, label).width;
|
||||
if(labelWidth > 0) {
|
||||
labelWidth += config.label.padding * 2;
|
||||
}
|
||||
|
||||
const info1 = env.agentInfos.get(agentIDs[0]);
|
||||
if(agentIDs[0] === agentIDs[1]) {
|
||||
if(loopback) {
|
||||
env.addSpacing(agentIDs[0], {
|
||||
left: 0,
|
||||
right: (
|
||||
|
@ -490,6 +501,8 @@ define([
|
|||
}
|
||||
|
||||
class ConnectDelayEnd extends Connect {
|
||||
prepareMeasurements() {}
|
||||
|
||||
separationPre() {}
|
||||
|
||||
separation() {}
|
||||
|
|
|
@ -10,6 +10,11 @@ define([
|
|||
'use strict';
|
||||
|
||||
class Divider extends BaseComponent {
|
||||
prepareMeasurements({mode, formattedLabel}, env) {
|
||||
const config = env.theme.getDivider(mode);
|
||||
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
|
||||
}
|
||||
|
||||
separation({mode, formattedLabel}, env) {
|
||||
const config = env.theme.getDivider(mode);
|
||||
|
||||
|
|
|
@ -20,6 +20,11 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}
|
||||
|
||||
class NoteComponent extends BaseComponent {
|
||||
prepareMeasurements({mode, label}, env) {
|
||||
const config = env.theme.getNote(mode);
|
||||
env.textSizer.expectMeasure(config.labelAttrs, label);
|
||||
}
|
||||
|
||||
renderPre({agentIDs}) {
|
||||
return {agentIDs};
|
||||
}
|
||||
|
|
|
@ -28,16 +28,23 @@ define([
|
|||
}
|
||||
|
||||
class Parallel extends BaseComponent {
|
||||
separationPre(stage, env) {
|
||||
stage.stages.forEach((subStage) => {
|
||||
env.components.get(subStage.type).separationPre(subStage, env);
|
||||
invokeChildren(stage, env, methodName) {
|
||||
return stage.stages.map((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
return component[methodName](subStage, env);
|
||||
});
|
||||
}
|
||||
|
||||
prepareMeasurements(stage, env) {
|
||||
this.invokeChildren(stage, env, 'prepareMeasurements');
|
||||
}
|
||||
|
||||
separationPre(stage, env) {
|
||||
this.invokeChildren(stage, env, 'separationPre');
|
||||
}
|
||||
|
||||
separation(stage, env) {
|
||||
stage.stages.forEach((subStage) => {
|
||||
env.components.get(subStage.type).separation(subStage, env);
|
||||
});
|
||||
this.invokeChildren(stage, env, 'separation');
|
||||
}
|
||||
|
||||
renderPre(stage, env) {
|
||||
|
@ -47,11 +54,9 @@ define([
|
|||
asynchronousY: null,
|
||||
};
|
||||
|
||||
return stage.stages.map((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
const subResult = component.renderPre(subStage, env);
|
||||
return BaseComponent.cleanRenderPreResult(subResult);
|
||||
}).reduce(mergeResults, baseResults);
|
||||
return this.invokeChildren(stage, env, 'renderPre')
|
||||
.map((r) => BaseComponent.cleanRenderPreResult(r))
|
||||
.reduce(mergeResults, baseResults);
|
||||
}
|
||||
|
||||
render(stage, env) {
|
||||
|
@ -73,24 +78,19 @@ define([
|
|||
}
|
||||
|
||||
renderHidden(stage, env) {
|
||||
stage.stages.forEach((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
component.renderHidden(subStage, env);
|
||||
});
|
||||
this.invokeChildren(stage, env, 'renderHidden');
|
||||
}
|
||||
|
||||
shouldHide(stage, env) {
|
||||
const result = {
|
||||
const baseResults = {
|
||||
self: false,
|
||||
nest: 0,
|
||||
};
|
||||
stage.stages.forEach((subStage) => {
|
||||
const component = env.components.get(subStage.type);
|
||||
const hide = component.shouldHide(subStage, env) || {};
|
||||
result.self = (result.self || Boolean(hide.self));
|
||||
result.nest += (hide.nest || 0);
|
||||
});
|
||||
return result;
|
||||
return this.invokeChildren(stage, env, 'shouldHide')
|
||||
.reduce((result, {self = false, nest = 0} = {}) => ({
|
||||
self: result.self || Boolean(self),
|
||||
nest: result.nest + nest,
|
||||
}), baseResults);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -491,8 +491,8 @@ define([
|
|||
// but this fails when exporting as SVG / PNG (svg tags must
|
||||
// have no external dependencies).
|
||||
// const url = 'https://fonts.googleapis.com/css?family=' + FONT;
|
||||
// style.innerText = '@import url("' + url + '")';
|
||||
style.innerText = (
|
||||
// style.textContent = '@import url("' + url + '")';
|
||||
style.textContent = (
|
||||
'@font-face{' +
|
||||
'font-family:"' + Handlee.name + '";' +
|
||||
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
|
||||
|
|
|
@ -114,25 +114,60 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
}
|
||||
|
||||
class SizeTester {
|
||||
measure(attrs, formatted) {
|
||||
if(attrs.state) {
|
||||
formatted = attrs.state.formatted;
|
||||
attrs = attrs.state.attrs;
|
||||
constructor() {
|
||||
this.expected = new Set();
|
||||
this.measured = new Set();
|
||||
}
|
||||
|
||||
if(!formatted || !formatted.length) {
|
||||
_getMeasurementOpts(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
if(typeof attrs === 'object' && attrs.state) {
|
||||
formatted = attrs.state.formatted || [];
|
||||
attrs = attrs.state.attrs;
|
||||
} else {
|
||||
formatted = [];
|
||||
}
|
||||
} else if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
return {attrs, formatted};
|
||||
}
|
||||
|
||||
expectMeasure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
this.expected.add(JSON.stringify(opts));
|
||||
}
|
||||
|
||||
performMeasurementsPre() {
|
||||
}
|
||||
|
||||
performMeasurementsAct() {
|
||||
this.measured = new Set(this.expected);
|
||||
}
|
||||
|
||||
performMeasurementsPost() {
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
|
||||
if(!this.measured.has(JSON.stringify(opts))) {
|
||||
throw new Error('Unexpected measurement', opts);
|
||||
}
|
||||
|
||||
if(!opts.formatted || !opts.formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
|
||||
let width = 0;
|
||||
formatted.forEach((line) => {
|
||||
opts.formatted.forEach((line) => {
|
||||
const length = line.reduce((v, pt) => v + pt.text.length, 0);
|
||||
width = Math.max(width, length);
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height: formatted.length,
|
||||
height: opts.formatted.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -145,9 +180,8 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
}
|
||||
|
||||
resetCache() {
|
||||
}
|
||||
|
||||
detach() {
|
||||
this.expected.clear();
|
||||
this.measured.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,27 +38,6 @@ define(['./SVGUtilities'], (svg) => {
|
|||
});
|
||||
}
|
||||
|
||||
function measureLine(tester, line) {
|
||||
if(!line.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labelKey = JSON.stringify(line);
|
||||
const knownWidth = tester.widths.get(labelKey);
|
||||
if(knownWidth !== undefined) {
|
||||
return knownWidth;
|
||||
}
|
||||
|
||||
// getComputedTextLength forces a reflow, so only call it if nothing
|
||||
// else can tell us the length
|
||||
|
||||
svg.empty(tester.node);
|
||||
populateSvgTextLine(tester.node, line);
|
||||
const width = tester.node.getComputedTextLength();
|
||||
tester.widths.set(labelKey, width);
|
||||
return width;
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
|
@ -175,35 +154,68 @@ define(['./SVGUtilities'], (svg) => {
|
|||
});
|
||||
this.container = container;
|
||||
this.cache = new Map();
|
||||
this.nodes = null;
|
||||
}
|
||||
|
||||
_expectMeasure({attrs, formatted}) {
|
||||
if(!formatted.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let attrCache = this.cache.get(attrKey);
|
||||
if(!attrCache) {
|
||||
attrCache = {
|
||||
attrs,
|
||||
lines: new Map(),
|
||||
};
|
||||
this.cache.set(attrKey, attrCache);
|
||||
}
|
||||
|
||||
formatted.forEach((line) => {
|
||||
if(!line.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelKey = JSON.stringify(line);
|
||||
if(!attrCache.lines.has(labelKey)) {
|
||||
attrCache.lines.set(labelKey, {
|
||||
formatted: line,
|
||||
width: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return attrCache;
|
||||
}
|
||||
|
||||
_measureHeight({attrs, formatted}) {
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
}
|
||||
|
||||
_measureWidth({attrs, formatted}) {
|
||||
if(!formatted.length) {
|
||||
_measureLine(attrCache, line) {
|
||||
if(!line.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let tester = this.cache.get(attrKey);
|
||||
if(!tester) {
|
||||
const node = svg.make('text', attrs);
|
||||
this.testers.appendChild(node);
|
||||
tester = {
|
||||
node,
|
||||
widths: new Map(),
|
||||
};
|
||||
this.cache.set(attrKey, tester);
|
||||
const labelKey = JSON.stringify(line);
|
||||
const cache = attrCache.lines.get(labelKey);
|
||||
if(cache.width === null) {
|
||||
window.console.warn('Performing unexpected measurement', line);
|
||||
this.performMeasurements();
|
||||
}
|
||||
return cache.width;
|
||||
}
|
||||
|
||||
if(!this.testers.parentNode) {
|
||||
this.container.appendChild(this.testers);
|
||||
_measureWidth(opts) {
|
||||
if(!opts.formatted.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (formatted
|
||||
.map((line) => measureLine(tester, line))
|
||||
const attrCache = this._expectMeasure(opts);
|
||||
|
||||
return (opts.formatted
|
||||
.map((line) => this._measureLine(attrCache, line))
|
||||
.reduce((a, b) => Math.max(a, b), 0)
|
||||
);
|
||||
}
|
||||
|
@ -222,6 +234,52 @@ define(['./SVGUtilities'], (svg) => {
|
|||
return {attrs, formatted};
|
||||
}
|
||||
|
||||
expectMeasure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
this._expectMeasure(opts);
|
||||
}
|
||||
|
||||
performMeasurementsPre() {
|
||||
this.nodes = [];
|
||||
this.cache.forEach(({attrs, lines}) => {
|
||||
lines.forEach((cacheLine) => {
|
||||
if(cacheLine.width === null) {
|
||||
const node = svg.make('text', attrs);
|
||||
populateSvgTextLine(node, cacheLine.formatted);
|
||||
this.testers.appendChild(node);
|
||||
this.nodes.push({node, cacheLine});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if(this.nodes.length) {
|
||||
this.container.appendChild(this.testers);
|
||||
}
|
||||
}
|
||||
|
||||
performMeasurementsAct() {
|
||||
this.nodes.forEach(({node, cacheLine}) => {
|
||||
cacheLine.width = node.getComputedTextLength();
|
||||
});
|
||||
}
|
||||
|
||||
performMeasurementsPost() {
|
||||
if(this.nodes.length) {
|
||||
this.container.removeChild(this.testers);
|
||||
svg.empty(this.testers);
|
||||
}
|
||||
this.nodes = null;
|
||||
}
|
||||
|
||||
performMeasurements() {
|
||||
// getComputedTextLength forces a reflow, so we try to batch as
|
||||
// many measurements as possible into a single DOM change
|
||||
|
||||
this.performMeasurementsPre();
|
||||
this.performMeasurementsAct();
|
||||
this.performMeasurementsPost();
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
return {
|
||||
|
@ -236,15 +294,8 @@ define(['./SVGUtilities'], (svg) => {
|
|||
}
|
||||
|
||||
resetCache() {
|
||||
svg.empty(this.testers);
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
detach() {
|
||||
if(this.testers.parentNode) {
|
||||
this.container.removeChild(this.testers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SVGTextBlock.SizeTester = SizeTester;
|
||||
|
|
|
@ -206,23 +206,5 @@ defineDescribe('SVGTextBlock', [
|
|||
expect(hold.children.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.detach', () => {
|
||||
it('removes the test node from the DOM', () => {
|
||||
tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
tester.detach();
|
||||
expect(hold.children.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('does not prevent using the tester again later', () => {
|
||||
tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
tester.detach();
|
||||
|
||||
const size = tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue