front-end code refactoring
Since the front-end received much more code (features) than first expected, the files became much too large. For this reason, the JS and CSS code has now been split by component and thus into several small files. However, since there are now many small files, a JS and CSS bundle tool had to come :D.pull/30/head
parent
080cbaada9
commit
478ef3bce7
|
@ -9,6 +9,7 @@
|
|||
"epstext",
|
||||
"etag",
|
||||
"ghaction",
|
||||
"inmemory",
|
||||
"Lalloni",
|
||||
"monaco",
|
||||
"plantuml",
|
||||
|
@ -22,5 +23,8 @@
|
|||
"utxt"
|
||||
],
|
||||
"cSpell.allowCompoundWords": true,
|
||||
"svg.preview.background": "transparent"
|
||||
"svg.preview.background": "dark-transparent",
|
||||
"files.associations": {
|
||||
"*.jspf": "html"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Front-end Contribution
|
||||
|
||||
## Web UI
|
||||
|
||||
The Web UI uses vanilla javascript.
|
||||
|
||||
As online editor Microsoft's [Monaco Editor](https://github.com/microsoft/monaco-editor).
|
||||
The documentation can be found [here](https://microsoft.github.io/monaco-editor/docs.html).
|
||||
You may recognize the editor since it's the code editor from [VS Code](https://github.com/microsoft/vscode).
|
||||
|
||||
The main entry file are `index.jsp`, `previewer.jsp` and `error.jsp`.
|
||||
|
||||
The code structure is mainly divided into `components` and `js`:
|
||||
- `components` are for example a modal or dialog.
|
||||
Anything that include things directly seen and rendered on the page.
|
||||
- `js` contains more the things that do not have a direct influence on the UI. For example the PlantUML language features or the methods for cross-browser/cross-tab communication.
|
||||
|
||||
|
||||
## PlantUML Language Features
|
||||
|
||||
At the moment there is no defined PlantUML language.
|
||||
Feel free to create one!
|
||||
But until then the syntax highlighting form `apex` is used.
|
||||
IMHO it works quite well.
|
||||
|
||||
All PlantUML language features are bundled into a seperate file `plantuml-language.min.js`.
|
||||
Therefore anything under `js/language` should be independent!
|
||||
|
||||
### Code Completion
|
||||
What do you need to do to create a new code completion feature:
|
||||
1. create a new JS file under `js/language/completion` - let's say `xxx.js`
|
||||
2. create a new `registerXxxCompletion` method
|
||||
_It may help you if you look into the [documentation](https://microsoft.github.io/monaco-editor/docs.html#functions/languages.registerCompletionItemProvider.html) or at the provided [sample code](https://microsoft.github.io/monaco-editor/playground.html?source=v0.38.0#example-extending-language-services-completion-provider-example) to understand more about `monaco.languages.registerCompletionItemProvider`._
|
||||
```js
|
||||
PlantUmlLanguageFeatures.prototype.registerEmojiCompletion = function() {
|
||||
monaco.languages.registerCompletionItemProvider(PlantUmlLanguageFeatures.languageSelector, {
|
||||
provideCompletionItems: async (model, position) => {
|
||||
// ...
|
||||
return { suggestions };
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
4. add your new method inside the language initialization inside `js/language/language.js`
|
||||
```diff
|
||||
const PlantUmlLanguageFeatures = function(initialize = true) {
|
||||
if (initialize) {
|
||||
// initialize all validation and code completion methods
|
||||
this.addStartEndValidationListeners();
|
||||
this.registerThemeCompletion();
|
||||
this.registerIconCompletion();
|
||||
this.registerEmojiCompletion();
|
||||
+ this.registerXxxCompletion();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Code Validation
|
||||
What do you need to do to create a new code validation feature:
|
||||
1. create a new JS file under `js/language/validation/listeners` - let's say `zzz-validation.js`
|
||||
2. register your validation methods to the designated event listener
|
||||
The validation event order is: `before` → `code` → `line` → `after`
|
||||
You may look at `js/language/validation/listeners/start-end-validation.js` to get an idea how to register a new listener.
|
||||
3. add your new method inside the language initialization inside `js/language/language.js`
|
||||
```diff
|
||||
const PlantUmlLanguageFeatures = function(initialize = true) {
|
||||
if (initialize) {
|
||||
// initialize all validation and code completion methods
|
||||
this.addStartEndValidationListeners();
|
||||
+ this.addZzzValidationListeners();
|
||||
this.registerThemeCompletion();
|
||||
this.registerIconCompletion();
|
||||
this.registerEmojiCompletion();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### Tipps
|
||||
|
||||
- `pom.xml`: set `withoutCSSJSCompress` to `true` to deactivate the minification
|
||||
- use `mvn fizzed-watcher:run` to watch changes and automatically update the bundled `plantuml.min.{css,js}` and `plantuml-language.min.js` files
|
||||
- if the browser get the error `ReferenceError: require is not defined` or something similar related to the webjars, try `mvn clean install` to get things straight
|
96
pom.jdk8.xml
96
pom.jdk8.xml
|
@ -28,6 +28,10 @@
|
|||
- mvn test -DskipTests=false -DargLine="-Dsystem.test.server=http://localhost:8080/plantuml"
|
||||
-->
|
||||
<skipTests>true</skipTests>
|
||||
<!--
|
||||
JS and CSS compression / minify
|
||||
-->
|
||||
<withoutCSSJSCompress>false</withoutCSSJSCompress>
|
||||
|
||||
<!--
|
||||
This artifact is required for:
|
||||
|
@ -75,7 +79,7 @@
|
|||
<junit.version>5.9.3</junit.version>
|
||||
<junit-suite.version>1.9.3</junit-suite.version>
|
||||
<selenium.version>4.9.1</selenium.version>
|
||||
<selenium-webdrivermanager.version>5.3.2</selenium-webdrivermanager.version>
|
||||
<selenium-webdrivermanager.version>5.3.3</selenium-webdrivermanager.version>
|
||||
<commons-io.version>2.11.0</commons-io.version>
|
||||
<jetty-server.version>${jetty.version}</jetty-server.version>
|
||||
|
||||
|
@ -101,6 +105,7 @@
|
|||
<jetty-maven-plugin.version>${jetty.version}</jetty-maven-plugin.version>
|
||||
<duplicate-finder-maven-plugin.version>1.5.1</duplicate-finder-maven-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.5.0</maven-javadoc-plugin.version>
|
||||
<resources-optimizer-maven-plugin.version>2.4.4</resources-optimizer-maven-plugin.version><!-- no JDK8 support starting version 2.5.0 -->
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
@ -479,6 +484,95 @@
|
|||
</webResources>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>clean-minified-resources</id>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<excludeDefaultDirectories>true</excludeDefaultDirectories>
|
||||
<filesets>
|
||||
<fileset>
|
||||
<directory>${basedir}/src/main/webapp/min</directory>
|
||||
</fileset>
|
||||
</filesets>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.primefaces.extensions</groupId>
|
||||
<artifactId>resources-optimizer-maven-plugin</artifactId>
|
||||
<version>${resources-optimizer-maven-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>optimize</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>optimize</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<warningLevel>DEFAULT</warningLevel>
|
||||
<failOnWarning>true</failOnWarning>
|
||||
<suffix>.min</suffix>
|
||||
<languageIn>ECMASCRIPT_2020</languageIn>
|
||||
<languageOut>ECMASCRIPT5_STRICT</languageOut>
|
||||
<emitUseStrict>true</emitUseStrict>
|
||||
<resourcesSets>
|
||||
<resourcesSet>
|
||||
<inputDir>${basedir}/src/main/webapp</inputDir>
|
||||
<includes>
|
||||
<include>components/**/*.js</include>
|
||||
<include>js/**/*.js</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>js/language/**</exclude>
|
||||
</excludes>
|
||||
<aggregations>
|
||||
<aggregation>
|
||||
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
|
||||
<removeIncluded>false</removeIncluded>
|
||||
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.js</outputFile>
|
||||
</aggregation>
|
||||
</aggregations>
|
||||
</resourcesSet>
|
||||
<resourcesSet>
|
||||
<inputDir>${basedir}/src/main/webapp/js/language</inputDir>
|
||||
<includes>
|
||||
<include>language.js</include>
|
||||
<include>validation/validation.js</include>
|
||||
<include>**/*.js</include>
|
||||
</includes>
|
||||
<aggregations>
|
||||
<aggregation>
|
||||
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
|
||||
<removeIncluded>false</removeIncluded>
|
||||
<outputFile>${basedir}/src/main/webapp/min/plantuml-language.min.js</outputFile>
|
||||
</aggregation>
|
||||
</aggregations>
|
||||
</resourcesSet>
|
||||
<resourcesSet>
|
||||
<inputDir>${basedir}/src/main/webapp/components</inputDir>
|
||||
<includes>
|
||||
<include>**/*.css</include>
|
||||
</includes>
|
||||
<aggregations>
|
||||
<aggregation>
|
||||
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
|
||||
<removeIncluded>false</removeIncluded>
|
||||
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.css</outputFile>
|
||||
</aggregation>
|
||||
</aggregations>
|
||||
</resourcesSet>
|
||||
</resourcesSets>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
128
pom.xml
128
pom.xml
|
@ -28,6 +28,12 @@
|
|||
- mvn test -DskipTests=false -DargLine="-Dsystem.test.server=http://localhost:8080/plantuml"
|
||||
-->
|
||||
<skipTests>true</skipTests>
|
||||
<!--
|
||||
JS and CSS compression / minify
|
||||
If false minify is enabled.
|
||||
Dev Tipp: set to `true` and run `mvn fizzed-watcher:run` while developing the frontend
|
||||
-->
|
||||
<withoutCSSJSCompress>false</withoutCSSJSCompress>
|
||||
|
||||
<!--
|
||||
This artifact is required for:
|
||||
|
@ -75,7 +81,7 @@
|
|||
<junit.version>5.9.3</junit.version>
|
||||
<junit-suite.version>1.9.3</junit-suite.version>
|
||||
<selenium.version>4.9.1</selenium.version>
|
||||
<selenium-webdrivermanager.version>5.3.2</selenium-webdrivermanager.version>
|
||||
<selenium-webdrivermanager.version>5.3.3</selenium-webdrivermanager.version>
|
||||
<commons-io.version>2.11.0</commons-io.version>
|
||||
<jetty-server.version>${jetty.version}</jetty-server.version>
|
||||
|
||||
|
@ -93,7 +99,7 @@
|
|||
<maven-site-plugin.version>3.12.1</maven-site-plugin.version>
|
||||
<maven-project-info-reports-plugin.version>3.4.3</maven-project-info-reports-plugin.version>
|
||||
<maven-checkstyle-plugin.version>3.2.2</maven-checkstyle-plugin.version>
|
||||
<checkstyle.version>10.10.0</checkstyle.version>
|
||||
<checkstyle.version>10.11.0</checkstyle.version>
|
||||
|
||||
<!-- plugins -->
|
||||
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
|
||||
|
@ -101,6 +107,8 @@
|
|||
<jetty-maven-plugin.version>${jetty.version}</jetty-maven-plugin.version>
|
||||
<duplicate-finder-maven-plugin.version>1.5.1</duplicate-finder-maven-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.5.0</maven-javadoc-plugin.version>
|
||||
<resources-optimizer-maven-plugin.version>2.5.6</resources-optimizer-maven-plugin.version>
|
||||
<fizzed-watcher-maven-plugin.verson>1.0.6</fizzed-watcher-maven-plugin.verson>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
@ -173,7 +181,7 @@
|
|||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
|
@ -451,6 +459,120 @@
|
|||
</webResources>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>clean-minified-resources</id>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<excludeDefaultDirectories>true</excludeDefaultDirectories>
|
||||
<filesets>
|
||||
<fileset>
|
||||
<directory>${basedir}/src/main/webapp/min</directory>
|
||||
</fileset>
|
||||
</filesets>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.primefaces.extensions</groupId>
|
||||
<artifactId>resources-optimizer-maven-plugin</artifactId>
|
||||
<version>${resources-optimizer-maven-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>optimize</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>optimize</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<warningLevel>DEFAULT</warningLevel>
|
||||
<failOnWarning>true</failOnWarning>
|
||||
<suffix>.min</suffix>
|
||||
<languageIn>ECMASCRIPT_2020</languageIn>
|
||||
<languageOut>ECMASCRIPT5_STRICT</languageOut>
|
||||
<emitUseStrict>true</emitUseStrict>
|
||||
<resourcesSets>
|
||||
<resourcesSet>
|
||||
<inputDir>${basedir}/src/main/webapp</inputDir>
|
||||
<includes>
|
||||
<include>components/**/*.js</include>
|
||||
<include>js/**/*.js</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>js/language/**</exclude>
|
||||
</excludes>
|
||||
<aggregations>
|
||||
<aggregation>
|
||||
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
|
||||
<removeIncluded>false</removeIncluded>
|
||||
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.js</outputFile>
|
||||
</aggregation>
|
||||
</aggregations>
|
||||
</resourcesSet>
|
||||
<resourcesSet>
|
||||
<inputDir>${basedir}/src/main/webapp/js/language</inputDir>
|
||||
<includes>
|
||||
<include>language.js</include>
|
||||
<include>validation/validation.js</include>
|
||||
<include>**/*.js</include>
|
||||
</includes>
|
||||
<aggregations>
|
||||
<aggregation>
|
||||
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
|
||||
<removeIncluded>false</removeIncluded>
|
||||
<outputFile>${basedir}/src/main/webapp/min/plantuml-language.min.js</outputFile>
|
||||
</aggregation>
|
||||
</aggregations>
|
||||
</resourcesSet>
|
||||
<resourcesSet>
|
||||
<inputDir>${basedir}/src/main/webapp/components</inputDir>
|
||||
<includes>
|
||||
<include>**/*.css</include>
|
||||
</includes>
|
||||
<aggregations>
|
||||
<aggregation>
|
||||
<withoutCompress>${withoutCSSJSCompress}</withoutCompress>
|
||||
<removeIncluded>false</removeIncluded>
|
||||
<outputFile>${basedir}/src/main/webapp/min/plantuml.min.css</outputFile>
|
||||
</aggregation>
|
||||
</aggregations>
|
||||
</resourcesSet>
|
||||
</resourcesSets>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.fizzed</groupId>
|
||||
<artifactId>fizzed-watcher-maven-plugin</artifactId>
|
||||
<version>${fizzed-watcher-maven-plugin.verson}</version>
|
||||
<configuration>
|
||||
<watches>
|
||||
<watch>
|
||||
<directory>${basedir}/src/main/webapp/components</directory>
|
||||
<recursive>true</recursive>
|
||||
<includes>
|
||||
<include>*.js</include>
|
||||
<include>*.css</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>*.min.js</exclude>
|
||||
<exclude>*.min.css</exclude>
|
||||
</excludes>
|
||||
</watch>
|
||||
</watches>
|
||||
<goals>
|
||||
<goal>clean:clean@clean-minified-resources</goal>
|
||||
<goal>org.primefaces.extensions:resources-optimizer-maven-plugin:optimize</goal>
|
||||
</goals>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.7 KiB |
|
@ -8,7 +8,7 @@
|
|||
<meta name="color-scheme" content="light dark" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon"/>
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
|
||||
<link rel="stylesheet" href="plantuml.css" />
|
||||
<link rel="stylesheet" href="min/plantuml.min.css" />
|
||||
<script src="min/plantuml.min.js"></script>
|
||||
<script src="min/plantuml-language.min.js"></script>
|
||||
<script src="webjars/monaco-editor/0.36.1/min/vs/loader.js"></script>
|
||||
<script src="plantumllanguage.js"></script>
|
||||
<script src="plantuml.js"></script>
|
|
@ -0,0 +1,165 @@
|
|||
/**********************************
|
||||
* PlantUML Server Application CSS *
|
||||
***********************************/
|
||||
|
||||
/************* variables *************/
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--font-color: black;
|
||||
--font-color-disabled: #888;
|
||||
--bg-color: white;
|
||||
--border-color: #ccc;
|
||||
--border-color-2: #aaa;
|
||||
--footer-font-color: #666;
|
||||
--footer-bg-color: #eee;
|
||||
--modal-bg-color: #fefefe;
|
||||
--file-drop-color: #eee;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--font-color: #ccc;
|
||||
--font-color-disabled: #777;
|
||||
--bg-color: #212121;
|
||||
--border-color: #848484;
|
||||
--border-color-2: #aaa;
|
||||
--footer-font-color: #ccc;
|
||||
--footer-bg-color: black;
|
||||
--modal-bg-color: #424242;
|
||||
--file-drop-color: #212121;
|
||||
}
|
||||
|
||||
/************* default settings *************/
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html {
|
||||
font-family: arial,helvetica,sans-serif;
|
||||
}
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
overflow: auto;
|
||||
}
|
||||
@media screen and (min-width: 900px) {
|
||||
body {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
input:not([type="image"]) {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
}
|
||||
input[type="file"]::file-selector-button {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
}
|
||||
select {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
/************* ruler *************/
|
||||
.hr {
|
||||
padding: 1rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
.flex-columns > .hr {
|
||||
padding: 0 1rem;
|
||||
width: initial;
|
||||
height: 100%;
|
||||
}
|
||||
.hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
background-color: var(--border-color);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 3px;
|
||||
min-width: 3px;
|
||||
}
|
||||
|
||||
/************* wait cursor *************/
|
||||
.wait {
|
||||
cursor: wait;
|
||||
}
|
||||
.wait > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/************* flex rows and columns *************/
|
||||
.flex-columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-main {
|
||||
flex: 1 1 1px;
|
||||
overflow: auto;
|
||||
}
|
||||
.flex-columns > *, .flex-rows > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/*******************************************************************/
|
||||
/************* header, main, footer *************/
|
||||
.header {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.main {
|
||||
margin: 1% 5%;
|
||||
z-index: 1;
|
||||
}
|
||||
.main > div {
|
||||
margin: 0 1.75%;
|
||||
}
|
||||
.main > div:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.main > div:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.main {
|
||||
display: block;
|
||||
overflow: inherit;
|
||||
}
|
||||
.main > div {
|
||||
margin: 1.75% 0;
|
||||
}
|
||||
.main > div:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.main > div:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.footer p {
|
||||
background-color: var(--footer-bg-color);
|
||||
color: var(--footer-font-color);
|
||||
font-size: 0.7em;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*******************************************************************/
|
||||
/************* color themes *************/
|
||||
[data-theme="dark"] img:not(#diagram-png):not(.no-filter) {
|
||||
filter: invert() contrast(30%);
|
||||
}
|
||||
[data-theme="dark"] input[type="image"] {
|
||||
filter: invert() contrast(30%);
|
||||
}
|
||||
[data-theme="dark"] a {
|
||||
color: white;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*********************************
|
||||
* PlantUML Server Application JS *
|
||||
**********************************/
|
||||
"use strict";
|
||||
|
||||
async function initApp() {
|
||||
const view = new URL(window.location.href).searchParams.get("view")?.toLowerCase();
|
||||
|
||||
function initializeAppData() {
|
||||
const analysedUrl = analyseUrl(window.location.href);
|
||||
const code = document.editor?.getValue();
|
||||
document.appData = Object.assign({}, window.opener?.document.appData);
|
||||
if (Object.keys(document.appData).length === 0) {
|
||||
document.appData = {
|
||||
encodedDiagram: analysedUrl.encodedDiagram,
|
||||
index: analysedUrl.index,
|
||||
numberOfDiagramPages: (code) ? getNumberOfDiagramPagesFromCode(code) : 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await initEditor(view);
|
||||
initializeAppData();
|
||||
initTheme();
|
||||
initAppCommunication();
|
||||
await initPreview(view);
|
||||
initModals(view);
|
||||
|
||||
if (document.editor) {
|
||||
document.editor.focus();
|
||||
if (document.appData.encodedDiagram == "SyfFKj2rKt3CoKnELR1Io4ZDoSa70000") {
|
||||
// if default `Bob -> Alice : hello` example mark example code for faster editing
|
||||
document.editor.setSelection({
|
||||
startLineNumber: 2,
|
||||
endLineNumber: 2,
|
||||
startColumn: 1,
|
||||
endColumn: 21,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.appConfig.autoRefreshState = "complete";
|
||||
}
|
||||
|
||||
// main entry
|
||||
window.onload = initApp;
|
|
@ -0,0 +1,28 @@
|
|||
/*************
|
||||
* Editor CSS *
|
||||
**************/
|
||||
|
||||
.editor {
|
||||
border: 3px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.editor {
|
||||
height: 20em;
|
||||
}
|
||||
}
|
||||
.editor .monaco-editor-container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#monaco-editor {
|
||||
height: 100%;
|
||||
}
|
||||
/* Hack to display the icons and emojis in the auto completion documentation in a visible size.
|
||||
* (see PlantUmlLanguageFeatures.register{Icon,Emoji}Completion) */
|
||||
#monaco-editor .overlayWidgets .suggest-details p img[alt="icon"],
|
||||
#monaco-editor .overlayWidgets .suggest-details p img[alt="emoji"] {
|
||||
height: 1.2rem;
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/************
|
||||
* Editor JS *
|
||||
*************/
|
||||
|
||||
const { setEditorValue, initEditor } = (function() {
|
||||
function setEditorValue(
|
||||
editor,
|
||||
text,
|
||||
{ suppressEditorChangedMessage=false, forceMoveMarkers=undefined } = {}
|
||||
) {
|
||||
if (suppressEditorChangedMessage && editor === document.editor) {
|
||||
suppressNextMessage("editor");
|
||||
}
|
||||
// replace editor value but preserve undo stack
|
||||
editor.executeEdits("", [{ range: editor.getModel().getFullModelRange(), text, forceMoveMarkers }]);
|
||||
}
|
||||
|
||||
async function initEditor(view) {
|
||||
function loadMonacoCodeEditorAsync() {
|
||||
return new Promise((resolve, _reject) => {
|
||||
require.config({ paths: { vs: "webjars/monaco-editor/0.36.1/min/vs" } });
|
||||
require(["vs/editor/editor.main"], resolve);
|
||||
});
|
||||
}
|
||||
function createEditorModel() {
|
||||
let plantumlFeatures;
|
||||
function onPlantumlEditorContentChanged(code, sender=undefined, broadcastChanges=true) {
|
||||
function broadcastCodeEditorChanges() {
|
||||
document.appConfig.autoRefreshState = "started";
|
||||
const numberOfDiagramPages = getNumberOfDiagramPagesFromCode(code);
|
||||
let index = document.appData.index;
|
||||
if (index === undefined || numberOfDiagramPages === 1) {
|
||||
index = undefined;
|
||||
} else if (index >= numberOfDiagramPages) {
|
||||
index = numberOfDiagramPages - 1;
|
||||
}
|
||||
makeRequest("POST", "coder", { data: code }).then((encodedDiagram) => {
|
||||
sendMessage({
|
||||
sender,
|
||||
data: { encodedDiagram, numberOfDiagramPages, index },
|
||||
synchronize: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
const updatePlantumlLanguageMarkers = (function() {
|
||||
return function() {
|
||||
const model = document.editor.getModel();
|
||||
plantumlFeatures = plantumlFeatures || new PlantUmlLanguageFeatures();
|
||||
plantumlFeatures.validateCode(model)
|
||||
.then(markers => monaco.editor.setModelMarkers(model, "plantuml", markers));
|
||||
}
|
||||
})();
|
||||
if (sender && broadcastChanges) broadcastCodeEditorChanges();
|
||||
updatePlantumlLanguageMarkers();
|
||||
}
|
||||
function getInitPlantumlCodeAndRemoveElement() {
|
||||
const initCodeEl = document.getElementById("initCode");
|
||||
const initCode = initCodeEl.value;
|
||||
initCodeEl.remove();
|
||||
return initCode;
|
||||
}
|
||||
// create editor model
|
||||
const model = monaco.editor.createModel(
|
||||
getInitPlantumlCodeAndRemoveElement(),
|
||||
"apex",
|
||||
monaco.Uri.parse("inmemory://plantuml")
|
||||
);
|
||||
// create editor model watcher
|
||||
let timer = 0;
|
||||
model.onDidChangeContent(() => {
|
||||
clearTimeout(timer);
|
||||
document.appConfig.autoRefreshState = "waiting";
|
||||
timer = setTimeout(
|
||||
() => onPlantumlEditorContentChanged(model.getValue(), "editor"),
|
||||
document.appConfig.editorWatcherTimeout
|
||||
);
|
||||
});
|
||||
return model;
|
||||
}
|
||||
function getDefaultStorageService() {
|
||||
// create own storage service to expand suggestion documentation by default
|
||||
return {
|
||||
get() {},
|
||||
getBoolean(key) { return key === "expandSuggestionDocs"; },
|
||||
getNumber() { return 0; },
|
||||
remove() {},
|
||||
store() {},
|
||||
onWillSaveState() {},
|
||||
onDidChangeStorage() {},
|
||||
onDidChangeValue() {},
|
||||
};
|
||||
}
|
||||
|
||||
// load monaco editor requirements
|
||||
await loadMonacoCodeEditorAsync();
|
||||
if (view !== "previewer") {
|
||||
// create editor
|
||||
const model = createEditorModel();
|
||||
const storageService = getDefaultStorageService();
|
||||
document.editor = monaco.editor.create(document.getElementById("monaco-editor"), {
|
||||
model, ...document.appConfig.editorCreateOptions
|
||||
}, { storageService });
|
||||
// sometimes the monaco editor has resize problems
|
||||
document.addEventListener("resize", () => document.editor.layout());
|
||||
// init editor components
|
||||
initEditorUrlInput();
|
||||
initEditorMenu();
|
||||
}
|
||||
}
|
||||
|
||||
return { setEditorValue, initEditor };
|
||||
})();
|
|
@ -0,0 +1,10 @@
|
|||
<div id="editor-main-container" class="editor flex-main flex-rows">
|
||||
<div>
|
||||
<%@ include file="/components/editor/url-input/editor-url-input.jsp" %>
|
||||
</div>
|
||||
<div class="flex-main monaco-editor-container">
|
||||
<textarea id="initCode" name="initCode" style="display: none;"><%= net.sourceforge.plantuml.servlet.PlantUmlServlet.stringToHTMLString(decoded) %></textarea>
|
||||
<div id="monaco-editor"></div>
|
||||
<%@ include file="/components/editor/menu/editor-menu.jsp" %>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,92 @@
|
|||
/******************
|
||||
* Editor Menu CSS *
|
||||
*******************/
|
||||
|
||||
.monaco-editor-container .editor-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.monaco-editor-container .editor-menu .menu-item {
|
||||
display: none;
|
||||
margin: 1rem 0;
|
||||
height: 1.75rem;
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
-webkit-animation-name: editor-menu-animateitem;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: editor-menu-animateitem;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
@-webkit-keyframes editor-menu-animateitem {
|
||||
from { top: -50%; opacity: 0; }
|
||||
to { top: 0; opacity: 0.5; }
|
||||
}
|
||||
@keyframes editor-menu-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;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*****************
|
||||
* Editor Menu JS *
|
||||
******************/
|
||||
|
||||
function initEditorMenu() {
|
||||
function copyCodeToClipboard() {
|
||||
const range = document.editor.getModel().getFullModelRange();
|
||||
document.editor.focus();
|
||||
document.editor.setSelection(range);
|
||||
const code = document.editor.getValue();
|
||||
navigator.clipboard?.writeText(code).catch(() => {});
|
||||
}
|
||||
// add listener
|
||||
document.getElementById("menu-item-editor-code-copy").addEventListener("click", copyCodeToClipboard);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<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
|
||||
id="menu-item-editor-code-copy"
|
||||
class="menu-item"
|
||||
type="image"
|
||||
alt="copy"
|
||||
title="Copy code"
|
||||
src="assets/actions/copy.svg"
|
||||
/>
|
||||
<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>
|
|
@ -0,0 +1,29 @@
|
|||
/***********************
|
||||
* Editor URL Input CSS *
|
||||
************************/
|
||||
|
||||
.editor .btn-input {
|
||||
align-items: center;
|
||||
border-bottom: 3px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.editor .btn-input input[type=text] {
|
||||
border: 0;
|
||||
flex: 1 1 1px;
|
||||
font-family: monospace;
|
||||
font-size: medium;
|
||||
padding: 0.2em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.editor .btn-input input[type=text]:focus {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
.editor .btn-input input[type="image"] {
|
||||
height: 1rem;
|
||||
margin-left: 0.7em;
|
||||
padding: 0 0.3em;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/**********************
|
||||
* Editor URL Input JS *
|
||||
***********************/
|
||||
|
||||
const { setUrlValue, initEditorUrlInput } = (function() {
|
||||
function setUrlValue(
|
||||
url=undefined,
|
||||
{ encodedDiagram=undefined, index=undefined } = {},
|
||||
{ suppressEditorChangedMessage=false } = {}
|
||||
) {
|
||||
if (!url && !encodedDiagram) return;
|
||||
if (suppressEditorChangedMessage) {
|
||||
suppressNextMessage("url");
|
||||
}
|
||||
document.getElementById("url").value = url ? url : resolvePath(buildUrl("png", encodedDiagram, index));
|
||||
}
|
||||
|
||||
function initEditorUrlInput() {
|
||||
const input = document.getElementById("url");
|
||||
|
||||
function copyUrlToClipboard() {
|
||||
input.focus();
|
||||
input.select();
|
||||
navigator.clipboard?.writeText(input.value).catch(() => {});
|
||||
}
|
||||
async function onInputChanged(event) {
|
||||
document.appConfig.autoRefreshState = "started";
|
||||
event.target.title = event.target.value;
|
||||
const analysedUrl = analyseUrl(event.target.value);
|
||||
// decode diagram (server request)
|
||||
const code = await makeRequest("GET", "coder/" + analysedUrl.encodedDiagram);
|
||||
// change editor content without sending the editor change message
|
||||
setEditorValue(document.editor, code, { suppressEditorChangedMessage: true });
|
||||
sendMessage({
|
||||
sender: "url",
|
||||
data: {
|
||||
encodedDiagram: analysedUrl.encodedDiagram,
|
||||
index: analysedUrl.index,
|
||||
},
|
||||
synchronize: true,
|
||||
});
|
||||
}
|
||||
|
||||
// resolve relative path inside url input once
|
||||
setUrlValue(resolvePath(input.value));
|
||||
// update editor and everything else if the URL input is changed
|
||||
input.addEventListener("change", onInputChanged);
|
||||
// add listener
|
||||
document.getElementById("url-copy-btn").addEventListener("click", copyUrlToClipboard);
|
||||
}
|
||||
|
||||
return { setUrlValue, initEditorUrlInput };
|
||||
})();
|
|
@ -0,0 +1,4 @@
|
|||
<div class="btn-input">
|
||||
<input id="url" type="text" name="url" value="png/<%= diagramUrl %>" />
|
||||
<input id="url-copy-btn" type="image" alt="copy" src="assets/actions/copy.svg" tabindex="-1" />
|
||||
</div>
|
|
@ -1 +1 @@
|
|||
<p><%= net.sourceforge.plantuml.version.Version.fullDescription() %></p>
|
||||
<p><%= net.sourceforge.plantuml.version.Version.fullDescription() %></p>
|
|
@ -0,0 +1,17 @@
|
|||
<div>
|
||||
<img
|
||||
style="display: inline; position: absolute; top: 0; right: 0; border: 0; max-width: 25%;"
|
||||
class="no-filter"
|
||||
src="assets/github-fork-me.svg"
|
||||
alt="Fork me on GitHub"
|
||||
usemap="#github-banner"
|
||||
/>
|
||||
<map id="github-banner" name="github-banner" style="cursor: pointer;">
|
||||
<area
|
||||
shape="poly"
|
||||
coords="10,0 50,0 149,100 149,140"
|
||||
href="https://github.com/plantuml/plantuml-server"
|
||||
alt="Fork me on GitHub"
|
||||
/>
|
||||
</map>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<h1>PlantUML Server</h1>
|
||||
<% if (showSocialButtons) { %>
|
||||
<%@ include file="/components/header/social-buttons.jsp" %>
|
||||
<% } %>
|
||||
<% if (showGithubRibbon) { %>
|
||||
<%@ include file="/components/header/github-ribbon.jsp" %>
|
||||
<% } %>
|
||||
<p>Create your <a href="https://plantuml.com">PlantUML</a> diagrams directly in your browser!</p>
|
|
@ -0,0 +1 @@
|
|||
<!-- Insert here the html code of your social buttons -->
|
|
@ -0,0 +1,7 @@
|
|||
/*********************
|
||||
* Diagram Export CSS *
|
||||
**********************/
|
||||
|
||||
#diagram-export.modal .label-input-pair label {
|
||||
min-width: 8rem;
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/********************
|
||||
* Diagram Export JS *
|
||||
*********************/
|
||||
|
||||
function initDiagramExport() {
|
||||
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);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*********************
|
||||
* Diagram Import CSS *
|
||||
**********************/
|
||||
|
||||
#diagram-import p.error-message {
|
||||
color: darkred;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#diagram-import input[type="file"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0.2rem dashed var(--border-color);
|
||||
border-radius: 0.4rem;
|
||||
box-sizing: border-box;
|
||||
padding: 5rem 2rem;
|
||||
}
|
||||
#diagram-import input[type="file"],
|
||||
#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);
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/********************
|
||||
* Diagram Import JS *
|
||||
*********************/
|
||||
|
||||
function initDiagramImport() {
|
||||
const dialogElement = document.getElementById("diagram-import");
|
||||
const fileInput = document.getElementById("diagram-import-input");
|
||||
const okButton = document.getElementById("diagram-import-ok-btn");
|
||||
const errorMessageElement = document.getElementById("diagram-import-error-message");
|
||||
|
||||
function openDialog(isOpenManually = true) {
|
||||
setVisibility(dialogElement, true, true);
|
||||
dialogElement.dataset.isOpenManually = isOpenManually.toString();
|
||||
// reset or clear file input
|
||||
fileInput.value = "";
|
||||
onFileInputChange(fileInput);
|
||||
}
|
||||
function closeDialog() {
|
||||
fileInput.value = ""; // reset or clear
|
||||
onFileInputChange(fileInput);
|
||||
dialogElement.removeAttribute("data-is-open-manually");
|
||||
setVisibility(dialogElement, false);
|
||||
}
|
||||
|
||||
function onFileInputChange(fileInput) {
|
||||
errorMessageElement.innerText = "";
|
||||
okButton.disabled = fileInput.files?.length < 1;
|
||||
}
|
||||
|
||||
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) {
|
||||
setEditorValue(document.editor, code);
|
||||
}
|
||||
function requestMetadata(file) {
|
||||
const fd = new FormData();
|
||||
fd.append("diagram", file, file.name);
|
||||
return makeRequest("POST", "metadata", {
|
||||
data: fd,
|
||||
responseType: "json",
|
||||
headers: { "Accept": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
dialogElement.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(() => closeDialog(), () => {}).finally(() => dialogElement.classList.remove("wait"));
|
||||
}
|
||||
|
||||
function onGlobalDragEnter(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (!isVisible(dialogElement)) {
|
||||
openDialog(false);
|
||||
}
|
||||
}
|
||||
|
||||
function onFileInputDragOver(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer !== null) {
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
}
|
||||
function onFileInputDrop(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 (dialogElement.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
|
||||
fileInput.addEventListener("dragenter", event => event.target.classList.add("drop-able"), false);
|
||||
fileInput.addEventListener("dragover", onFileInputDragOver, false);
|
||||
fileInput.addEventListener("dragexit", event => event.target.classList.remove("drop-able"), false);
|
||||
fileInput.addEventListener("drop", onFileInputDrop, false);
|
||||
fileInput.addEventListener("change", event => onFileInputChange(event.target));
|
||||
// ok button
|
||||
okButton.addEventListener("click", () => {
|
||||
const file = fileInput.files[0]; // should be always a valid file
|
||||
importDiagram(file, checkFileLocally(file)); // otherwise button should be disabled
|
||||
});
|
||||
// register model listeners
|
||||
registerModalListener("diagram-import", openDialog, closeDialog);
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
<div class="hr"></div>
|
||||
</div>
|
||||
<div class="modal-main flex-main">
|
||||
<input id="diagram-import-input" type="file" name="diagram" onchange="onDiagramImportInputChange(this);" />
|
||||
<input id="diagram-import-input" type="file" name="diagram" />
|
||||
<p id="diagram-import-error-message" class="error-message"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
|
@ -0,0 +1,95 @@
|
|||
/*************
|
||||
* Modals CSS *
|
||||
**************/
|
||||
|
||||
.modal {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
padding: 5%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
background-color: rgb(0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.modal .modal-content {
|
||||
background-color: var(--modal-bg-color);
|
||||
margin: auto;
|
||||
padding: 2rem;
|
||||
border: 3px solid var(--border-color);
|
||||
max-width: 30rem;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
-webkit-animation-name: modal-animatetop;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: modal-animatetop;
|
||||
animation-duration: 0.4s;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
@-webkit-keyframes modal-animatetop {
|
||||
from { top: -50%; opacity: 0; }
|
||||
to { top: 50%; opacity: 1; }
|
||||
}
|
||||
@keyframes modal-animatetop {
|
||||
from { top: -50%; opacity: 0; }
|
||||
to { top: 50%; opacity: 1; }
|
||||
}
|
||||
/************* header, main, footer *************/
|
||||
.modal .modal-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.modal .modal-main {
|
||||
flex: 1;
|
||||
}
|
||||
.modal .modal-footer {
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
/************* inputs *************/
|
||||
.modal input, .modal select {
|
||||
border: 1px solid var(--border-color);
|
||||
|