diff --git a/lib/sequence-diagram.js b/lib/sequence-diagram.js
index dc7b7e0..b156df0 100644
--- a/lib/sequence-diagram.js
+++ b/lib/sequence-diagram.js
@@ -2736,6 +2736,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
blockType,
tag: this.textFormatter(tag),
label: this.textFormatter(label),
+ canHide: true,
left: leftGAgent.id,
right: rightGAgent.id,
ln,
@@ -2881,6 +2882,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
type: 'block begin',
blockType,
tag: this.textFormatter(tag),
+ canHide: false,
label: this.textFormatter(label),
left: details.leftGAgent.id,
right: details.rightGAgent.id,
@@ -3898,6 +3900,7 @@ define('sequence/components/BaseComponent',[],() => {
}
separationPre(/*stage, {
+ renderer,
theme,
agentInfos,
visibleAgentIDs,
@@ -3911,6 +3914,7 @@ define('sequence/components/BaseComponent',[],() => {
}
separation(/*stage, {
+ renderer,
theme,
agentInfos,
visibleAgentIDs,
@@ -3924,6 +3928,7 @@ define('sequence/components/BaseComponent',[],() => {
}
renderPre(/*stage, {
+ renderer,
theme,
agentInfos,
textSizer,
@@ -3934,6 +3939,7 @@ define('sequence/components/BaseComponent',[],() => {
}
render(/*stage, {
+ renderer,
topY,
primaryY,
fillLayer,
@@ -3949,6 +3955,22 @@ define('sequence/components/BaseComponent',[],() => {
}*/) {
// 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 = ({
@@ -4070,6 +4092,12 @@ define('sequence/components/Block',[
'x2': agentInfoR.x,
'y2': y,
}));
+ } else if(blockInfo.canHide) {
+ clickable.setAttribute(
+ 'class',
+ clickable.getAttribute('class') +
+ (blockInfo.hide ? ' collapsed' : ' expanded')
+ );
}
return y + labelHeight + config.section.padding.top;
@@ -4086,8 +4114,11 @@ define('sequence/components/Block',[
}
storeBlockInfo(stage, env) {
+ const canHide = stage.canHide;
const blockInfo = {
type: stage.blockType,
+ canHide,
+ hide: canHide && env.renderer.isCollapsed(stage.ln),
hold: null,
startY: null,
};
@@ -4125,6 +4156,14 @@ define('sequence/components/Block',[
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 {
@@ -4148,7 +4187,12 @@ define('sequence/components/Block',[
const agentInfoL = env.agentInfos.get(left);
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,
y: blockInfo.startY,
width: agentInfoR.x - agentInfoL.x,
@@ -4168,6 +4212,14 @@ define('sequence/components/Block',[
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());
@@ -4254,6 +4306,27 @@ define('sequence/components/Parallel',[
env.makeRegion = originalMakeRegion;
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());
@@ -4276,6 +4349,10 @@ define('sequence/components/Marker',['./BaseComponent'], (BaseComponent) => {
render({name}, {topY, state}) {
state.marks.set(name, topY);
}
+
+ renderHidden(stage, env) {
+ this.render(stage, env);
+ }
}
class Async extends BaseComponent {
@@ -4633,6 +4710,12 @@ define('sequence/components/AgentCap',[
});
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));
@@ -4674,6 +4757,10 @@ define('sequence/components/AgentHighlight',['./BaseComponent'], (BaseComponent)
});
return env.primaryY + env.theme.actionMargin;
}
+
+ renderHidden(stage, env) {
+ this.render(stage, env);
+ }
}
BaseComponent.register('agent highlight', new AgentHighlight());
@@ -5164,6 +5251,10 @@ define('sequence/components/Connect',[
});
return env.primaryY + env.theme.actionMargin;
}
+
+ renderHidden(stage, env) {
+ this.render(stage, env);
+ }
}
class ConnectDelayEnd extends Connect {
@@ -5669,6 +5760,7 @@ define('sequence/Renderer',[
this.knownThemeDefs = new Set();
this.knownDefs = new Set();
this.highlights = new Map();
+ this.collapsed = new Set();
this.currentHighlight = -1;
this.buildStaticElements();
this.components.forEach((component) => {
@@ -5679,7 +5771,6 @@ define('sequence/Renderer',[
_bindMethods() {
this.separationStage = this.separationStage.bind(this);
this.renderStage = this.renderStage.bind(this);
- this.addSeparation = this.addSeparation.bind(this);
this.addThemeDef = this.addThemeDef.bind(this);
this.addDef = this.addDef.bind(this);
}
@@ -5775,9 +5866,37 @@ define('sequence/Renderer',[
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) {
const agentSpaces = new Map();
const agentIDs = this.visibleAgentIDs.slice();
+ const seps = [];
const addSpacing = (agentID, {left, right}) => {
const current = agentSpaces.get(agentID);
@@ -5785,30 +5904,47 @@ define('sequence/Renderer',[
current.right = Math.max(current.right, right);
};
+ const addSeparation = (agentID1, agentID2, dist) => {
+ seps.push({agentID1, agentID2, dist});
+ };
+
this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad;
agentSpaces.set(agentInfo.id, {left: rad, right: rad});
});
+
const env = {
+ renderer: this,
theme: this.theme,
agentInfos: this.agentInfos,
visibleAgentIDs: this.visibleAgentIDs,
momentaryAgentIDs: agentIDs,
textSizer: this.sizer,
addSpacing,
- addSeparation: this.addSeparation,
+ addSeparation,
state: this.state,
components: this.components,
};
+
const component = this.components.get(stage.type);
if(!component) {
throw new Error('Unknown component: ' + stage.type);
}
+
component.separationPre(stage, env);
component.separation(stage, env);
+
+ if(this.checkHidden(stage)) {
+ return;
+ }
+
array.mergeSets(agentIDs, this.visibleAgentIDs);
+ seps.forEach(({agentID1, agentID2, dist}) => {
+ this.addSeparation(agentID1, agentID2, dist);
+ });
+
agentIDs.forEach((agentIDR) => {
const infoR = this.agentInfos.get(agentIDR);
const sepR = agentSpaces.get(agentIDR);
@@ -5885,6 +6021,13 @@ define('sequence/Renderer',[
list.push(o);
}
+ forwardEvent(source, sourceEvent, forwardEvent, forwardArgs) {
+ source.addEventListener(
+ sourceEvent,
+ this.trigger.bind(this, forwardEvent, forwardArgs)
+ );
+ }
+
renderStage(stage) {
this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad;
@@ -5892,6 +6035,7 @@ define('sequence/Renderer',[
});
const envPre = {
+ renderer: this,
theme: this.theme,
agentInfos: this.agentInfos,
textSizer: this.sizer,
@@ -5905,10 +6049,6 @@ define('sequence/Renderer',[
const topY = this.checkAgentRange(agentIDs, asynchronousY);
- const eventOut = () => {
- this.trigger('mouseout');
- };
-
const makeRegion = ({
stageOverride = null,
unmasked = false,
@@ -5917,18 +6057,16 @@ define('sequence/Renderer',[
const targetStage = (stageOverride || stage);
this.addHighlightObject(targetStage.ln, o);
o.setAttribute('class', 'region');
- o.addEventListener('mouseenter', () => {
- this.trigger('mouseover', [targetStage]);
- });
- o.addEventListener('mouseleave', eventOut);
- o.addEventListener('click', () => {
- this.trigger('click', [targetStage]);
- });
+ this.forwardEvent(o, 'mouseenter', 'mouseover', [targetStage]);
+ this.forwardEvent(o, 'mouseleave', 'mouseout', [targetStage]);
+ this.forwardEvent(o, 'click', 'click', [targetStage]);
+ this.forwardEvent(o, 'dblclick', 'dblclick', [targetStage]);
(unmasked ? this.unmaskedShapes : this.shapes).appendChild(o);
return o;
};
const env = {
+ renderer: this,
topY,
primaryY: topY + topShift,
fillLayer: this.backgroundFills,
@@ -5950,9 +6088,15 @@ define('sequence/Renderer',[
components: this.components,
};
- const bottomY = Math.max(topY, component.render(stage, env) || 0);
- this.markAgentRange(agentIDs, bottomY);
+ let bottomY = topY;
+ 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;
}
@@ -6057,6 +6201,7 @@ define('sequence/Renderer',[
component.resetState(this.state);
});
this.currentY = 0;
+ this.hideNest = 0;
}
_reset(theme) {
@@ -6103,6 +6248,49 @@ define('sequence/Renderer',[
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) {
const prevHighlight = this.currentHighlight;
const oldTheme = this.theme;
@@ -7143,6 +7331,13 @@ define('sequence/themes/Basic',[
'rx': 2,
'ry': 2,
}),
+ collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
+ 'fill': '#FFFFFF',
+ 'stroke': '#000000',
+ 'stroke-width': 1.5,
+ 'rx': 2,
+ 'ry': 2,
+ }),
section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000',
@@ -7527,6 +7722,11 @@ define('sequence/themes/Monospace',[
'stroke': '#000000',
'stroke-width': 2,
}),
+ collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
+ 'fill': '#FFFFFF',
+ 'stroke': '#000000',
+ 'stroke-width': 2,
+ }),
section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000',
@@ -7919,6 +8119,13 @@ define('sequence/themes/Chunky',[
'rx': 5,
'ry': 5,
}),
+ collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
+ 'fill': '#FFFFFF',
+ 'stroke': '#000000',
+ 'stroke-width': 4,
+ 'rx': 5,
+ 'ry': 5,
+ }),
section: SHARED_BLOCK_SECTION,
sepRenderer: SVGShapes.renderLine.bind(null, {
'stroke': '#000000',
@@ -8712,6 +8919,7 @@ define('sequence/themes/Sketch',[
bottom: 0,
},
boxRenderer: null,
+ collapsedBoxRenderer: null,
section: SHARED_BLOCK_SECTION,
sepRenderer: null,
},
@@ -8921,6 +9129,8 @@ define('sequence/themes/Sketch',[
this.blocks.ref.boxRenderer = this.renderRefBlock.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[''].section.tag.boxRenderer = this.renderTag;
this.blocks[''].sepRenderer = this.renderSeparator.bind(this);
@@ -9318,6 +9528,10 @@ define('sequence/themes/Sketch',[
return this.renderBox(position, {fill: 'none', thick: true});
}
+ renderCollapsedBlock(position) {
+ return this.renderRefBlock(position);
+ }
+
renderTag({x, y, width, height}) {
const x2 = x + width;
const y2 = y + height;
@@ -9486,9 +9700,14 @@ define('sequence/SequenceDiagram',[
this.renderer = new Renderer(Object.assign({themes}, options));
this.exporter = new Exporter();
this.renderer.addEventForwarding(this);
+ this.latestProcessed = null;
+ this.isInteractive = false;
if(options.container) {
options.container.appendChild(this.dom());
}
+ if(options.interactive) {
+ this.addInteractivity();
+ }
if(typeof this.code === 'string') {
this.render();
}
@@ -9501,6 +9720,7 @@ define('sequence/SequenceDiagram',[
themes: this.renderer.getThemes(),
namespace: null,
components: this.renderer.components,
+ interactive: this.isInteractive,
SVGTextBlockClass: this.renderer.SVGTextBlockClass,
}, options));
}
@@ -9523,10 +9743,40 @@ define('sequence/SequenceDiagram',[
this.renderer.addTheme(theme);
}
- setHighlight(line = null) {
+ setHighlight(line) {
this.renderer.setHighlight(line);
}
+ isCollapsed(line) {
+ return this.renderer.isCollapsed(line);
+ }
+
+ setCollapsed(line, collapsed = true, {render = true} = {}) {
+ if(!this.renderer.setCollapsed(line, collapsed)) {
+ return false;
+ }
+ if(render && this.latestProcessed) {
+ this.render(this.latestProcessed);
+ }
+ return true;
+ }
+
+ collapse(line, options) {
+ return this.setCollapsed(line, true, options);
+ }
+
+ expand(line, options) {
+ return this.setCollapsed(line, false, options);
+ }
+
+ toggleCollapsed(line, options) {
+ return this.setCollapsed(line, !this.isCollapsed(line), options);
+ }
+
+ expandAll(options) {
+ return this.setCollapsed(null, false, options);
+ }
+
getThemeNames() {
return this.renderer.getThemeNames();
}
@@ -9593,6 +9843,8 @@ define('sequence/SequenceDiagram',[
processed = this.process(this.code);
}
this.renderer.render(processed);
+ this.latestProcessed = processed;
+ this.trigger('render', [this]);
} finally {
if(dom.parentNode !== originalParent) {
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) {
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 = {}) {
if(element.tagName === 'svg') {
return null;
@@ -9633,7 +9907,13 @@ define('sequence/SequenceDiagram',[
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();
element.parentNode.insertBefore(newElement, element);
element.parentNode.removeChild(element);
diff --git a/lib/sequence-diagram.min.js b/lib/sequence-diagram.min.js
index 419f6ec..7d14cbb 100644
--- a/lib/sequence-diagram.min.js
+++ b/lib/sequence-diagram.min.js
@@ -1 +1 @@
-!function(){var e,t,n;!function(r){function s(e,t){return x.call(e,t)}function i(e,t){var n,r,s,i,a,o,l,h,d,g,c,u=t&&t.split("/"),p=b.map,f=p&&p["*"]||{};if(e){for(a=(e=e.split("/")).length-1,b.nodeIdCompat&&w.test(e[a])&&(e[a]=e[a].replace(w,"")),"."===e[0].charAt(0)&&u&&(e=u.slice(0,u.length-1).concat(e)),d=0;d
code
: Alternative way of specifying code, instead of using a separate argument.container
: DOM node to append the diagram to (defaults to null).themes
: List of themes to make available to the diagram (defaults to globally registered themes).namespace
: 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.code
: Alternative way of specifying code, instead of using a
+separate argument.container
: DOM node to append the diagram to (defaults to
+null).themes
: List of themes to make available to the diagram
+(defaults to globally registered themes).namespace
: 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.interactive
: If true
, will automatically call
+addInteractivity
when constructing the diagram.+ begin A, B + if bored + A -> +B + -B --> A + end + if still bored + A -> +B + -B --> A + end ++
+diagram.addInteractivity(); ++ +
+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. +
+ +The example here has CSS styling applied:
+ ++.region.collapsed, +.region.expanded { + cursor: pointer; + user-select: none; +} + +.region.collapsed:hover .outline, +.region.expanded:hover .outline { + fill: rgba(255, 128, 0, 0.5); +} ++ +
It is also possible to enable interactivity using a HTML attribute:
+ ++<pre class="sequence-diagram" data-sd-interactive> + A -> +B + if something + -B --> A + end +</pre> ++
@@ -608,6 +667,87 @@ example: }+
+diagram.setCollapsed(line, collapsed, options); +diagram.setCollapsed(line, collapsed); ++ +
+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. +
+line
can also be an array of lines.
+By default, calling this method will trigger an automatic render (unless called
+as a no-op). This can be disabled by passing {render: false}
in the
+options argument.
+
+collapsed = diagram.isCollapsed(line); ++ +
+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. +
+ ++diagram.collapse(line, options); +diagram.collapse(line); ++ +
+Shorthand for .setCollapsed(line, true, options)
.
+
+diagram.expand(line, options); +diagram.expand(line); ++ +
+Shorthand for .setCollapsed(line, false, options)
.
+
+diagram.toggleCollapsed(line, options); +diagram.toggleCollapsed(line); ++ +
+Toggles the given line’s collapsed status by calling
+.setCollapsed
.
+
+diagram.expandAll(options); +diagram.expandAll(); ++ +
+Marks all lines as non-collapsed. Returns true if a change occurred, or false +if all lines were already non-collapsed. +
+
+By default, calling this method will trigger an automatic render (unless called
+as a no-op). This can be disabled by passing {render: false}
in the
+options argument.
+
@@ -623,9 +763,13 @@ Registers an event listener. The available events are: diagram.
click
: called when the user clicks on a region of the
diagram.dblclick
: called when the user double-clicks on a region of the
+diagram.render
: called when the diagram finishes rendering. Receives
+the sequence diagram object as an argument.mouseover
and click
are invoked with a single
-parameter: the element. This object contains:
All mouse events are invoked with a single parameter: the element. This +object contains:
ln
: the line number of the source code which defined the
element.