diff --git a/scripts/interface/Interface.js b/scripts/interface/Interface.js index 069d811..be87d4c 100644 --- a/scripts/interface/Interface.js +++ b/scripts/interface/Interface.js @@ -28,6 +28,18 @@ define([ events.forEach((event) => element.addEventListener(event, fn)); } + function simplifyPreview(code) { + code = code.replace(/\{Agent([0-9]*)\}/g, (match, num) => { + if(num === undefined) { + return 'A'; + } else { + return String.fromCharCode('A'.charCodeAt(0) + Number(num) - 1); + } + }); + code = code.replace(/[{}]/g, ''); + return code; + } + return class Interface { constructor({ parser, @@ -36,6 +48,7 @@ define([ exporter, defaultCode = '', localStorage = '', + library = null, }) { this.parser = parser; this.generator = generator; @@ -43,6 +56,7 @@ define([ this.exporter = exporter; this.defaultCode = defaultCode; this.localStorage = localStorage; + this.library = library; this.minScale = 1.5; this.debounced = null; @@ -133,8 +147,8 @@ define([ code.on('endCompletion', () => { lastKey = 0; }); - code.on('change', (cm) => { - if(cm.state.completionActive) { + code.on('change', (cm, change) => { + if(cm.state.completionActive || change.origin === 'library') { return; } if(lastKey === 13 || lastKey === 8) { @@ -193,12 +207,43 @@ define([ }); } - build(container) { - const codePane = makeNode('div', {'class': 'pane-code'}); - const viewPane = makeNode('div', {'class': 'pane-view'}); - this.errorPane = makeNode('div', {'class': 'pane-error'}); + buildLibrary(container) { + this.library.forEach((lib) => { + const hold = makeNode('div', { + 'class': 'library-item', + }); + const holdInner = makeNode('div', { + 'title': lib.title || lib.code, + }); + hold.appendChild(holdInner); + hold.addEventListener( + 'click', + this.addCodeBlock.bind(this, lib.code) + ); + container.appendChild(hold); + try { + const preview = simplifyPreview(lib.preview || lib.code); + const parsed = this.parser.parse(preview); + const generated = this.generator.generate(parsed); + const rendering = this.renderer.clone(); + holdInner.appendChild(rendering.svg()); + rendering.render(generated); + } catch(e) { + hold.setAttribute('class', 'library-item broken'); + holdInner.appendChild(makeText(lib.code)); + } + }); + } + + buildErrorReport() { + this.errorMsg = makeNode('div', {'class': 'msg-error'}); this.errorText = makeText(); - this.errorPane.appendChild(this.errorText); + this.errorMsg.appendChild(this.errorText); + return this.errorMsg; + } + + buildViewPane() { + const viewPane = makeNode('div', {'class': 'pane-view'}); const viewPaneScroller = makeNode('div', { 'class': 'pane-view-scroller', }); @@ -210,10 +255,31 @@ define([ viewPaneScroller.appendChild(this.viewPaneInner); viewPane.appendChild(this.buildOptionsLinks()); viewPane.appendChild(this.buildOptionsDownloads()); + viewPane.appendChild(this.buildErrorReport()); + return viewPane; + } + + build(container) { + const codePane = makeNode('div', {'class': 'pane-code'}); container.appendChild(codePane); - container.appendChild(this.errorPane); - container.appendChild(viewPane); + + if(this.library !== null) { + const libPane = makeNode('div', {'class': 'pane-library'}); + const libPaneScroller = makeNode('div', { + 'class': 'pane-library-scroller', + }); + const libPaneInner = makeNode('div', { + 'class': 'pane-library-inner', + }); + libPaneScroller.appendChild(libPaneInner); + libPane.appendChild(libPaneScroller); + container.appendChild(libPane); + codePane.setAttribute('class', 'pane-code reduced'); + this.buildLibrary(libPaneInner); + } + + container.appendChild(this.buildViewPane()); this.code = this.buildEditor(codePane); this.viewPaneInner.appendChild(this.renderer.svg()); @@ -222,6 +288,20 @@ define([ this.update(); } + addCodeBlock(block) { + const cur = this.code.getCursor('head'); + const pos = {line: cur.line + ((cur.ch > 0) ? 1 : 0), ch: 0}; + const lines = block.split('\n').length; + this.code.replaceRange( + block + '\n', + pos, + null, + 'library' + ); + this.code.setCursor({line: pos.line + lines, ch: 0}); + this.code.focus(); + } + updateMinSize(width, height) { const style = this.viewPaneInner.style; style.minWidth = Math.ceil(width * this.minScale) + 'px'; @@ -265,12 +345,12 @@ define([ } else { this.errorText.nodeValue = error; } - this.errorPane.setAttribute('class', 'pane-error error'); + this.errorMsg.setAttribute('class', 'msg-error error'); } markOK() { - this.errorText.nodeValue = 'All OK'; - this.errorPane.setAttribute('class', 'pane-error ok'); + this.errorText.nodeValue = ''; + this.errorMsg.setAttribute('class', 'msg-error'); } update(immediate = true) { diff --git a/scripts/main.js b/scripts/main.js index ff3af1d..cc4661d 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -40,6 +40,156 @@ '\n' + 'terminators box\n' ); + const library = [ + { + title: 'Simple arrow', + code: '{Agent1} -> {Agent2}: {Message}', + }, + { + title: 'Arrow with dotted line', + code: '{Agent1} --> {Agent2}: {Message}', + }, + { + title: 'Open arrow', + code: '{Agent1} ->> {Agent2}: {Message}', + }, + { + title: 'Self-connection', + code: '{Agent1} -> {Agent1}: {Message}', + }, + { + title: 'Request/response pair', + code: ( + '{Agent1} -> +{Agent2}: {Request}\n' + + '{Agent1} <-- -{Agent2}: {Response}' + ), + }, + { + title: 'Inline agent creation / destruction', + code: ( + '{Agent1} -> *{Agent2}: {Request}\n' + + '{Agent1} <-- !{Agent2}: {Response}' + ), + }, + { + title: 'Agent creation / destruction', + code: ( + '{Agent1} -> {Agent2}: {Request}\n' + + '{Agent1} <-- {Agent2}: {Response}\n' + + 'end {Agent2}' + ), + preview: ( + 'begin A\n' + + '::\n' + + 'A -> B: Request\n' + + 'A <-- B: Response\n' + + 'end B' + ), + }, + { + title: 'Conditional blocks', + code: ( + 'if {Condition1}\n' + + ' {Agent1} -> {Agent2}\n' + + 'else if {Condition2}\n' + + ' {Agent1} -> {Agent2}\n' + + 'else\n' + + ' {Agent1} -> {Agent2}\n' + + 'end' + ), + preview: ( + 'begin A, B\n' + + 'if Condition1\n' + + ' A -> B\n' + + 'else if Condition2\n' + + ' A -> B\n' + + 'else\n' + + ' A -> B\n' + + 'end' + ), + }, + { + title: 'Repeated blocks', + code: ( + 'repeat {Condition}\n' + + ' {Agent1} -> {Agent2}\n' + + 'end' + ), + preview: ( + 'begin A, B\n' + + 'repeat Condition\n' + + ' A -> B\n' + + 'end' + ), + }, + { + title: 'Note over agent', + code: 'note over {Agent1}: {Message}', + }, + { + title: 'Note over multiple agents', + code: 'note over {Agent1}, {Agent2}: {Message}', + }, + { + title: 'Note left of agent', + code: 'note left of {Agent1}: {Message}', + }, + { + title: 'Note right of agent', + code: 'note right of {Agent1}: {Message}', + }, + { + title: 'Note between agents', + code: 'note between {Agent1}, {Agent2}: {Message}', + }, + { + title: 'State over agent', + code: 'state over {Agent1}: {State}', + }, + { + title: 'Arrows to/from the sides', + code: '[ -> {Agent1}: {Message1}\n{Agent1} -> ]: {Message2}', + }, + { + title: 'Text beside the diagram', + code: 'text right: {Message}', + preview: ( + 'A -> B\n' + + 'simultaneously:\n' + + 'text right: "Message\\non the\\nside"' + ), + }, + { + title: 'Title', + code: 'title {Title}', + preview: 'title Title\nA -> B', + }, + { + title: 'Chunky theme', + code: 'theme chunky', + preview: 'theme chunky\nA -> B', + }, + { + title: 'Cross terminators', + code: 'terminators cross', + preview: 'A -> B\nterminators cross', + }, + { + title: 'Fade terminators', + code: 'terminators fade', + preview: 'A -> B\nterminators fade', + }, + { + title: 'Bar terminators', + code: 'terminators bar', + preview: 'A -> B\nterminators bar', + }, + { + title: 'Box terminators', + code: 'terminators box', + preview: 'A -> B\nterminators box', + }, + ]; const ui = new Interface({ defaultCode, parser: new Parser(), @@ -49,6 +199,7 @@ new ChunkyTheme(), ]}), exporter: new Exporter(), + library, localStorage: 'src', }); ui.build(document.body); diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index bc2b1df..d624e74 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -135,6 +135,15 @@ define([ }); } + clone({namespace = null} = {}) { + return new Renderer({ + themes: this.getThemes(), + namespace, + components: this.components, + SVGTextBlockClass: this.SVGTextBlockClass, + }); + } + buildStaticElements() { this.base = svg.makeContainer(); @@ -625,6 +634,10 @@ define([ ); } + getThemes() { + return this.getThemeNames().map((name) => this.themes.get(name)); + } + getAgentX(name) { return this.agentInfos.get(name).x; } diff --git a/styles/main.css b/styles/main.css index 8e32757..179b6f3 100644 --- a/styles/main.css +++ b/styles/main.css @@ -7,12 +7,16 @@ html, body { position: absolute; left: 0; top: 0; - bottom: 100px; + bottom: 0; width: 30%; box-sizing: border-box; border-right: 1px solid #808080; } +.pane-code.reduced { + bottom: 200px; +} + .pane-code .CodeMirror { width: 100%; height: 100%; @@ -67,29 +71,88 @@ html, body { height: 100%; } -.pane-error { +.pane-library { position: absolute; left: 0; bottom: 0; width: 30%; + height: 200px; + box-sizing: border-box; + background: #EEEEEE; + border-top: 1px solid #808080; + border-right: 1px solid #808080; + user-select: none; +} + +.pane-library-scroller { + width: 100%; + height: 100%; + overflow: auto; +} + +.pane-library-inner { + padding: 5px; +} + +.library-item { + display: inline-block; + width: 80px; + height: 80px; +} + +.library-item > div { + width: 80px; + height: 80px; + border: 2px solid #EEEEEE; + background: #FFFFFF; + box-sizing: border-box; + cursor: pointer; + overflow: hidden; + transition: transform 0.2s, border-color 0.2s, background 0.2s; +} + +.library-item.broken > div { + padding: 5px; + font: 6px monospace; + white-space: pre; +} + +.library-item svg { + width: 100%; + height: 100%; +} + +.library-item:hover > div { + border-color: #FFCC00; + background: #FFFFDD; +/* position: absolute;*/ + transform: scale(1.1); + z-index: 10; +} + +.msg-error { + display: none; + position: absolute; + top: 50%; + left: 20%; + margin-top: -50px; + width: 60%; height: 100px; overflow: auto; box-sizing: border-box; padding: 5px 10px; font-family: monospace; - background: #DDDDDD; - border-top: 1px solid #808080; - border-right: 1px solid #808080; -} - -.pane-error.ok { - color: #007700; - background: #E8EEE8; -} - -.pane-error.error { color: #770000; background: #EEE8E8; + border: 1px solid rgba(0, 0, 0, 0.5); + border-radius: 5px; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.2); + opacity: 0.95; + z-index: 4; +} + +.msg-error.error { + display: block; } .options {