Add support for collapsing blocks [#46]

This commit is contained in:
David Evans 2018-02-04 14:59:46 +00:00
parent 46fdca1599
commit e22381e37d
24 changed files with 825 additions and 46 deletions

View File

@ -2736,6 +2736,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
blockType, blockType,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
label: this.textFormatter(label), label: this.textFormatter(label),
canHide: true,
left: leftGAgent.id, left: leftGAgent.id,
right: rightGAgent.id, right: rightGAgent.id,
ln, ln,
@ -2881,6 +2882,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
type: 'block begin', type: 'block begin',
blockType, blockType,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
canHide: false,
label: this.textFormatter(label), label: this.textFormatter(label),
left: details.leftGAgent.id, left: details.leftGAgent.id,
right: details.rightGAgent.id, right: details.rightGAgent.id,
@ -3898,6 +3900,7 @@ define('sequence/components/BaseComponent',[],() => {
} }
separationPre(/*stage, { separationPre(/*stage, {
renderer,
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
@ -3911,6 +3914,7 @@ define('sequence/components/BaseComponent',[],() => {
} }
separation(/*stage, { separation(/*stage, {
renderer,
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
@ -3924,6 +3928,7 @@ define('sequence/components/BaseComponent',[],() => {
} }
renderPre(/*stage, { renderPre(/*stage, {
renderer,
theme, theme,
agentInfos, agentInfos,
textSizer, textSizer,
@ -3934,6 +3939,7 @@ define('sequence/components/BaseComponent',[],() => {
} }
render(/*stage, { render(/*stage, {
renderer,
topY, topY,
primaryY, primaryY,
fillLayer, fillLayer,
@ -3949,6 +3955,22 @@ define('sequence/components/BaseComponent',[],() => {
}*/) { }*/) {
// return bottom Y coordinate // return bottom Y coordinate
} }
renderHidden(/*stage, {
(same args as render, with primaryY = topY)
}*/) {
}
shouldHide(/*stage, {
renderer,
theme,
agentInfos,
textSizer,
state,
components,
}*/) {
// return {self, nest}
}
} }
BaseComponent.cleanRenderPreResult = ({ BaseComponent.cleanRenderPreResult = ({
@ -4070,6 +4092,12 @@ define('sequence/components/Block',[
'x2': agentInfoR.x, 'x2': agentInfoR.x,
'y2': y, 'y2': y,
})); }));
} else if(blockInfo.canHide) {
clickable.setAttribute(
'class',
clickable.getAttribute('class') +
(blockInfo.hide ? ' collapsed' : ' expanded')
);
} }
return y + labelHeight + config.section.padding.top; return y + labelHeight + config.section.padding.top;
@ -4086,8 +4114,11 @@ define('sequence/components/Block',[
} }
storeBlockInfo(stage, env) { storeBlockInfo(stage, env) {
const canHide = stage.canHide;
const blockInfo = { const blockInfo = {
type: stage.blockType, type: stage.blockType,
canHide,
hide: canHide && env.renderer.isCollapsed(stage.ln),
hold: null, hold: null,
startY: null, startY: null,
}; };
@ -4125,6 +4156,14 @@ define('sequence/components/Block',[
return super.render(stage, env, true); return super.render(stage, env, true);
} }
shouldHide({left}, env) {
const blockInfo = env.state.blocks.get(left);
return {
self: false,
nest: blockInfo.hide ? 1 : 0,
};
}
} }
class BlockEnd extends BaseComponent { class BlockEnd extends BaseComponent {
@ -4148,7 +4187,12 @@ define('sequence/components/Block',[
const agentInfoL = env.agentInfos.get(left); const agentInfoL = env.agentInfos.get(left);
const agentInfoR = env.agentInfos.get(right); const agentInfoR = env.agentInfos.get(right);
let shapes = config.boxRenderer({ let renderFn = config.boxRenderer;
if(blockInfo.hide) {
renderFn = config.collapsedBoxRenderer || renderFn;
}
let shapes = renderFn({
x: agentInfoL.x, x: agentInfoL.x,
y: blockInfo.startY, y: blockInfo.startY,
width: agentInfoR.x - agentInfoL.x, width: agentInfoR.x - agentInfoL.x,
@ -4168,6 +4212,14 @@ define('sequence/components/Block',[
return env.primaryY + config.margin.bottom + env.theme.actionMargin; return env.primaryY + config.margin.bottom + env.theme.actionMargin;
} }
shouldHide({left}, env) {
const blockInfo = env.state.blocks.get(left);
return {
self: false,
nest: blockInfo.hide ? -1 : 0,
};
}
} }
BaseComponent.register('block begin', new BlockBegin()); BaseComponent.register('block begin', new BlockBegin());
@ -4254,6 +4306,27 @@ define('sequence/components/Parallel',[
env.makeRegion = originalMakeRegion; env.makeRegion = originalMakeRegion;
return bottomY; return bottomY;
} }
renderHidden(stage, env) {
stage.stages.forEach((subStage) => {
const component = env.components.get(subStage.type);
component.renderHidden(subStage, env);
});
}
shouldHide(stage, env) {
const result = {
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;
}
} }
BaseComponent.register('parallel', new Parallel()); BaseComponent.register('parallel', new Parallel());
@ -4276,6 +4349,10 @@ define('sequence/components/Marker',['./BaseComponent'], (BaseComponent) => {
render({name}, {topY, state}) { render({name}, {topY, state}) {
state.marks.set(name, topY); state.marks.set(name, topY);
} }
renderHidden(stage, env) {
this.render(stage, env);
}
} }
class Async extends BaseComponent { class Async extends BaseComponent {
@ -4633,6 +4710,12 @@ define('sequence/components/AgentCap',[
}); });
return maxEnd + env.theme.actionMargin; return maxEnd + env.theme.actionMargin;
} }
renderHidden({agentIDs}, env) {
agentIDs.forEach((id) => {
env.drawAgentLine(id, env.topY, !this.begin);
});
}
} }
BaseComponent.register('agent begin', new AgentCap(true)); BaseComponent.register('agent begin', new AgentCap(true));
@ -4674,6 +4757,10 @@ define('sequence/components/AgentHighlight',['./BaseComponent'], (BaseComponent)
}); });
return env.primaryY + env.theme.actionMargin; return env.primaryY + env.theme.actionMargin;
} }
renderHidden(stage, env) {
this.render(stage, env);
}
} }
BaseComponent.register('agent highlight', new AgentHighlight()); BaseComponent.register('agent highlight', new AgentHighlight());
@ -5164,6 +5251,10 @@ define('sequence/components/Connect',[
}); });
return env.primaryY + env.theme.actionMargin; return env.primaryY + env.theme.actionMargin;
} }
renderHidden(stage, env) {
this.render(stage, env);
}
} }
class ConnectDelayEnd extends Connect { class ConnectDelayEnd extends Connect {
@ -5669,6 +5760,7 @@ define('sequence/Renderer',[
this.knownThemeDefs = new Set(); this.knownThemeDefs = new Set();
this.knownDefs = new Set(); this.knownDefs = new Set();
this.highlights = new Map(); this.highlights = new Map();
this.collapsed = new Set();
this.currentHighlight = -1; this.currentHighlight = -1;
this.buildStaticElements(); this.buildStaticElements();
this.components.forEach((component) => { this.components.forEach((component) => {
@ -5679,7 +5771,6 @@ define('sequence/Renderer',[
_bindMethods() { _bindMethods() {
this.separationStage = this.separationStage.bind(this); this.separationStage = this.separationStage.bind(this);
this.renderStage = this.renderStage.bind(this); this.renderStage = this.renderStage.bind(this);
this.addSeparation = this.addSeparation.bind(this);
this.addThemeDef = this.addThemeDef.bind(this); this.addThemeDef = this.addThemeDef.bind(this);
this.addDef = this.addDef.bind(this); this.addDef = this.addDef.bind(this);
} }
@ -5775,9 +5866,37 @@ define('sequence/Renderer',[
info2.separations.set(agentID1, Math.max(d2, dist)); info2.separations.set(agentID1, Math.max(d2, dist));
} }
checkHidden(stage) {
const component = this.components.get(stage.type);
const env = {
renderer: this,
theme: this.theme,
agentInfos: this.agentInfos,
textSizer: this.sizer,
state: this.state,
components: this.components,
};
const hide = component.shouldHide(stage, env) || {};
const wasHidden = (this.hideNest > 0);
this.hideNest += hide.nest || 0;
const isHidden = (this.hideNest > 0);
if(this.hideNest < 0) {
throw new Error('Unexpected nesting in ' + stage.type);
}
if(wasHidden === isHidden) {
return isHidden;
} else {
return Boolean(hide.self);
}
}
separationStage(stage) { separationStage(stage) {
const agentSpaces = new Map(); const agentSpaces = new Map();
const agentIDs = this.visibleAgentIDs.slice(); const agentIDs = this.visibleAgentIDs.slice();
const seps = [];
const addSpacing = (agentID, {left, right}) => { const addSpacing = (agentID, {left, right}) => {
const current = agentSpaces.get(agentID); const current = agentSpaces.get(agentID);
@ -5785,30 +5904,47 @@ define('sequence/Renderer',[
current.right = Math.max(current.right, right); current.right = Math.max(current.right, right);
}; };
const addSeparation = (agentID1, agentID2, dist) => {
seps.push({agentID1, agentID2, dist});
};
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad; const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad; agentInfo.currentMaxRad = rad;
agentSpaces.set(agentInfo.id, {left: rad, right: rad}); agentSpaces.set(agentInfo.id, {left: rad, right: rad});
}); });
const env = { const env = {
renderer: this,
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
visibleAgentIDs: this.visibleAgentIDs, visibleAgentIDs: this.visibleAgentIDs,
momentaryAgentIDs: agentIDs, momentaryAgentIDs: agentIDs,
textSizer: this.sizer, textSizer: this.sizer,
addSpacing, addSpacing,
addSeparation: this.addSeparation, addSeparation,
state: this.state, state: this.state,
components: this.components, components: this.components,
}; };
const component = this.components.get(stage.type); const component = this.components.get(stage.type);
if(!component) { if(!component) {
throw new Error('Unknown component: ' + stage.type); throw new Error('Unknown component: ' + stage.type);
} }
component.separationPre(stage, env); component.separationPre(stage, env);
component.separation(stage, env); component.separation(stage, env);
if(this.checkHidden(stage)) {
return;
}
array.mergeSets(agentIDs, this.visibleAgentIDs); array.mergeSets(agentIDs, this.visibleAgentIDs);
seps.forEach(({agentID1, agentID2, dist}) => {
this.addSeparation(agentID1, agentID2, dist);
});
agentIDs.forEach((agentIDR) => { agentIDs.forEach((agentIDR) => {
const infoR = this.agentInfos.get(agentIDR); const infoR = this.agentInfos.get(agentIDR);
const sepR = agentSpaces.get(agentIDR); const sepR = agentSpaces.get(agentIDR);
@ -5885,6 +6021,13 @@ define('sequence/Renderer',[
list.push(o); list.push(o);
} }
forwardEvent(source, sourceEvent, forwardEvent, forwardArgs) {
source.addEventListener(
sourceEvent,
this.trigger.bind(this, forwardEvent, forwardArgs)
);
}
renderStage(stage) { renderStage(stage) {
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad; const rad = agentInfo.currentRad;
@ -5892,6 +6035,7 @@ define('sequence/Renderer',[
}); });
const envPre = { const envPre = {
renderer: this,
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
textSizer: this.sizer, textSizer: this.sizer,
@ -5905,10 +6049,6 @@ define('sequence/Renderer',[
const topY = this.checkAgentRange(agentIDs, asynchronousY); const topY = this.checkAgentRange(agentIDs, asynchronousY);
const eventOut = () => {
this.trigger('mouseout');
};
const makeRegion = ({ const makeRegion = ({
stageOverride = null, stageOverride = null,
unmasked = false, unmasked = false,
@ -5917,18 +6057,16 @@ define('sequence/Renderer',[
const targetStage = (stageOverride || stage); const targetStage = (stageOverride || stage);
this.addHighlightObject(targetStage.ln, o); this.addHighlightObject(targetStage.ln, o);
o.setAttribute('class', 'region'); o.setAttribute('class', 'region');
o.addEventListener('mouseenter', () => { this.forwardEvent(o, 'mouseenter', 'mouseover', [targetStage]);
this.trigger('mouseover', [targetStage]); this.forwardEvent(o, 'mouseleave', 'mouseout', [targetStage]);
}); this.forwardEvent(o, 'click', 'click', [targetStage]);
o.addEventListener('mouseleave', eventOut); this.forwardEvent(o, 'dblclick', 'dblclick', [targetStage]);
o.addEventListener('click', () => {
this.trigger('click', [targetStage]);
});
(unmasked ? this.unmaskedShapes : this.shapes).appendChild(o); (unmasked ? this.unmaskedShapes : this.shapes).appendChild(o);
return o; return o;
}; };
const env = { const env = {
renderer: this,
topY, topY,
primaryY: topY + topShift, primaryY: topY + topShift,
fillLayer: this.backgroundFills, fillLayer: this.backgroundFills,
@ -5950,9 +6088,15 @@ define('sequence/Renderer',[
components: this.components, components: this.components,
}; };
const bottomY = Math.max(topY, component.render(stage, env) || 0); let bottomY = topY;
this.markAgentRange(agentIDs, bottomY); if(this.checkHidden(stage)) {
env.primaryY = topY;
component.renderHidden(stage, env);
} else {
bottomY = Math.max(bottomY, component.render(stage, env) || 0);
}
this.markAgentRange(agentIDs, bottomY);
this.currentY = bottomY; this.currentY = bottomY;
} }
@ -6057,6 +6201,7 @@ define('sequence/Renderer',[
component.resetState(this.state); component.resetState(this.state);
}); });
this.currentY = 0; this.currentY = 0;
this.hideNest = 0;
} }
_reset(theme) { _reset(theme) {
@ -6103,6 +6248,49 @@ define('sequence/Renderer',[
this.currentHighlight = line; this.currentHighlight = line;
} }
isCollapsed(line) {
return this.collapsed.has(line);
}
setCollapseAll(collapsed) {
if(collapsed) {
throw new Error('Cannot collapse all');
} else {
if(this.collapsed.size === 0) {
return false;
}
this.collapsed.clear();
}
return true;
}
_setCollapsed(line, collapsed) {
if(typeof line !== 'number') {
return false;
}
if(collapsed === this.isCollapsed(line)) {
return false;
}
if(collapsed) {
this.collapsed.add(line);
} else {
this.collapsed.delete(line);
}
return true;
}
setCollapsed(line, collapsed = true) {
if(line === null) {
return this.setCollapseAll(collapsed);
}
if(Array.isArray(line)) {
return line
.map((ln) => this._setCollapsed(ln, collapsed))
.some((changed) => changed);
}
return this._setCollapsed(line, collapsed);
}
render(sequence) { render(sequence) {
const prevHighlight = this.currentHighlight; const prevHighlight = this.currentHighlight;
const oldTheme = this.theme; const oldTheme = this.theme;
@ -7143,6 +7331,13 @@ define('sequence/themes/Basic',[
'rx': 2, 'rx': 2,
'ry': 2, 'ry': 2,
}), }),
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
}),
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, { sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000', 'stroke': '#000000',
@ -7527,6 +7722,11 @@ define('sequence/themes/Monospace',[
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 2, 'stroke-width': 2,
}), }),
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 2,
}),
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, { sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000', 'stroke': '#000000',
@ -7919,6 +8119,13 @@ define('sequence/themes/Chunky',[
'rx': 5, 'rx': 5,
'ry': 5, 'ry': 5,
}), }),
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 4,
'rx': 5,
'ry': 5,
}),
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, { sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000', 'stroke': '#000000',
@ -8712,6 +8919,7 @@ define('sequence/themes/Sketch',[
bottom: 0, bottom: 0,
}, },
boxRenderer: null, boxRenderer: null,
collapsedBoxRenderer: null,
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: null, sepRenderer: null,
}, },
@ -8921,6 +9129,8 @@ define('sequence/themes/Sketch',[
this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this); this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this);
this.blocks[''].boxRenderer = this.renderBlock.bind(this); this.blocks[''].boxRenderer = this.renderBlock.bind(this);
this.blocks[''].collapsedBoxRenderer =
this.renderCollapsedBlock.bind(this);
this.blocks.ref.section.tag.boxRenderer = this.renderTag; this.blocks.ref.section.tag.boxRenderer = this.renderTag;
this.blocks[''].section.tag.boxRenderer = this.renderTag; this.blocks[''].section.tag.boxRenderer = this.renderTag;
this.blocks[''].sepRenderer = this.renderSeparator.bind(this); this.blocks[''].sepRenderer = this.renderSeparator.bind(this);
@ -9318,6 +9528,10 @@ define('sequence/themes/Sketch',[
return this.renderBox(position, {fill: 'none', thick: true}); return this.renderBox(position, {fill: 'none', thick: true});
} }
renderCollapsedBlock(position) {
return this.renderRefBlock(position);
}
renderTag({x, y, width, height}) { renderTag({x, y, width, height}) {
const x2 = x + width; const x2 = x + width;
const y2 = y + height; const y2 = y + height;
@ -9486,9 +9700,14 @@ define('sequence/SequenceDiagram',[
this.renderer = new Renderer(Object.assign({themes}, options)); this.renderer = new Renderer(Object.assign({themes}, options));
this.exporter = new Exporter(); this.exporter = new Exporter();
this.renderer.addEventForwarding(this); this.renderer.addEventForwarding(this);
this.latestProcessed = null;
this.isInteractive = false;
if(options.container) { if(options.container) {
options.container.appendChild(this.dom()); options.container.appendChild(this.dom());
} }
if(options.interactive) {
this.addInteractivity();
}
if(typeof this.code === 'string') { if(typeof this.code === 'string') {
this.render(); this.render();
} }
@ -9501,6 +9720,7 @@ define('sequence/SequenceDiagram',[
themes: this.renderer.getThemes(), themes: this.renderer.getThemes(),
namespace: null, namespace: null,
components: this.renderer.components, components: this.renderer.components,
interactive: this.isInteractive,
SVGTextBlockClass: this.renderer.SVGTextBlockClass, SVGTextBlockClass: this.renderer.SVGTextBlockClass,
}, options)); }, options));
} }
@ -9523,10 +9743,40 @@ define('sequence/SequenceDiagram',[
this.renderer.addTheme(theme); this.renderer.addTheme(theme);
} }
setHighlight(line = null) { setHighlight(line) {
this.renderer.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() { getThemeNames() {
return this.renderer.getThemeNames(); return this.renderer.getThemeNames();
} }
@ -9593,6 +9843,8 @@ define('sequence/SequenceDiagram',[
processed = this.process(this.code); processed = this.process(this.code);
} }
this.renderer.render(processed); this.renderer.render(processed);
this.latestProcessed = processed;
this.trigger('render', [this]);
} finally { } finally {
if(dom.parentNode !== originalParent) { if(dom.parentNode !== originalParent) {
document.body.removeChild(dom); document.body.removeChild(dom);
@ -9613,6 +9865,17 @@ define('sequence/SequenceDiagram',[
} }
} }
addInteractivity() {
if(this.isInteractive) {
return;
}
this.isInteractive = true;
this.addEventListener('click', (element) => {
this.toggleCollapsed(element.ln);
});
}
extractCodeFromSVG(svg) { extractCodeFromSVG(svg) {
return extractCodeFromSVG(svg); return extractCodeFromSVG(svg);
} }
@ -9622,6 +9885,17 @@ define('sequence/SequenceDiagram',[
} }
} }
function datasetBoolean(value) {
return value !== undefined && value !== 'false';
}
function parseTagOptions(element) {
return {
namespace: element.dataset.sdNamespace || null,
interactive: datasetBoolean(element.dataset.sdInteractive),
};
}
function convert(element, code = null, options = {}) { function convert(element, code = null, options = {}) {
if(element.tagName === 'svg') { if(element.tagName === 'svg') {
return null; return null;
@ -9633,7 +9907,13 @@ define('sequence/SequenceDiagram',[
options = code; options = code;
code = options.code; code = options.code;
} }
const diagram = new SequenceDiagram(code, options);
const tagOptions = parseTagOptions(element);
const diagram = new SequenceDiagram(
code,
Object.assign(tagOptions, options)
);
const newElement = diagram.dom(); const newElement = diagram.dom();
element.parentNode.insertBefore(newElement, element); element.parentNode.insertBefore(newElement, element);
element.parentNode.removeChild(element); element.parentNode.removeChild(element);

File diff suppressed because one or more lines are too long

View File

@ -409,10 +409,17 @@ diagram = new SequenceDiagram();
Creates a new SequenceDiagram object. Options is an object which can contain: Creates a new SequenceDiagram object. Options is an object which can contain:
</p> </p>
<ul> <ul>
<li><code>code</code>: Alternative way of specifying code, instead of using a separate argument.</li> <li><code>code</code>: Alternative way of specifying code, instead of using a
<li><code>container</code>: DOM node to append the diagram to (defaults to null).</li> separate argument.</li>
<li><code>themes</code>: List of themes to make available to the diagram (defaults to globally registered themes).</li> <li><code>container</code>: DOM node to append the diagram to (defaults to
<li><code>namespace</code>: Each diagram on a page must have a unique namespace. By default a unique namespace is generated, but if you want something specific, enter it here.</li> null).</li>
<li><code>themes</code>: List of themes to make available to the diagram
(defaults to globally registered themes).</li>
<li><code>namespace</code>: Each diagram on a page must have a unique namespace.
By default a unique namespace is generated, but if you want something specific,
enter it here.</li>
<li><code>interactive</code>: If <code>true</code>, will automatically call
<code>addInteractivity</code> when constructing the diagram.</li>
</ul> </ul>
<h3 id="API_clone">.clone</h3> <h3 id="API_clone">.clone</h3>
@ -503,6 +510,58 @@ themes = diagram.getThemes();
Returns a list of themes which are available to this diagram. Returns a list of themes which are available to this diagram.
</p> </p>
<h3 id="API_addInteractivity">.addInteractivity</h3>
<div class="right">
<pre class="sequence-diagram" data-lang="sequence" data-sd-interactive>
begin A, B
if bored
A -> +B
-B --> A
end
if still bored
A -> +B
-B --> A
end
</pre>
</div>
<pre data-lang="javascript">
diagram.addInteractivity();
</pre>
<p>
Makes the rendered diagram interactive. Currently this means adding a click
listener to any groups which causes them to collapse / expand. Try clicking on
the example to the right.
</p>
<p>The example here has CSS styling applied:</p>
<pre data-lang="css">
.region.collapsed,
.region.expanded {
cursor: pointer;
user-select: none;
}
.region.collapsed:hover .outline,
.region.expanded:hover .outline {
fill: rgba(255, 128, 0, 0.5);
}
</pre>
<p>It is also possible to enable interactivity using a HTML attribute:</p>
<pre data-lang="text/html">
&lt;pre class="sequence-diagram" data-sd-interactive&gt;
A -&gt; +B
if something
-B --&gt; A
end
&lt;/pre&gt;
</pre>
<h3 id="API_getSVGSynchronous">.getSVGSynchronous</h3> <h3 id="API_getSVGSynchronous">.getSVGSynchronous</h3>
<pre data-lang="javascript"> <pre data-lang="javascript">
@ -608,6 +667,87 @@ example:
} }
</pre> </pre>
<h3 id="API_setCollapsed">.setCollapsed</h3>
<pre data-lang="javascript">
diagram.setCollapsed(line, collapsed, options);
diagram.setCollapsed(line, collapsed);
</pre>
<p>
Marks the given line as collapsed or non-collapsed. If an element defined at
that line can be collapsed, it will be modified during the next render. Returns
true if a change occurred, or false if the line already had the requested state.
</p>
<p><code>line</code> can also be an array of lines.</p>
<p>
By default, calling this method will trigger an automatic render (unless called
as a no-op). This can be disabled by passing <code>{render: false}</code> in the
options argument.
</p>
<h3 id="API_isCollapsed">.isCollapsed</h3>
<pre data-lang="javascript">
collapsed = diagram.isCollapsed(line);
</pre>
<p>
Returns true if the given line is marked as collapsed, regardless of whether
that line being collapsed has a meaningful impact on the rendered document.
</p>
<h3 id="API_collapse">.collapse</h3>
<pre data-lang="javascript">
diagram.collapse(line, options);
diagram.collapse(line);
</pre>
<p>
Shorthand for <code>.setCollapsed(line, true, options)</code>.
</p>
<h3 id="API_expand">.expand</h3>
<pre data-lang="javascript">
diagram.expand(line, options);
diagram.expand(line);
</pre>
<p>
Shorthand for <code>.setCollapsed(line, false, options)</code>.
</p>
<h3 id="API_toggleCollapsed">.toggleCollapsed</h3>
<pre data-lang="javascript">
diagram.toggleCollapsed(line, options);
diagram.toggleCollapsed(line);
</pre>
<p>
Toggles the given line&rsquo;s collapsed status by calling
<code>.setCollapsed</code>.
</p>
<h3 id="API_expandAll">.expandAll</h3>
<pre data-lang="javascript">
diagram.expandAll(options);
diagram.expandAll();
</pre>
<p>
Marks all lines as non-collapsed. Returns true if a change occurred, or false
if all lines were already non-collapsed.
</p>
<p>
By default, calling this method will trigger an automatic render (unless called
as a no-op). This can be disabled by passing <code>{render: false}</code> in the
options argument.
</p>
<h3 id="API_addEventListener">.addEventListener</h3> <h3 id="API_addEventListener">.addEventListener</h3>
<pre data-lang="javascript"> <pre data-lang="javascript">
@ -623,9 +763,13 @@ Registers an event listener. The available events are:</p>
diagram.</li> diagram.</li>
<li><code>click</code>: called when the user clicks on a region of the <li><code>click</code>: called when the user clicks on a region of the
diagram.</li> diagram.</li>
<li><code>dblclick</code>: called when the user double-clicks on a region of the
diagram.</li>
<li><code>render</code>: called when the diagram finishes rendering. Receives
the sequence diagram object as an argument.</li>
</ul> </ul>
<p><code>mouseover</code> and <code>click</code> are invoked with a single <p>All mouse events are invoked with a single parameter: the element. This
parameter: the element. This object contains:</p> object contains:</p>
<ul> <ul>
<li><code>ln</code>: the line number of the source code which defined the <li><code>ln</code>: the line number of the source code which defined the
element.</li> element.</li>

View File

@ -216,6 +216,11 @@ define(['require'], (require) => {
registerListeners() { registerListeners() {
this.code.addEventListener('input', () => this.update(false)); this.code.addEventListener('input', () => this.update(false));
this.diagram.addEventListener('render', () => {
this.updateMinSize(this.diagram.getSize());
this.pngDirty = true;
});
this.diagram.addEventListener('mouseover', (element) => { this.diagram.addEventListener('mouseover', (element) => {
if(this.marker) { if(this.marker) {
this.marker.clear(); this.marker.clear();
@ -256,6 +261,10 @@ define(['require'], (require) => {
} }
}); });
this.diagram.addEventListener('dblclick', (element) => {
this.diagram.toggleCollapsed(element.ln);
});
this.container.addEventListener('dragover', (event) => { this.container.addEventListener('dragover', (event) => {
event.preventDefault(); event.preventDefault();
if(hasDroppedFile(event, 'image/svg+xml')) { if(hasDroppedFile(event, 'image/svg+xml')) {
@ -423,10 +432,8 @@ define(['require'], (require) => {
redraw(sequence) { redraw(sequence) {
clearTimeout(this.debounced); clearTimeout(this.debounced);
this.debounced = null; this.debounced = null;
this.pngDirty = true;
this.renderedSeq = sequence; this.renderedSeq = sequence;
this.diagram.render(sequence); this.diagram.render(sequence);
this.updateMinSize(this.diagram.getSize());
} }
saveCode(src) { saveCode(src) {
@ -481,6 +488,7 @@ define(['require'], (require) => {
} else { } else {
this.code.value = code; this.code.value = code;
} }
this.diagram.expandAll({render: false});
this.update(true); this.update(true);
this.diagram.setHighlight(null); this.diagram.setHighlight(null);
} }

View File

@ -467,6 +467,7 @@ define(['core/ArrayUtilities'], (array) => {
blockType, blockType,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
label: this.textFormatter(label), label: this.textFormatter(label),
canHide: true,
left: leftGAgent.id, left: leftGAgent.id,
right: rightGAgent.id, right: rightGAgent.id,
ln, ln,
@ -612,6 +613,7 @@ define(['core/ArrayUtilities'], (array) => {
type: 'block begin', type: 'block begin',
blockType, blockType,
tag: this.textFormatter(tag), tag: this.textFormatter(tag),
canHide: false,
label: this.textFormatter(label), label: this.textFormatter(label),
left: details.leftGAgent.id, left: details.leftGAgent.id,
right: details.rightGAgent.id, right: details.rightGAgent.id,

View File

@ -190,6 +190,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
blockBegin: (blockType, { blockBegin: (blockType, {
tag = any(), tag = any(),
label = any(), label = any(),
canHide = any(),
left = any(), left = any(),
right = any(), right = any(),
ln = any(), ln = any(),
@ -199,6 +200,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
blockType, blockType,
tag, tag,
label, label,
canHide,
left, left,
right, right,
ln, ln,
@ -1298,7 +1300,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
GENERATED.blockBegin( GENERATED.blockBegin(
'if', 'if',
{tag: 'if!', label: 'abc!', ln: 10} {tag: 'if!', label: 'abc!', canHide: true, ln: 10}
), ),
any(), any(),
any(), any(),
@ -1492,6 +1494,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
GENERATED.blockBegin('ref', { GENERATED.blockBegin('ref', {
tag: 'ref!', tag: 'ref!',
label: 'Foo!', label: 'Foo!',
canHide: false,
left: bounds.left, left: bounds.left,
right: bounds.right, right: bounds.right,
}), }),

View File

@ -89,6 +89,7 @@ define([
this.knownThemeDefs = new Set(); this.knownThemeDefs = new Set();
this.knownDefs = new Set(); this.knownDefs = new Set();
this.highlights = new Map(); this.highlights = new Map();
this.collapsed = new Set();
this.currentHighlight = -1; this.currentHighlight = -1;
this.buildStaticElements(); this.buildStaticElements();
this.components.forEach((component) => { this.components.forEach((component) => {
@ -99,7 +100,6 @@ define([
_bindMethods() { _bindMethods() {
this.separationStage = this.separationStage.bind(this); this.separationStage = this.separationStage.bind(this);
this.renderStage = this.renderStage.bind(this); this.renderStage = this.renderStage.bind(this);
this.addSeparation = this.addSeparation.bind(this);
this.addThemeDef = this.addThemeDef.bind(this); this.addThemeDef = this.addThemeDef.bind(this);
this.addDef = this.addDef.bind(this); this.addDef = this.addDef.bind(this);
} }
@ -195,9 +195,37 @@ define([
info2.separations.set(agentID1, Math.max(d2, dist)); info2.separations.set(agentID1, Math.max(d2, dist));
} }
checkHidden(stage) {
const component = this.components.get(stage.type);
const env = {
renderer: this,
theme: this.theme,
agentInfos: this.agentInfos,
textSizer: this.sizer,
state: this.state,
components: this.components,
};
const hide = component.shouldHide(stage, env) || {};
const wasHidden = (this.hideNest > 0);
this.hideNest += hide.nest || 0;
const isHidden = (this.hideNest > 0);
if(this.hideNest < 0) {
throw new Error('Unexpected nesting in ' + stage.type);
}
if(wasHidden === isHidden) {
return isHidden;
} else {
return Boolean(hide.self);
}
}
separationStage(stage) { separationStage(stage) {
const agentSpaces = new Map(); const agentSpaces = new Map();
const agentIDs = this.visibleAgentIDs.slice(); const agentIDs = this.visibleAgentIDs.slice();
const seps = [];
const addSpacing = (agentID, {left, right}) => { const addSpacing = (agentID, {left, right}) => {
const current = agentSpaces.get(agentID); const current = agentSpaces.get(agentID);
@ -205,30 +233,47 @@ define([
current.right = Math.max(current.right, right); current.right = Math.max(current.right, right);
}; };
const addSeparation = (agentID1, agentID2, dist) => {
seps.push({agentID1, agentID2, dist});
};
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad; const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad; agentInfo.currentMaxRad = rad;
agentSpaces.set(agentInfo.id, {left: rad, right: rad}); agentSpaces.set(agentInfo.id, {left: rad, right: rad});
}); });
const env = { const env = {
renderer: this,
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
visibleAgentIDs: this.visibleAgentIDs, visibleAgentIDs: this.visibleAgentIDs,
momentaryAgentIDs: agentIDs, momentaryAgentIDs: agentIDs,
textSizer: this.sizer, textSizer: this.sizer,
addSpacing, addSpacing,
addSeparation: this.addSeparation, addSeparation,
state: this.state, state: this.state,
components: this.components, components: this.components,
}; };
const component = this.components.get(stage.type); const component = this.components.get(stage.type);
if(!component) { if(!component) {
throw new Error('Unknown component: ' + stage.type); throw new Error('Unknown component: ' + stage.type);
} }
component.separationPre(stage, env); component.separationPre(stage, env);
component.separation(stage, env); component.separation(stage, env);
if(this.checkHidden(stage)) {
return;
}
array.mergeSets(agentIDs, this.visibleAgentIDs); array.mergeSets(agentIDs, this.visibleAgentIDs);
seps.forEach(({agentID1, agentID2, dist}) => {
this.addSeparation(agentID1, agentID2, dist);
});
agentIDs.forEach((agentIDR) => { agentIDs.forEach((agentIDR) => {
const infoR = this.agentInfos.get(agentIDR); const infoR = this.agentInfos.get(agentIDR);
const sepR = agentSpaces.get(agentIDR); const sepR = agentSpaces.get(agentIDR);
@ -305,6 +350,13 @@ define([
list.push(o); list.push(o);
} }
forwardEvent(source, sourceEvent, forwardEvent, forwardArgs) {
source.addEventListener(
sourceEvent,
this.trigger.bind(this, forwardEvent, forwardArgs)
);
}
renderStage(stage) { renderStage(stage) {
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad; const rad = agentInfo.currentRad;
@ -312,6 +364,7 @@ define([
}); });
const envPre = { const envPre = {
renderer: this,
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
textSizer: this.sizer, textSizer: this.sizer,
@ -325,10 +378,6 @@ define([
const topY = this.checkAgentRange(agentIDs, asynchronousY); const topY = this.checkAgentRange(agentIDs, asynchronousY);
const eventOut = () => {
this.trigger('mouseout');
};
const makeRegion = ({ const makeRegion = ({
stageOverride = null, stageOverride = null,
unmasked = false, unmasked = false,
@ -337,18 +386,16 @@ define([
const targetStage = (stageOverride || stage); const targetStage = (stageOverride || stage);
this.addHighlightObject(targetStage.ln, o); this.addHighlightObject(targetStage.ln, o);
o.setAttribute('class', 'region'); o.setAttribute('class', 'region');
o.addEventListener('mouseenter', () => { this.forwardEvent(o, 'mouseenter', 'mouseover', [targetStage]);
this.trigger('mouseover', [targetStage]); this.forwardEvent(o, 'mouseleave', 'mouseout', [targetStage]);
}); this.forwardEvent(o, 'click', 'click', [targetStage]);
o.addEventListener('mouseleave', eventOut); this.forwardEvent(o, 'dblclick', 'dblclick', [targetStage]);
o.addEventListener('click', () => {
this.trigger('click', [targetStage]);
});
(unmasked ? this.unmaskedShapes : this.shapes).appendChild(o); (unmasked ? this.unmaskedShapes : this.shapes).appendChild(o);
return o; return o;
}; };
const env = { const env = {
renderer: this,
topY, topY,
primaryY: topY + topShift, primaryY: topY + topShift,
fillLayer: this.backgroundFills, fillLayer: this.backgroundFills,
@ -370,9 +417,15 @@ define([
components: this.components, components: this.components,
}; };
const bottomY = Math.max(topY, component.render(stage, env) || 0); let bottomY = topY;
this.markAgentRange(agentIDs, bottomY); if(this.checkHidden(stage)) {
env.primaryY = topY;
component.renderHidden(stage, env);
} else {
bottomY = Math.max(bottomY, component.render(stage, env) || 0);
}
this.markAgentRange(agentIDs, bottomY);
this.currentY = bottomY; this.currentY = bottomY;
} }
@ -477,6 +530,7 @@ define([
component.resetState(this.state); component.resetState(this.state);
}); });
this.currentY = 0; this.currentY = 0;
this.hideNest = 0;
} }
_reset(theme) { _reset(theme) {
@ -523,6 +577,49 @@ define([
this.currentHighlight = line; this.currentHighlight = line;
} }
isCollapsed(line) {
return this.collapsed.has(line);
}
setCollapseAll(collapsed) {
if(collapsed) {
throw new Error('Cannot collapse all');
} else {
if(this.collapsed.size === 0) {
return false;
}
this.collapsed.clear();
}
return true;
}
_setCollapsed(line, collapsed) {
if(typeof line !== 'number') {
return false;
}
if(collapsed === this.isCollapsed(line)) {
return false;
}
if(collapsed) {
this.collapsed.add(line);
} else {
this.collapsed.delete(line);
}
return true;
}
setCollapsed(line, collapsed = true) {
if(line === null) {
return this.setCollapseAll(collapsed);
}
if(Array.isArray(line)) {
return line
.map((ln) => this._setCollapsed(ln, collapsed))
.some((changed) => changed);
}
return this._setCollapsed(line, collapsed);
}
render(sequence) { render(sequence) {
const prevHighlight = this.currentHighlight; const prevHighlight = this.currentHighlight;
const oldTheme = this.theme; const oldTheme = this.theme;

View File

@ -77,9 +77,14 @@ define([
this.renderer = new Renderer(Object.assign({themes}, options)); this.renderer = new Renderer(Object.assign({themes}, options));
this.exporter = new Exporter(); this.exporter = new Exporter();
this.renderer.addEventForwarding(this); this.renderer.addEventForwarding(this);
this.latestProcessed = null;
this.isInteractive = false;
if(options.container) { if(options.container) {
options.container.appendChild(this.dom()); options.container.appendChild(this.dom());
} }
if(options.interactive) {
this.addInteractivity();
}
if(typeof this.code === 'string') { if(typeof this.code === 'string') {
this.render(); this.render();
} }
@ -92,6 +97,7 @@ define([
themes: this.renderer.getThemes(), themes: this.renderer.getThemes(),
namespace: null, namespace: null,
components: this.renderer.components, components: this.renderer.components,
interactive: this.isInteractive,
SVGTextBlockClass: this.renderer.SVGTextBlockClass, SVGTextBlockClass: this.renderer.SVGTextBlockClass,
}, options)); }, options));
} }
@ -114,10 +120,40 @@ define([
this.renderer.addTheme(theme); this.renderer.addTheme(theme);
} }
setHighlight(line = null) { setHighlight(line) {
this.renderer.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() { getThemeNames() {
return this.renderer.getThemeNames(); return this.renderer.getThemeNames();
} }
@ -184,6 +220,8 @@ define([
processed = this.process(this.code); processed = this.process(this.code);
} }
this.renderer.render(processed); this.renderer.render(processed);
this.latestProcessed = processed;
this.trigger('render', [this]);
} finally { } finally {
if(dom.parentNode !== originalParent) { if(dom.parentNode !== originalParent) {
document.body.removeChild(dom); document.body.removeChild(dom);
@ -204,6 +242,17 @@ define([
} }
} }
addInteractivity() {
if(this.isInteractive) {
return;
}
this.isInteractive = true;
this.addEventListener('click', (element) => {
this.toggleCollapsed(element.ln);
});
}
extractCodeFromSVG(svg) { extractCodeFromSVG(svg) {
return extractCodeFromSVG(svg); return extractCodeFromSVG(svg);
} }
@ -213,6 +262,17 @@ define([
} }
} }
function datasetBoolean(value) {
return value !== undefined && value !== 'false';
}
function parseTagOptions(element) {
return {
namespace: element.dataset.sdNamespace || null,
interactive: datasetBoolean(element.dataset.sdInteractive),
};
}
function convert(element, code = null, options = {}) { function convert(element, code = null, options = {}) {
if(element.tagName === 'svg') { if(element.tagName === 'svg') {
return null; return null;
@ -224,7 +284,13 @@ define([
options = code; options = code;
code = options.code; code = options.code;
} }
const diagram = new SequenceDiagram(code, options);
const tagOptions = parseTagOptions(element);
const diagram = new SequenceDiagram(
code,
Object.assign(tagOptions, options)
);
const newElement = diagram.dom(); const newElement = diagram.dom();
element.parentNode.insertBefore(newElement, element); element.parentNode.insertBefore(newElement, element);
element.parentNode.removeChild(element); element.parentNode.removeChild(element);

View File

@ -138,4 +138,19 @@ defineDescribe('SequenceDiagram', [
'<polygon points="46 31 51 26 46 21"' '<polygon points="46 31 51 26 46 21"'
); );
}); });
it('renders collapsed blocks', () => {
diagram.set('if\nA -> B\nend');
diagram.setCollapsed(0, true);
const content = getSimplifiedContent(diagram);
expect(content).toContain('<svg viewBox="-5 -5 60 37">');
expect(content).toContain('<line x1="20" y1="5" x2="20" y2="27"');
expect(content).toContain('<line x1="30" y1="5" x2="30" y2="27"');
expect(content).toContain('<rect x="10" y="0" width="30" height="7"');
expect(content).toContain('<g class="region collapsed"');
});
}); });

View File

@ -15,10 +15,31 @@ defineDescribe('SequenceDiagram Visuals', [
const IMAGE_BASE_PATH = 'scripts/sequence/test-images/'; const IMAGE_BASE_PATH = 'scripts/sequence/test-images/';
const COLLAPSE_REGEX = new RegExp(/# collapse/g);
function findCollapseLines(code) {
const results = [];
let p = 0;
let ln = -1;
while(true) {
const match = COLLAPSE_REGEX.exec(code);
if(!match) {
break;
}
while(p !== -1 && p <= match.index) {
p = code.indexOf('\n', p) + 1;
++ ln;
}
results.push(ln);
}
return results;
}
function loadAndRenderSVG(svg, size = {resolution: RESOLUTION}) { function loadAndRenderSVG(svg, size = {resolution: RESOLUTION}) {
const code = SequenceDiagram.extractCodeFromSVG(svg); const code = SequenceDiagram.extractCodeFromSVG(svg);
const diagram = new SequenceDiagram(code); const diagram = new SequenceDiagram(code);
diagram.collapse(findCollapseLines(code));
return Promise.all([ return Promise.all([
diagram.getCanvas(size).then(ImageRegion.fromCanvas), diagram.getCanvas(size).then(ImageRegion.fromCanvas),

View File

@ -332,6 +332,12 @@ define([
}); });
return maxEnd + env.theme.actionMargin; return maxEnd + env.theme.actionMargin;
} }
renderHidden({agentIDs}, env) {
agentIDs.forEach((id) => {
env.drawAgentLine(id, env.topY, !this.begin);
});
}
} }
BaseComponent.register('agent begin', new AgentCap(true)); BaseComponent.register('agent begin', new AgentCap(true));

View File

@ -31,6 +31,10 @@ define(['./BaseComponent'], (BaseComponent) => {
}); });
return env.primaryY + env.theme.actionMargin; return env.primaryY + env.theme.actionMargin;
} }
renderHidden(stage, env) {
this.render(stage, env);
}
} }
BaseComponent.register('agent highlight', new AgentHighlight()); BaseComponent.register('agent highlight', new AgentHighlight());

View File

@ -10,6 +10,7 @@ define(() => {
} }
separationPre(/*stage, { separationPre(/*stage, {
renderer,
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
@ -23,6 +24,7 @@ define(() => {
} }
separation(/*stage, { separation(/*stage, {
renderer,
theme, theme,
agentInfos, agentInfos,
visibleAgentIDs, visibleAgentIDs,
@ -36,6 +38,7 @@ define(() => {
} }
renderPre(/*stage, { renderPre(/*stage, {
renderer,
theme, theme,
agentInfos, agentInfos,
textSizer, textSizer,
@ -46,6 +49,7 @@ define(() => {
} }
render(/*stage, { render(/*stage, {
renderer,
topY, topY,
primaryY, primaryY,
fillLayer, fillLayer,
@ -61,6 +65,22 @@ define(() => {
}*/) { }*/) {
// return bottom Y coordinate // return bottom Y coordinate
} }
renderHidden(/*stage, {
(same args as render, with primaryY = topY)
}*/) {
}
shouldHide(/*stage, {
renderer,
theme,
agentInfos,
textSizer,
state,
components,
}*/) {
// return {self, nest}
}
} }
BaseComponent.cleanRenderPreResult = ({ BaseComponent.cleanRenderPreResult = ({

View File

@ -87,6 +87,12 @@ define([
'x2': agentInfoR.x, 'x2': agentInfoR.x,
'y2': y, 'y2': y,
})); }));
} else if(blockInfo.canHide) {
clickable.setAttribute(
'class',
clickable.getAttribute('class') +
(blockInfo.hide ? ' collapsed' : ' expanded')
);
} }
return y + labelHeight + config.section.padding.top; return y + labelHeight + config.section.padding.top;
@ -103,8 +109,11 @@ define([
} }
storeBlockInfo(stage, env) { storeBlockInfo(stage, env) {
const canHide = stage.canHide;
const blockInfo = { const blockInfo = {
type: stage.blockType, type: stage.blockType,
canHide,
hide: canHide && env.renderer.isCollapsed(stage.ln),
hold: null, hold: null,
startY: null, startY: null,
}; };
@ -142,6 +151,14 @@ define([
return super.render(stage, env, true); return super.render(stage, env, true);
} }
shouldHide({left}, env) {
const blockInfo = env.state.blocks.get(left);
return {
self: false,
nest: blockInfo.hide ? 1 : 0,
};
}
} }
class BlockEnd extends BaseComponent { class BlockEnd extends BaseComponent {
@ -165,7 +182,12 @@ define([
const agentInfoL = env.agentInfos.get(left); const agentInfoL = env.agentInfos.get(left);
const agentInfoR = env.agentInfos.get(right); const agentInfoR = env.agentInfos.get(right);
let shapes = config.boxRenderer({ let renderFn = config.boxRenderer;
if(blockInfo.hide) {
renderFn = config.collapsedBoxRenderer || renderFn;
}
let shapes = renderFn({
x: agentInfoL.x, x: agentInfoL.x,
y: blockInfo.startY, y: blockInfo.startY,
width: agentInfoR.x - agentInfoL.x, width: agentInfoR.x - agentInfoL.x,
@ -185,6 +207,14 @@ define([
return env.primaryY + config.margin.bottom + env.theme.actionMargin; return env.primaryY + config.margin.bottom + env.theme.actionMargin;
} }
shouldHide({left}, env) {
const blockInfo = env.state.blocks.get(left);
return {
self: false,
nest: blockInfo.hide ? -1 : 0,
};
}
} }
BaseComponent.register('block begin', new BlockBegin()); BaseComponent.register('block begin', new BlockBegin());

View File

@ -481,6 +481,10 @@ define([
}); });
return env.primaryY + env.theme.actionMargin; return env.primaryY + env.theme.actionMargin;
} }
renderHidden(stage, env) {
this.render(stage, env);
}
} }
class ConnectDelayEnd extends Connect { class ConnectDelayEnd extends Connect {

View File

@ -13,6 +13,10 @@ define(['./BaseComponent'], (BaseComponent) => {
render({name}, {topY, state}) { render({name}, {topY, state}) {
state.marks.set(name, topY); state.marks.set(name, topY);
} }
renderHidden(stage, env) {
this.render(stage, env);
}
} }
class Async extends BaseComponent { class Async extends BaseComponent {

View File

@ -71,6 +71,27 @@ define([
env.makeRegion = originalMakeRegion; env.makeRegion = originalMakeRegion;
return bottomY; return bottomY;
} }
renderHidden(stage, env) {
stage.stages.forEach((subStage) => {
const component = env.components.get(subStage.type);
component.renderHidden(subStage, env);
});
}
shouldHide(stage, env) {
const result = {
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;
}
} }
BaseComponent.register('parallel', new Parallel()); BaseComponent.register('parallel', new Parallel());

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -5,6 +5,7 @@ define([], [
'DividerMasking.svg', 'DividerMasking.svg',
'Asynchronous.svg', 'Asynchronous.svg',
'Block.svg', 'Block.svg',
'CollapsedBlocks.svg',
'Reference.svg', 'Reference.svg',
'ReferenceLayering.svg', 'ReferenceLayering.svg',
'Markdown.svg', 'Markdown.svg',

View File

@ -258,6 +258,13 @@ define([
'rx': 2, 'rx': 2,
'ry': 2, 'ry': 2,
}), }),
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
}),
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, { sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000', 'stroke': '#000000',

View File

@ -269,6 +269,13 @@ define([
'rx': 5, 'rx': 5,
'ry': 5, 'ry': 5,
}), }),
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 4,
'rx': 5,
'ry': 5,
}),
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, { sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000', 'stroke': '#000000',

View File

@ -261,6 +261,11 @@ define([
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 2, 'stroke-width': 2,
}), }),
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 2,
}),
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, { sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000', 'stroke': '#000000',

View File

@ -230,6 +230,7 @@ define([
bottom: 0, bottom: 0,
}, },
boxRenderer: null, boxRenderer: null,
collapsedBoxRenderer: null,
section: SHARED_BLOCK_SECTION, section: SHARED_BLOCK_SECTION,
sepRenderer: null, sepRenderer: null,
}, },
@ -439,6 +440,8 @@ define([
this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this); this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this);
this.blocks[''].boxRenderer = this.renderBlock.bind(this); this.blocks[''].boxRenderer = this.renderBlock.bind(this);
this.blocks[''].collapsedBoxRenderer =
this.renderCollapsedBlock.bind(this);
this.blocks.ref.section.tag.boxRenderer = this.renderTag; this.blocks.ref.section.tag.boxRenderer = this.renderTag;
this.blocks[''].section.tag.boxRenderer = this.renderTag; this.blocks[''].section.tag.boxRenderer = this.renderTag;
this.blocks[''].sepRenderer = this.renderSeparator.bind(this); this.blocks[''].sepRenderer = this.renderSeparator.bind(this);
@ -836,6 +839,10 @@ define([
return this.renderBox(position, {fill: 'none', thick: true}); return this.renderBox(position, {fill: 'none', thick: true});
} }
renderCollapsedBlock(position) {
return this.renderRefBlock(position);
}
renderTag({x, y, width, height}) { renderTag({x, y, width, height}) {
const x2 = x + width; const x2 = x + width;
const y2 = y + height; const y2 = y + height;

View File

@ -94,6 +94,17 @@ a:hover, a:active {
stroke: rgba(255, 128, 0, 0.5); stroke: rgba(255, 128, 0, 0.5);
} }
.sequence-diagram[data-sd-interactive] .region.collapsed,
.sequence-diagram[data-sd-interactive] .region.expanded {
cursor: pointer;
user-select: none;
}
.sequence-diagram[data-sd-interactive] .region.collapsed:hover .outline,
.sequence-diagram[data-sd-interactive] .region.expanded:hover .outline {
fill: rgba(255, 128, 0, 0.5);
}
pre { pre {
background: #EEEEEE; background: #EEEEEE;
margin: 5px 0; margin: 5px 0;