add import and export diagram
- export diagram * add a diagram export dialog where you can choose the file name and download type (code, png, pdf, ...) * set default download type to code * open file save dialog via menu or Ctrl+S (or Meta+S for Mac) - import diagram * similar to [draw.io](https://app.diagrams.net) * open a PlantUML diagram image, use metat data to get diagram code and load this diagram (Note: meta data is currently only supported by PNG and SVG diagram files) * support drag&drop * add diagram import dialog - since three are now multiple options/action -> create a little editor menu - add documentation (including gif examples)
@ -20,3 +20,6 @@ insert_final_newline = false
|
|||||||
|
|
||||||
[{Dockerfile,Dockerfile.*}]
|
[{Dockerfile,Dockerfile.*}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[.vscode/*.json]
|
||||||
|
indent_size = 4
|
||||||
|
4
.vscode/settings.json
vendored
@ -12,6 +12,7 @@
|
|||||||
"Lalloni",
|
"Lalloni",
|
||||||
"monaco",
|
"monaco",
|
||||||
"plantuml",
|
"plantuml",
|
||||||
|
"puml",
|
||||||
"Roques",
|
"Roques",
|
||||||
"servlet",
|
"servlet",
|
||||||
"servlets",
|
"servlets",
|
||||||
@ -20,5 +21,6 @@
|
|||||||
"undock",
|
"undock",
|
||||||
"utxt"
|
"utxt"
|
||||||
],
|
],
|
||||||
"cSpell.allowCompoundWords": true
|
"cSpell.allowCompoundWords": true,
|
||||||
|
"svg.preview.background": "transparent"
|
||||||
}
|
}
|
BIN
docs/WebUI/gifs/diagram-export.gif
Normal file
After Width: | Height: | Size: 296 KiB |
BIN
docs/WebUI/gifs/diagram-import.gif
Normal file
After Width: | Height: | Size: 1.5 MiB |
21
docs/WebUI/import-export.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Import and Export editable PlantUML Diagrams
|
||||||
|
|
||||||
|
Similar to [draw.io](https://app.diagrams.net) it is possible to load and continue editing PlantUML diagram images.
|
||||||
|
|
||||||
|
|
||||||
|
# Export a diagram
|
||||||
|
|
||||||
|
Via the editor menu or Ctrl+S (or Meta+S in the case of a Mac) you can open the file save dialog.
|
||||||
|
Here you can edit the file name, choose a file/diagram type and download the diagram.
|
||||||
|
The default is to download the PlantUML code.
|
||||||
|
|
||||||
|
![export](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/diagram-export.gif)
|
||||||
|
|
||||||
|
|
||||||
|
# Import a diagram
|
||||||
|
|
||||||
|
This feature is based on the PlantUML meta data which currently **support only PNG and SVG** diagrams.
|
||||||
|
Besides a diagram image, you can of course also load a diagram code file.
|
||||||
|
Moreover, because it is so nice and convenient, we also added a drag-and-drop feature.
|
||||||
|
|
||||||
|
![import](https://raw.githubusercontent.com/plantuml/plantuml-server/master/docs/WebUI/gifs/diagram-import.gif)
|
Before Width: | Height: | Size: 301 B After Width: | Height: | Size: 301 B |
Before Width: | Height: | Size: 384 B After Width: | Height: | Size: 384 B |
1
src/main/webapp/assets/actions/download.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
After Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
1
src/main/webapp/assets/actions/upload.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
After Width: | Height: | Size: 241 B |
@ -36,13 +36,26 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="btn-input">
|
<div class="btn-input">
|
||||||
<input id="url" type="text" name="url" value="png/<%= diagramUrl %>" />
|
<input id="url" type="text" name="url" value="png/<%= diagramUrl %>" />
|
||||||
<input type="image" alt="copy" src="assets/copy.svg" onclick="copyUrlToClipboard()" />
|
<input type="image" alt="copy" src="assets/actions/copy.svg" onclick="copyUrlToClipboard()" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-main monaco-editor-container">
|
<div class="flex-main monaco-editor-container">
|
||||||
<textarea id="initCode" name="initCode" style="display: none;"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
|
<textarea id="initCode" name="initCode" style="display: none;"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
|
||||||
<div id="monaco-editor"></div>
|
<div id="monaco-editor"></div>
|
||||||
<input type="image" alt="copy" src="assets/copy.svg" onclick="copyCodeToClipboard()" />
|
<div class="editor-menu" tabindex="-1">
|
||||||
|
<div class="menu-kebab">
|
||||||
|
<div class="kebab-circle"></div>
|
||||||
|
<div class="kebab-circle"></div>
|
||||||
|
<div class="kebab-circle"></div>
|
||||||
|
<div class="kebab-circle"></div>
|
||||||
|
<div class="kebab-circle"></div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-items">
|
||||||
|
<input class="menu-item" type="image" alt="copy" title="Copy code" src="assets/actions/copy.svg" onclick="copyCodeToClipboard()" />
|
||||||
|
<input class="menu-item" type="image" alt="import" title="Import diagram" src="assets/actions/upload.svg" onclick="openModal('diagram-import')" />
|
||||||
|
<input class="menu-item" type="image" alt="export" title="Export diagram" src="assets/actions/download.svg" onclick="openModal('diagram-export')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="previewer-main-container" class="previewer flex-main">
|
<div id="previewer-main-container" class="previewer flex-main">
|
||||||
@ -52,6 +65,9 @@
|
|||||||
<div class="footer">
|
<div class="footer">
|
||||||
<%@ include file="resource/footer.jsp" %>
|
<%@ include file="resource/footer.jsp" %>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- editor modals -->
|
||||||
|
<%@ include file="resource/diagram-import.jsp" %>
|
||||||
|
<%@ include file="resource/diagram-export.jsp" %>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -6,19 +6,25 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
--font-color: black;
|
--font-color: black;
|
||||||
|
--font-color-disabled: #888;
|
||||||
--bg-color: white;
|
--bg-color: white;
|
||||||
--border-color: #ccc;
|
--border-color: #ccc;
|
||||||
|
--border-color-2: #aaa;
|
||||||
--footer-font-color: #666;
|
--footer-font-color: #666;
|
||||||
--footer-bg-color: #eee;
|
--footer-bg-color: #eee;
|
||||||
--settings-bg-color: #fefefe;
|
--modal-bg-color: #fefefe;
|
||||||
|
--file-drop-color: #eee;
|
||||||
}
|
}
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--font-color: #ccc;
|
--font-color: #ccc;
|
||||||
|
--font-color-disabled: #777;
|
||||||
--bg-color: #212121;
|
--bg-color: #212121;
|
||||||
--border-color: #848484;
|
--border-color: #848484;
|
||||||
|
--border-color-2: #aaa;
|
||||||
--footer-font-color: #ccc;
|
--footer-font-color: #ccc;
|
||||||
--footer-bg-color: black;
|
--footer-bg-color: black;
|
||||||
--settings-bg-color: #424242;
|
--modal-bg-color: #424242;
|
||||||
|
--file-drop-color: #212121;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************* default settings *************/
|
/************* default settings *************/
|
||||||
@ -43,7 +49,11 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input:not([type=image]) {
|
input:not([type="image"]) {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
input[type="file"]::file-selector-button {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
}
|
}
|
||||||
@ -72,6 +82,14 @@ select {
|
|||||||
min-width: 3px;
|
min-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* wait cursor *************/
|
||||||
|
.wait {
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
.wait > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/************* flex rows and columns *************/
|
/************* flex rows and columns *************/
|
||||||
.flex-columns {
|
.flex-columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -181,24 +199,102 @@ select {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
.btn-input input[type=image] {
|
.btn-input input[type="image"] {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin-left: 0.7em;
|
margin-left: 0.7em;
|
||||||
padding: 0 0.3em;
|
padding: 0 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************* Monaco editor copy button *************/
|
/************* Monaco editor action menu *************/
|
||||||
.monaco-editor-container input[type=image] {
|
.monaco-editor-container .editor-menu {
|
||||||
height: 1.5rem;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2rem;
|
right: 0;
|
||||||
top: 1rem;
|
top: 0;
|
||||||
opacity: 0.5;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.monaco-editor-container input[type=image]:hover {
|
.monaco-editor-container .editor-menu > div.menu-kebab {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
scale: 0.5;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .editor-menu:hover > div.menu-kebab,
|
||||||
|
.monaco-editor-container .editor-menu:focus > div.menu-kebab {
|
||||||
|
outline: none;
|
||||||
|
scale: 0.65;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .menu-kebab .kebab-circle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin: 3px;
|
||||||
|
background: var(--font-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .menu-kebab {
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
transition: all 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4),
|
||||||
|
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(4) {
|
||||||
|
margin-left: -25px;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .menu-kebab .kebab-circle:nth-child(5) {
|
||||||
|
margin-left: 13px;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .editor-menu:hover .menu-kebab,
|
||||||
|
.monaco-editor-container .editor-menu:focus .menu-kebab {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.monaco-editor-container .editor-menu:hover .menu-kebab .kebab-circle,
|
||||||
|
.monaco-editor-container .editor-menu:focus .menu-kebab .kebab-circle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monaco-editor-container .editor-menu .menu-item {
|
||||||
|
display: none;
|
||||||
|
margin: 1rem 0;
|
||||||
|
height: 1.75rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
position: relative;
|
||||||
|
-webkit-animation-name: animateitem;
|
||||||
|
-webkit-animation-duration: 0.4s;
|
||||||
|
animation-name: animateitem;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes animateitem {
|
||||||
|
from { top: -50%; opacity: 0; }
|
||||||
|
to { top: 0; opacity: 0.5; }
|
||||||
|
}
|
||||||
|
@keyframes animateitem {
|
||||||
|
from { top: -50%; opacity: 0; }
|
||||||
|
to { top: 0; opacity: 0.5; }
|
||||||
|
}
|
||||||
|
.monaco-editor-container .editor-menu .menu-item:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.monaco-editor-container .editor-menu:hover .menu-item,
|
||||||
|
.monaco-editor-container .editor-menu:focus .menu-item {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/*******************************************************************/
|
/*******************************************************************/
|
||||||
/************* previewer *************/
|
/************* previewer *************/
|
||||||
.content.viewer-content {
|
.content.viewer-content {
|
||||||
@ -300,7 +396,6 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************/
|
/*******************************************************************/
|
||||||
/************* settings *************/
|
|
||||||
/************* modal *************/
|
/************* modal *************/
|
||||||
.modal {
|
.modal {
|
||||||
display: block;
|
display: block;
|
||||||
@ -315,8 +410,8 @@ select {
|
|||||||
background-color: rgb(0, 0, 0);
|
background-color: rgb(0, 0, 0);
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
.modal-content {
|
.modal .modal-content {
|
||||||
background-color: var(--settings-bg-color);
|
background-color: var(--modal-bg-color);
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border: 3px solid var(--border-color);
|
border: 3px solid var(--border-color);
|
||||||
@ -339,52 +434,96 @@ select {
|
|||||||
to { top: 50%; opacity: 1; }
|
to { top: 50%; opacity: 1; }
|
||||||
}
|
}
|
||||||
/************* header, main, footer *************/
|
/************* header, main, footer *************/
|
||||||
#settings .settings-header h2 {
|
.modal .modal-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#settings .settings-main {
|
.modal .modal-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
#settings .settings-footer {
|
.modal .modal-footer {
|
||||||
|
margin-top: 1rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
/************* label + input *************/
|
/************* inputs *************/
|
||||||
#settings .setting {
|
.modal input, .modal select {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.modal input:not(:focus):invalid {
|
||||||
|
border-bottom-color: red;
|
||||||
|
}
|
||||||
|
.modal input[type="file"]::file-selector-button {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
/************* ok + cancel buttons *************/
|
||||||
|
.modal input.ok, .modal input.cancel {
|
||||||
|
min-width: 5rem;
|
||||||
|
}
|
||||||
|
.modal input.ok[disabled], .modal input.cancel[disabled] {
|
||||||
|
color: var(--font-color-disabled);
|
||||||
|
}
|
||||||
|
.modal input.ok:not([disabled]):hover {
|
||||||
|
border-bottom-color: green;
|
||||||
|
}
|
||||||
|
.modal input.cancel:not([disabled]):hover {
|
||||||
|
border-bottom-color: darkred;
|
||||||
|
}
|
||||||
|
/************* label + input pair *************/
|
||||||
|
.modal .label-input-pair {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#settings .setting:first-child {
|
.modal .label-input-pair:first-child {
|
||||||
margin: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
#settings .setting label {
|
.modal .label-input-pair:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.modal .label-input-pair label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 15rem;
|
min-width: 15rem;
|
||||||
}
|
}
|
||||||
#settings .setting label + input, #settings .setting label + select {
|
.modal .label-input-pair label + input,
|
||||||
|
.modal .label-input-pair label + select {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
#settings input, #settings select {
|
|
||||||
border: 1px solid var(--border-color);
|
/************* settings *************/
|
||||||
}
|
|
||||||
#settings input:not(:focus):invalid {
|
|
||||||
border-bottom-color: red;
|
|
||||||
}
|
|
||||||
/************* settings editor *************/
|
|
||||||
#settings #settings-monaco-editor {
|
#settings #settings-monaco-editor {
|
||||||
height: 17rem;
|
height: 17rem;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
/************* ok + cancel buttons *************/
|
/************* diagram import *************/
|
||||||
#settings input.ok, #settings input.cancel {
|
#diagram-import p.error-message {
|
||||||
min-width: 5rem;
|
color: darkred;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
#settings input.ok:hover {
|
#diagram-import input[type="file"] {
|
||||||
border-bottom-color: green;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 0.2rem dashed var(--border-color);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 5rem 2rem;
|
||||||
}
|
}
|
||||||
#settings input.cancel:hover {
|
#diagram-import input[type="file"],
|
||||||
border-bottom-color: darkred;
|
#diagram-import input[type="file"]::file-selector-button {
|
||||||
|
background-color: var(--modal-bg-color);
|
||||||
|
}
|
||||||
|
#diagram-import input[type="file"]:hover,
|
||||||
|
#diagram-import input[type="file"].drop-able {
|
||||||
|
border-color: var(--border-color-2);
|
||||||
|
background-color: var(--file-drop-color);
|
||||||
|
}
|
||||||
|
#diagram-import input[type="file"]:hover::file-selector-button,
|
||||||
|
#diagram-import input[type="file"].drop-able::file-selector-button {
|
||||||
|
background-color: var(--file-drop-color);
|
||||||
|
}
|
||||||
|
/************* diagram export *************/
|
||||||
|
#diagram-export.modal .label-input-pair label {
|
||||||
|
min-width: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************/
|
/*******************************************************************/
|
||||||
@ -392,7 +531,7 @@ select {
|
|||||||
[data-theme="dark"] img:not(#diagram-png):not(.no-filter) {
|
[data-theme="dark"] img:not(#diagram-png):not(.no-filter) {
|
||||||
filter: invert() contrast(30%);
|
filter: invert() contrast(30%);
|
||||||
}
|
}
|
||||||
[data-theme="dark"] input[type=image] {
|
[data-theme="dark"] input[type="image"] {
|
||||||
filter: invert() contrast(30%);
|
filter: invert() contrast(30%);
|
||||||
}
|
}
|
||||||
[data-theme="dark"] a {
|
[data-theme="dark"] a {
|
||||||
|
@ -43,7 +43,7 @@ function isVisible(el) {
|
|||||||
// `offsetParent` returns `null` if the element, or any of its parents,
|
// `offsetParent` returns `null` if the element, or any of its parents,
|
||||||
// is hidden via the display style property.
|
// is hidden via the display style property.
|
||||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
||||||
return (el.offsetParent === null)
|
return (el.offsetParent !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibility(el, visibility, focus=false) {
|
function setVisibility(el, visibility, focus=false) {
|
||||||
@ -55,6 +55,11 @@ function setVisibility(el, visibility, focus=false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMac = (function() {
|
||||||
|
const PLATFORM = navigator?.userAgentData?.platform || navigator?.platform || "unknown";
|
||||||
|
return PLATFORM.match("Mac");
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
// ==========================================================================================================
|
// ==========================================================================================================
|
||||||
// == URL helpers ==
|
// == URL helpers ==
|
||||||
@ -184,27 +189,81 @@ function requestDiagramMap(encodedDiagram, index, callback) {
|
|||||||
requestDiagram("map", encodedDiagram, index, callback);
|
requestDiagram("map", encodedDiagram, index, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestMetadata(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("diagram", file, file.name);
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (xhr.status >= 200 && xhr.status <= 300) {
|
||||||
|
resolve(xhr.response);
|
||||||
|
} else {
|
||||||
|
reject({ status: xhr.status, response: xhr.response });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open("POST", "metadata", true);
|
||||||
|
xhr.setRequestHeader("Accept", "application/json");
|
||||||
|
xhr.responseType = "json";
|
||||||
|
xhr.send(fd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================================================================================
|
||||||
|
// == modal ==
|
||||||
|
|
||||||
|
const { registerModalListener, openModal, closeModal } = (function() {
|
||||||
|
const modalListener = {};
|
||||||
|
return {
|
||||||
|
registerModalListener: (id, fnOpen=undefined, fnClose=undefined) => modalListener[id] = { fnOpen, fnClose },
|
||||||
|
openModal: (id, ...args) => {
|
||||||
|
modalListener[id]?.fnOpen?.call(...args) || setVisibility(document.getElementById(id), true, true)
|
||||||
|
},
|
||||||
|
closeModal: (id, ...args) => {
|
||||||
|
modalListener[id]?.fnClose?.call(...args) || setVisibility(document.getElementById(id), false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
function initModals() {
|
||||||
|
document.querySelectorAll(".modal").forEach(modal => {
|
||||||
|
modal.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape" || event.key === "Esc") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeModal(modal.id);
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
const okBtn = modal.querySelector('input.ok[type="button"]');
|
||||||
|
if (okBtn && !okBtn.disabled) {
|
||||||
|
okBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isModalOpen(id) {
|
||||||
|
return isVisible(document.getElementById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllModals() {
|
||||||
|
document.querySelectorAll(".modal").forEach(modal => closeModal(modal.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================================================================================
|
// ==========================================================================================================
|
||||||
// == settings ==
|
// == settings ==
|
||||||
|
|
||||||
function initSettings() {
|
function initSettings() {
|
||||||
document.getElementById("settings").addEventListener("keydown", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
console.log(event);
|
|
||||||
if (event.key === "Escape" || event.key === "Esc") {
|
|
||||||
closeSettings();
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
document.getElementById("theme").addEventListener("change", (event) => {
|
document.getElementById("theme").addEventListener("change", (event) => {
|
||||||
const theme = event.target.value;
|
const theme = event.target.value;
|
||||||
const editorCreateOptionsString = document.settingsEditor.getValue();
|
const editorCreateOptionsString = document.settingsEditor.getValue();
|
||||||
const replaceTheme = (theme === "dark") ? "vs" : "vs-dark";
|
const replaceTheme = (theme === "dark") ? "vs" : "vs-dark";
|
||||||
const substituteTheme = (theme === "dark") ? "vs-dark" : "vs";
|
const substituteTheme = (theme === "dark") ? "vs-dark" : "vs";
|
||||||
const regex = new RegExp('("theme"\\s*:\\s*)"' + replaceTheme + '"', "gm");
|
const regex = new RegExp('("theme"\\s*:\\s*)"' + replaceTheme + '"', "gm");
|
||||||
document.settingsEditor.getModel().setValue(
|
setEditorValue(document.settingsEditor, editorCreateOptionsString.replace(regex, '$1"' + substituteTheme + '"'));
|
||||||
editorCreateOptionsString.replace(regex, '$1"' + substituteTheme + '"')
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
document.settingsEditor = monaco.editor.create(document.getElementById("settings-monaco-editor"), {
|
document.settingsEditor = monaco.editor.create(document.getElementById("settings-monaco-editor"), {
|
||||||
language: "json", ...document.appConfig.editorCreateOptions
|
language: "json", ...document.appConfig.editorCreateOptions
|
||||||
@ -220,13 +279,7 @@ function openSettings() {
|
|||||||
document.getElementById("theme").value = document.appConfig.theme;
|
document.getElementById("theme").value = document.appConfig.theme;
|
||||||
document.getElementById("diagramPreviewType").value = document.appConfig.diagramPreviewType;
|
document.getElementById("diagramPreviewType").value = document.appConfig.diagramPreviewType;
|
||||||
document.getElementById("editorWatcherTimeout").value = document.appConfig.editorWatcherTimeout;
|
document.getElementById("editorWatcherTimeout").value = document.appConfig.editorWatcherTimeout;
|
||||||
document.settingsEditor.getModel().setValue(
|
setEditorValue(document.settingsEditor, JSON.stringify(document.appConfig.editorCreateOptions, null, " "));
|
||||||
JSON.stringify(document.appConfig.editorCreateOptions, null, " ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeSettings() {
|
|
||||||
setVisibility(document.getElementById("settings"), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
@ -236,7 +289,7 @@ function saveSettings() {
|
|||||||
appConfig.diagramPreviewType = document.getElementById("diagramPreviewType").value;
|
appConfig.diagramPreviewType = document.getElementById("diagramPreviewType").value;
|
||||||
appConfig.editorCreateOptions = JSON.parse(document.settingsEditor.getValue());
|
appConfig.editorCreateOptions = JSON.parse(document.settingsEditor.getValue());
|
||||||
broadcastSettings(appConfig);
|
broadcastSettings(appConfig);
|
||||||
closeSettings();
|
closeModal("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastSettings(appConfig) {
|
function broadcastSettings(appConfig) {
|
||||||
@ -255,6 +308,248 @@ function applySettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================================================================================
|
||||||
|
// == diagram import ==
|
||||||
|
|
||||||
|
function openDiagramImportDialog(isOpenManually = true) {
|
||||||
|
const diagramImportDialog = document.getElementById("diagram-import");
|
||||||
|
setVisibility(diagramImportDialog, true, true);
|
||||||
|
diagramImportDialog.dataset.isOpenManually = isOpenManually.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDiagramImportInputChange(fileInput) {
|
||||||
|
document.getElementById("diagram-import-error-message").innerText = "";
|
||||||
|
document.getElementById("diagram-import-ok-btn").disabled = fileInput.files?.length < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDiagramImportDiaglog() {
|
||||||
|
const diagramImportDialog = document.getElementById("diagram-import");
|
||||||
|
const diagramInputElement = document.getElementById("diagram-import-input");
|
||||||
|
const errorMessageElement = document.getElementById("diagram-import-error-message");
|
||||||
|
|
||||||
|
function closeDiagramImportDialog() {
|
||||||
|
diagramInputElement.value = ""; // reset or clear
|
||||||
|
onDiagramImportInputChange(diagramInputElement);
|
||||||
|
diagramImportDialog.removeAttribute("data-is-open-manually");
|
||||||
|
setVisibility(diagramImportDialog, false);
|
||||||
|
}
|
||||||
|
function checkFileLocally(file) {
|
||||||
|
function getImageFileType({name, type}) {
|
||||||
|
const supported = ["png", "svg"];
|
||||||
|
// get type by mime type
|
||||||
|
let fileType = supported.filter(t => type.toLowerCase().indexOf(t) !== -1)[0];
|
||||||
|
if (fileType) return fileType;
|
||||||
|
// fallback: get type by filename extension
|
||||||
|
if (name.indexOf(".") === -1) return undefined;
|
||||||
|
const ext = name.substring(name.lastIndexOf(".")+1).toLowerCase();
|
||||||
|
return supported.filter(t => ext === t)[0];
|
||||||
|
}
|
||||||
|
function isDiagramCode({name, type}) {
|
||||||
|
// get type by mime type
|
||||||
|
let supported = ["plain", "text", "plantuml", "puml"];
|
||||||
|
if (supported.filter(t => type.toLowerCase().indexOf(t) !== -1).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// fallback: get type by filename extension
|
||||||
|
if (name.indexOf(".") === -1) return false;
|
||||||
|
const ext = name.substring(name.lastIndexOf('.')+1).toLowerCase();
|
||||||
|
supported = ["txt", "puml", "plantuml"];
|
||||||
|
return supported.filter(t => ext === t).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = getImageFileType(file);
|
||||||
|
const isCode = type === undefined ? isDiagramCode(file) : false;
|
||||||
|
if (!type && !isCode) {
|
||||||
|
errorMessageElement.innerText = "File not supported. " +
|
||||||
|
"Only PNG and SVG diagram images as well as PlantUML code text files are supported."
|
||||||
|
}
|
||||||
|
return { type, isDiagramCode: isCode, valid: type || isCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
function importDiagram(file, fileCheck) {
|
||||||
|
function loadDiagram(code) {
|
||||||
|
syncCodeEditor(code);
|
||||||
|
broadcastCodeEditorChanges("file-drop", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
diagramImportDialog.classList.add("wait");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (fileCheck.type) {
|
||||||
|
// upload diagram image, get meta data from server and load diagram from result
|
||||||
|
requestMetadata(file).then(
|
||||||
|
metadata => { loadDiagram(metadata.decoded); resolve(); },
|
||||||
|
({ response }) => { errorMessageElement.innerText = response.message || response; reject(); }
|
||||||
|
);
|
||||||
|
} else if (fileCheck.isDiagramCode) {
|
||||||
|
// read code (text) file
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = event => loadDiagram(event.target.result);
|
||||||
|
reader.readAsText(file);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
// this error should already be handled.
|
||||||
|
errorMessageElement.innerText = "File not supported. " +
|
||||||
|
"Only PNG and SVG diagram images as well as PlantUML code text files are supported.";
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
}).then(() => closeDiagramImportDialog(), () => {}).finally(() => diagramImportDialog.classList.remove("wait"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGlobalDragEnter(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
if (!isVisible(diagramImportDialog)) {
|
||||||
|
openDiagramImportDialog(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDiagramImportDragOver(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.dataTransfer !== null) {
|
||||||
|
event.dataTransfer.dropEffect = "copy";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onDiagramImportDrop(event) {
|
||||||
|
function stop() {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
const files = event.dataTransfer.files || event.target.files;
|
||||||
|
if (!files || files.length < 1) {
|
||||||
|
return stop();
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const fileCheck = checkFileLocally(file);
|
||||||
|
if (!fileCheck.valid) {
|
||||||
|
return stop();
|
||||||
|
}
|
||||||
|
if (diagramImportDialog.dataset.isOpenManually === "true") {
|
||||||
|
return; // let file input handle this event => no `stop()`!
|
||||||
|
}
|
||||||
|
// drop and go - close modal without additional ok button click
|
||||||
|
stop();
|
||||||
|
importDiagram(file, fileCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
// global drag&drop events
|
||||||
|
window.addEventListener("dragenter", onGlobalDragEnter, false);
|
||||||
|
// diagram import dialog drag&drop events
|
||||||
|
diagramImportDialog.addEventListener("dragenter", event => event.target.classList.add("drop-able"), false);
|
||||||
|
diagramImportDialog.addEventListener("dragover", onDiagramImportDragOver, false);
|
||||||
|
diagramImportDialog.addEventListener("dragexit", event => event.target.classList.remove("drop-able"), false);
|
||||||
|
diagramImportDialog.addEventListener("drop", onDiagramImportDrop, false);
|
||||||
|
// ok button
|
||||||
|
document.getElementById("diagram-import-ok-btn").addEventListener("click", () => {
|
||||||
|
const file = diagramInputElement.files[0]; // should be always a valid file
|
||||||
|
importDiagram(file, checkFileLocally(file)); // otherwise button should be disabled
|
||||||
|
});
|
||||||
|
// reset or clear file input
|
||||||
|
diagramInputElement.value = "";
|
||||||
|
onDiagramImportInputChange(diagramInputElement);
|
||||||
|
// register model listeners
|
||||||
|
registerModalListener("diagram-import", openDiagramImportDialog, closeDiagramImportDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================================================================================
|
||||||
|
// == diagram export ==
|
||||||
|
|
||||||
|
function initFileExportDialog() {
|
||||||
|
const filenameInput = document.getElementById("download-name");
|
||||||
|
const fileTypeSelect = document.getElementById("download-type");
|
||||||
|
|
||||||
|
function openDiagramExportDialog() {
|
||||||
|
setVisibility(document.getElementById("diagram-export"), true, true);
|
||||||
|
const code = document.editor.getValue();
|
||||||
|
const name = Array.from(
|
||||||
|
code.matchAll(/^\s*@start[a-zA-Z]*\s+([a-zA-Z-_äöüÄÖÜß ]+)\s*$/gm),
|
||||||
|
m => m[1]
|
||||||
|
)[0] || "diagram";
|
||||||
|
filenameInput.value = name + ".puml";
|
||||||
|
fileTypeSelect.value = "code";
|
||||||
|
filenameInput.focus();
|
||||||
|
}
|
||||||
|
function splitFilename(filename) {
|
||||||
|
const idx = filename.lastIndexOf(".");
|
||||||
|
if (idx < 1) {
|
||||||
|
return { name: filename, ext: null };
|
||||||
|
}
|
||||||
|
if (idx === filename.length - 1) {
|
||||||
|
return { name: filename.slice(0, -1), ext: null };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: filename.substring(0, idx),
|
||||||
|
ext: filename.substring(idx + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getExtensionByType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case "epstext": return "eps";
|
||||||
|
case "code": return "puml";
|
||||||
|
default: return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getTypeByExtension(ext) {
|
||||||
|
if (!ext) return ext;
|
||||||
|
ext = ext.toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case "puml":
|
||||||
|
case "plantuml":
|
||||||
|
case "code":
|
||||||
|
return "code";
|
||||||
|
case "ascii": return "txt"
|
||||||
|
default: return ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onTypeChanged(event) {
|
||||||
|
const type = event.target.value;
|
||||||
|
const ext = getExtensionByType(type);
|
||||||
|
const { name } = splitFilename(filenameInput.value);
|
||||||
|
filenameInput.value = name + "." + ext;
|
||||||
|
}
|
||||||
|
function onFilenameChanged(event) {
|
||||||
|
const { ext } = splitFilename(event.target.value);
|
||||||
|
const type = getTypeByExtension(ext);
|
||||||
|
if (!type) return;
|
||||||
|
fileTypeSelect.value = type;
|
||||||
|
}
|
||||||
|
function downloadFile() {
|
||||||
|
const filename = filenameInput.value;
|
||||||
|
const type = fileTypeSelect.value;
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = filename;
|
||||||
|
if (type === "code") {
|
||||||
|
const code = document.editor.getValue();
|
||||||
|
link.href = "data:," + encodeURIComponent(code);
|
||||||
|
} else {
|
||||||
|
if (document.appData.index !== undefined) {
|
||||||
|
link.href = type + "/" + document.appData.index + "/" + document.appData.encodedDiagram;
|
||||||
|
} else {
|
||||||
|
link.href = type + "/" + document.appData.encodedDiagram;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// register modal
|
||||||
|
registerModalListener("diagram-export", openDiagramExportDialog);
|
||||||
|
// add listener
|
||||||
|
filenameInput.addEventListener("change", onFilenameChanged);
|
||||||
|
fileTypeSelect.addEventListener("change", onTypeChanged);
|
||||||
|
document.getElementById("diagram-export-ok-btn").addEventListener("click", downloadFile);
|
||||||
|
// add Ctrl+S or Meta+S (Mac) key shortcut to open export dialog
|
||||||
|
window.addEventListener("keydown", event => {
|
||||||
|
if (event.key === "s" && (isMac ? event.metaKey : event.ctrlKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!isModalOpen("diagram-export")) {
|
||||||
|
openDiagramExportDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================================================================================
|
// ==========================================================================================================
|
||||||
// == dock (pop in) and undock (pop out) previewer ==
|
// == dock (pop in) and undock (pop out) previewer ==
|
||||||
|
|
||||||
@ -353,6 +648,11 @@ function updatePaginatorSelection() {
|
|||||||
// ==========================================================================================================
|
// ==========================================================================================================
|
||||||
// == sync data ==
|
// == sync data ==
|
||||||
|
|
||||||
|
function setEditorValue(editor, text, forceMoveMarkers=undefined) {
|
||||||
|
// replace editor value but preserve undo stack
|
||||||
|
editor.executeEdits('', [{ range: editor.getModel().getFullModelRange(), text, forceMoveMarkers }]);
|
||||||
|
}
|
||||||
|
|
||||||
function updateDiagramMap(mapString, mapEl) {
|
function updateDiagramMap(mapString, mapEl) {
|
||||||
const mapBtn = document.getElementById("map-diagram-link");
|
const mapBtn = document.getElementById("map-diagram-link");
|
||||||
mapEl = mapEl || document.getElementById("plantuml_map");
|
mapEl = mapEl || document.getElementById("plantuml_map");
|
||||||
@ -433,7 +733,7 @@ function syncUrlTextInput(encodedDiagram, index) {
|
|||||||
|
|
||||||
function syncCodeEditor(code) {
|
function syncCodeEditor(code) {
|
||||||
document.appConfig.changeEventsEnabled = false;
|
document.appConfig.changeEventsEnabled = false;
|
||||||
document.editor.getModel().setValue(code);
|
setEditorValue(document.editor, code);
|
||||||
document.appConfig.changeEventsEnabled = true;
|
document.appConfig.changeEventsEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,7 +796,10 @@ async function initializeApp(view) {
|
|||||||
initTheme();
|
initTheme();
|
||||||
await initializeDiagram();
|
await initializeDiagram();
|
||||||
initializePaginator();
|
initializePaginator();
|
||||||
|
initModals();
|
||||||
if (view !== "previewer") {
|
if (view !== "previewer") {
|
||||||
|
initDiagramImportDiaglog();
|
||||||
|
initFileExportDialog();
|
||||||
addSavePlantumlDocumentEvent();
|
addSavePlantumlDocumentEvent();
|
||||||
}
|
}
|
||||||
if (["previewer", "editor"].includes(view)) {
|
if (["previewer", "editor"].includes(view)) {
|
||||||
@ -514,22 +817,11 @@ function loadCodeEditor() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeCodeEditor() {
|
const broadcastCodeEditorChanges = (function() {
|
||||||
// create editor model including editor watcher
|
let plantumlFeatures;
|
||||||
let timer = 0;
|
return function(sender, code) {
|
||||||
const uri = monaco.Uri.parse("inmemory://plantuml");
|
plantumlFeatures = plantumlFeatures || new PlantUmlLanguageFeatures();
|
||||||
const initCodeEl = document.getElementById("initCode");
|
|
||||||
const initCode = initCodeEl.value;
|
|
||||||
initCodeEl.remove();
|
|
||||||
const plantumlFeatures = new PlantUmlLanguageFeatures();
|
|
||||||
const model = monaco.editor.createModel(initCode, "apex", uri);
|
|
||||||
model.onDidChangeContent(() => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (document.appConfig.changeEventsEnabled) {
|
|
||||||
document.appConfig.autoRefreshState = "waiting";
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
document.appConfig.autoRefreshState = "started";
|
document.appConfig.autoRefreshState = "started";
|
||||||
const code = model.getValue();
|
|
||||||
const numberOfDiagramPages = getNumberOfDiagramPagesFromCode(code);
|
const numberOfDiagramPages = getNumberOfDiagramPagesFromCode(code);
|
||||||
let index = document.appData.index;
|
let index = document.appData.index;
|
||||||
if (index === undefined || numberOfDiagramPages === 1) {
|
if (index === undefined || numberOfDiagramPages === 1) {
|
||||||
@ -539,14 +831,33 @@ function initializeCodeEditor() {
|
|||||||
}
|
}
|
||||||
encodeDiagram(code, (encodedDiagram) => {
|
encodeDiagram(code, (encodedDiagram) => {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
sender: "editor",
|
sender,
|
||||||
data: { encodedDiagram, numberOfDiagramPages, index },
|
data: { encodedDiagram, numberOfDiagramPages, index },
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const model = document.editor.getModel();
|
||||||
plantumlFeatures.validateCode(model)
|
plantumlFeatures.validateCode(model)
|
||||||
.then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
|
.then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
|
||||||
}, document.appConfig.editorWatcherTimeout);
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
function initializeCodeEditor() {
|
||||||
|
// create editor model including editor watcher
|
||||||
|
let timer = 0;
|
||||||
|
const uri = monaco.Uri.parse("inmemory://plantuml");
|
||||||
|
const initCodeEl = document.getElementById("initCode");
|
||||||
|
const initCode = initCodeEl.value;
|
||||||
|
initCodeEl.remove();
|
||||||
|
const model = monaco.editor.createModel(initCode, "apex", uri);
|
||||||
|
model.onDidChangeContent(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (document.appConfig.changeEventsEnabled) {
|
||||||
|
document.appConfig.autoRefreshState = "waiting";
|
||||||
|
timer = setTimeout(
|
||||||
|
() => broadcastCodeEditorChanges("editor", model.getValue()),
|
||||||
|
document.appConfig.editorWatcherTimeout
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// create storage service to expand suggestion documentation by default
|
// create storage service to expand suggestion documentation by default
|
||||||
@ -648,26 +959,11 @@ function initializePaginator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addSavePlantumlDocumentEvent() {
|
function addSavePlantumlDocumentEvent() {
|
||||||
const PLATFORM = navigator?.userAgentData?.platform || navigator?.platform || "unknown";
|
window.addEventListener("keydown", function(e) {
|
||||||
document.addEventListener("keydown", function(e) {
|
if (e.key === "," && (isMac ? e.metaKey : e.ctrlKey)) {
|
||||||
if (e.key === "s" && (PLATFORM.match("Mac") ? e.metaKey : e.ctrlKey)) {
|
|
||||||
// support Ctrl+S to download diagram
|
|
||||||
e.preventDefault();
|
|
||||||
const code = document.editor.getValue();
|
|
||||||
const name = Array.from(
|
|
||||||
code.matchAll(/^\s*@start[a-zA-Z]*\s+([a-zA-Z-_äöüÄÖÜß ]+)\s*$/gm),
|
|
||||||
m => m[1]
|
|
||||||
)[0] || "diagram";
|
|
||||||
// download via link
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.download = name + ".puml";
|
|
||||||
link.href = "data:," + encodeURIComponent(code);
|
|
||||||
link.click();
|
|
||||||
}
|
|
||||||
if (e.key === "," && (PLATFORM.match("Mac") ? e.metaKey : e.ctrlKey)) {
|
|
||||||
// support Ctrl+, to open the settings
|
// support Ctrl+, to open the settings
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (document.getElementById("settings")?.style?.display === "none") {
|
if (!isModalOpen("settings")) {
|
||||||
openSettings();
|
openSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -679,7 +975,7 @@ function addSavePlantumlDocumentEvent() {
|
|||||||
// == communication ==
|
// == communication ==
|
||||||
//
|
//
|
||||||
// send and receive data: {
|
// send and receive data: {
|
||||||
// sender: string = ["editor"|"url"|"paginator"|"settings"],
|
// sender: string = ["editor"|"url"|"paginator"|"settings"|"file-drop"],
|
||||||
// data: {
|
// data: {
|
||||||
// encodedDiagram: string | undefined,
|
// encodedDiagram: string | undefined,
|
||||||
// index: integer | undefined,
|
// index: integer | undefined,
|
||||||
|
32
src/main/webapp/resource/diagram-export.jsp
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<div id="diagram-export" class="modal" style="display: none;" tabindex="-1">
|
||||||
|
<div class="modal-content flex-rows">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Export Diagram</h2>
|
||||||
|
<div class="hr"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-main flex-main">
|
||||||
|
<div class="label-input-pair flex-columns">
|
||||||
|
<label for="download-name">Diagram name:</label>
|
||||||
|
<input class="flex-main" id="download-name" value="diagram.puml" />
|
||||||
|
</div>
|
||||||
|
<div class="label-input-pair flex-columns">
|
||||||
|
<label for="download-type">Diagram type:</label>
|
||||||
|
<select class="flex-main" id="download-type" name="download-type">
|
||||||
|
<option value="txt">ASCII Art</option>
|
||||||
|
<option value="base64">Base64</option>
|
||||||
|
<option value="eps">EPS</option>
|
||||||
|
<option value="epstext">EPS Text</option>
|
||||||
|
<option value="map">MAP</option>
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="code" selected>PlantUML source code</option>
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="svg">SVG</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="diagram-export-ok-btn" class="ok" type="button" value="Export" />
|
||||||
|
<input class="cancel" type="button" value="Cancel" onclick="closeModal('diagram-export')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
16
src/main/webapp/resource/diagram-import.jsp
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div id="diagram-import" class="modal" style="display: none;" tabindex="-1">
|
||||||
|
<div class="modal-content flex-rows">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Import Diagram</h2>
|
||||||
|
<div class="hr"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-main flex-main">
|
||||||
|
<input id="diagram-import-input" type="file" name="diagram" onchange="onDiagramImportInputChange(this);" />
|
||||||
|
<p id="diagram-import-error-message" class="error-message"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input id="diagram-import-ok-btn" class="ok" type="button" value="Import" disabled />
|
||||||
|
<input id="diagram-import-cancel-btn" class="cancel" type="button" value="Cancel" onclick="closeModal('diagram-import');" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -32,7 +32,7 @@
|
|||||||
id="btn-settings"
|
id="btn-settings"
|
||||||
class="btn-settings"
|
class="btn-settings"
|
||||||
type="image"
|
type="image"
|
||||||
src="assets/settings.svg"
|
src="assets/actions/settings.svg"
|
||||||
alt="settings"
|
alt="settings"
|
||||||
onclick="openSettings();"
|
onclick="openSettings();"
|
||||||
/>
|
/>
|
||||||
@ -40,7 +40,7 @@
|
|||||||
id="btn-undock"
|
id="btn-undock"
|
||||||
class="btn-dock"
|
class="btn-dock"
|
||||||
type="image"
|
type="image"
|
||||||
src="assets/undock.svg"
|
src="assets/actions/undock.svg"
|
||||||
alt="undock"
|
alt="undock"
|
||||||
onclick="undock();"
|
onclick="undock();"
|
||||||
/>
|
/>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
id="btn-dock"
|
id="btn-dock"
|
||||||
class="btn-dock"
|
class="btn-dock"
|
||||||
type="image"
|
type="image"
|
||||||
src="assets/dock.svg"
|
src="assets/actions/dock.svg"
|
||||||
alt="dock"
|
alt="dock"
|
||||||
onclick="window.close();"
|
onclick="window.close();"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<div id="settings" class="modal" style="display: none;" tabindex="-1">
|
<div id="settings" class="modal" style="display: none;" tabindex="-1">
|
||||||
<div class="modal-content flex-rows">
|
<div class="modal-content flex-rows">
|
||||||
<div class="settings-header">
|
<div class="modal-header">
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-main flex-main">
|
<div class="modal-main flex-main">
|
||||||
<div class="setting flex-columns">
|
<div class="label-input-pair flex-columns">
|
||||||
<label for="theme">Theme:</label>
|
<label for="theme">Theme:</label>
|
||||||
<select class="flex-main" id="theme" name="theme">
|
<select class="flex-main" id="theme" name="theme">
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting flex-columns">
|
<div class="label-input-pair flex-columns">
|
||||||
<label for="diagramPreviewType">Diagram Preview Type:</label>
|
<label for="diagramPreviewType">Diagram Preview Type:</label>
|
||||||
<select class="flex-main" id="diagramPreviewType" name="diagramPreviewType">
|
<select class="flex-main" id="diagramPreviewType" name="diagramPreviewType">
|
||||||
<option value="png">PNG</option>
|
<option value="png">PNG</option>
|
||||||
@ -21,19 +21,19 @@
|
|||||||
<option value="pdf">PDF</option>
|
<option value="pdf">PDF</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting flex-columns">
|
<div class="label-input-pair flex-columns">
|
||||||
<label for="editorWatcherTimeout">Editor Watcher Timeout:</label>
|
<label for="editorWatcherTimeout">Editor Watcher Timeout:</label>
|
||||||
<input class="flex-main" id="editorWatcherTimeout" type="number" pattern="[1-9]+[0-9]*" value="" />
|
<input class="flex-main" id="editorWatcherTimeout" type="number" pattern="[1-9]+[0-9]*" value="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="setting flex-main">
|
<div class="label-input-pair flex-main">
|
||||||
<label for="editorCreateOptions">Monaco Editor Create Options:</label>
|
<label for="editorCreateOptions">Monaco Editor Create Options:</label>
|
||||||
<br />
|
<br />
|
||||||
<div id="settings-monaco-editor"></div>
|
<div id="settings-monaco-editor"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-footer">
|
<div class="modal-footer">
|
||||||
<input class="ok" type="button" value="Save" onclick="saveSettings();" />
|
<input class="ok" type="button" value="Save" onclick="saveSettings();" />
|
||||||
<input class="cancel" type="button" value="Cancel" onclick="closeSettings();" />
|
<input class="cancel" type="button" value="Cancel" onclick="closeModal('settings');" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|