├── .gitignore ├── .travis.yml ├── README.md ├── build.sh ├── dist ├── css │ └── bootstrap.min.css ├── index.html └── js │ ├── full.render.js │ └── viz.js ├── elm.json ├── src ├── BoundingBox.elm ├── Canvas.elm ├── Data │ └── Layout.elm ├── Export.elm ├── GraphUtil.elm ├── GraphViz │ └── VizJs.elm ├── Main.elm ├── Ports.elm ├── SvgMouse.elm ├── Types.elm └── View.elm └── tests └── VizJs.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | app.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elm 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elm graph editor 2 | 3 | Simple editor for creating graphs implemented purely in Elm. 4 | See it [in action](http://janhrcek.cz/graph-editor/)! 5 | 6 | # Current features 7 | - [x] The editor has 3 modes, which determine what user interactions are doing with the graph 8 | - [x] In *Create/Edit* mode you can 9 | - [x] Create new nodes by clicking on the canvas (double click to immediately start editing node text). 10 | - [x] Edit node text by double clicking node. Enter confirms the edit. 11 | - [x] Create new edges by click & holding mouse button on initial node and dropping on target node. 12 | - [x] Edit edge text by double clicking edges. Enter confirms the edit. 13 | - [x] In *Layout* mode you can 14 | - [x] move nodes on the canvas using drag and drop. 15 | - [ ] reattach edges to different nodes by dragging node arrowheads 16 | - [x] get nodes arranged automatically using one of the supported [GraphViz](https://graphviz.gitlab.io/)'s layout engines 17 | - [x] bring nodes closer/further from each other in their current arrangement 18 | - [x] In *Delete* mode you can remove nodes and edges by clicking them. 19 | - [x] Help button that shows/hides info about how users can create/edit graphs 20 | - [x] Export graph in different formats 21 | - [x] [TGF](https://en.wikipedia.org/wiki/Trivial_Graph_Format) 22 | - [x] [DOT](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) 23 | 24 | # Upcoming Features 25 | - [ ] Ability to save / load multiple graphs in local storage 26 | 27 | # TODOs 28 | - [ ] Add mode dependent SVG cursors to make semantics of mouse actions clearer 29 | 30 | ## Start development server server 31 | 32 | You can start the app in development mode using [elm-live](https://github.com/wking-io/elm-live) command: 33 | 34 | ``bash 35 | elm-live --open --dir=dist -- src/Main.elm --output=dist/js/app.js 36 | ``` 37 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | OUT=dist/js/app.js 4 | 5 | elm make src/Main.elm --output=$OUT --optimize 6 | 7 | uglifyjs $OUT --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \ 8 | | uglifyjs --mangle --output $OUT 9 | -------------------------------------------------------------------------------- /dist/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::after,.was-validated .custom-file-input:valid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::after,.was-validated .custom-file-input:invalid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-label::after{border-color:#80bdff}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:not(:disabled):not(.disabled){cursor:pointer}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{color:#000;text-decoration:none;opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-dialog-centered::before{display:block;height:calc(100vh - (.5rem * 2));content:""}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-dialog-centered::before{height:calc(100vh - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-item-next,.carousel-item-prev,.carousel-item.active{transition:none}}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-fade .carousel-item{opacity:0;transition-duration:.6s;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} 7 | /*# sourceMappingURL=bootstrap.min.css.map */ -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Elm Graph Editor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /dist/js/viz.js: -------------------------------------------------------------------------------- 1 | /* 2 | Viz.js 2.1.2 (Graphviz 2.40.1, Expat 2.2.5, Emscripten 1.37.36) 3 | Copyright (c) 2014-2018 Michael Daines 4 | Licensed under MIT license 5 | 6 | This distribution contains other software in object code form: 7 | 8 | Graphviz 9 | Licensed under Eclipse Public License - v 1.0 10 | http://www.graphviz.org 11 | 12 | Expat 13 | Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd and Clark Cooper 14 | Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers. 15 | Licensed under MIT license 16 | http://www.libexpat.org 17 | 18 | zlib 19 | Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler 20 | http://www.zlib.net/zlib_license.html 21 | */ 22 | (function (global, factory) { 23 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 24 | typeof define === 'function' && define.amd ? define(factory) : 25 | (global.Viz = factory()); 26 | }(this, (function () { 'use strict'; 27 | 28 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 29 | return typeof obj; 30 | } : function (obj) { 31 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 32 | }; 33 | 34 | var classCallCheck = function (instance, Constructor) { 35 | if (!(instance instanceof Constructor)) { 36 | throw new TypeError("Cannot call a class as a function"); 37 | } 38 | }; 39 | 40 | var createClass = function () { 41 | function defineProperties(target, props) { 42 | for (var i = 0; i < props.length; i++) { 43 | var descriptor = props[i]; 44 | descriptor.enumerable = descriptor.enumerable || false; 45 | descriptor.configurable = true; 46 | if ("value" in descriptor) descriptor.writable = true; 47 | Object.defineProperty(target, descriptor.key, descriptor); 48 | } 49 | } 50 | 51 | return function (Constructor, protoProps, staticProps) { 52 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 53 | if (staticProps) defineProperties(Constructor, staticProps); 54 | return Constructor; 55 | }; 56 | }(); 57 | 58 | var _extends = Object.assign || function (target) { 59 | for (var i = 1; i < arguments.length; i++) { 60 | var source = arguments[i]; 61 | 62 | for (var key in source) { 63 | if (Object.prototype.hasOwnProperty.call(source, key)) { 64 | target[key] = source[key]; 65 | } 66 | } 67 | } 68 | 69 | return target; 70 | }; 71 | 72 | var WorkerWrapper = function () { 73 | function WorkerWrapper(worker) { 74 | var _this = this; 75 | 76 | classCallCheck(this, WorkerWrapper); 77 | 78 | this.worker = worker; 79 | this.listeners = []; 80 | this.nextId = 0; 81 | 82 | this.worker.addEventListener('message', function (event) { 83 | var id = event.data.id; 84 | var error = event.data.error; 85 | var result = event.data.result; 86 | 87 | _this.listeners[id](error, result); 88 | delete _this.listeners[id]; 89 | }); 90 | } 91 | 92 | createClass(WorkerWrapper, [{ 93 | key: 'render', 94 | value: function render(src, options) { 95 | var _this2 = this; 96 | 97 | return new Promise(function (resolve, reject) { 98 | var id = _this2.nextId++; 99 | 100 | _this2.listeners[id] = function (error, result) { 101 | if (error) { 102 | reject(new Error(error.message, error.fileName, error.lineNumber)); 103 | return; 104 | } 105 | resolve(result); 106 | }; 107 | 108 | _this2.worker.postMessage({ id: id, src: src, options: options }); 109 | }); 110 | } 111 | }]); 112 | return WorkerWrapper; 113 | }(); 114 | 115 | var ModuleWrapper = function ModuleWrapper(module, render) { 116 | classCallCheck(this, ModuleWrapper); 117 | 118 | var instance = module(); 119 | this.render = function (src, options) { 120 | return new Promise(function (resolve, reject) { 121 | try { 122 | resolve(render(instance, src, options)); 123 | } catch (error) { 124 | reject(error); 125 | } 126 | }); 127 | }; 128 | }; 129 | 130 | // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 131 | 132 | 133 | function b64EncodeUnicode(str) { 134 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { 135 | return String.fromCharCode('0x' + p1); 136 | })); 137 | } 138 | 139 | function defaultScale() { 140 | if ('devicePixelRatio' in window && window.devicePixelRatio > 1) { 141 | return window.devicePixelRatio; 142 | } else { 143 | return 1; 144 | } 145 | } 146 | 147 | function svgXmlToImageElement(svgXml) { 148 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 149 | _ref$scale = _ref.scale, 150 | scale = _ref$scale === undefined ? defaultScale() : _ref$scale, 151 | _ref$mimeType = _ref.mimeType, 152 | mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType, 153 | _ref$quality = _ref.quality, 154 | quality = _ref$quality === undefined ? 1 : _ref$quality; 155 | 156 | return new Promise(function (resolve, reject) { 157 | var svgImage = new Image(); 158 | 159 | svgImage.onload = function () { 160 | var canvas = document.createElement('canvas'); 161 | canvas.width = svgImage.width * scale; 162 | canvas.height = svgImage.height * scale; 163 | 164 | var context = canvas.getContext("2d"); 165 | context.drawImage(svgImage, 0, 0, canvas.width, canvas.height); 166 | 167 | canvas.toBlob(function (blob) { 168 | var image = new Image(); 169 | image.src = URL.createObjectURL(blob); 170 | image.width = svgImage.width; 171 | image.height = svgImage.height; 172 | 173 | resolve(image); 174 | }, mimeType, quality); 175 | }; 176 | 177 | svgImage.onerror = function (e) { 178 | var error; 179 | 180 | if ('error' in e) { 181 | error = e.error; 182 | } else { 183 | error = new Error('Error loading SVG'); 184 | } 185 | 186 | reject(error); 187 | }; 188 | 189 | svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml); 190 | }); 191 | } 192 | 193 | function svgXmlToImageElementFabric(svgXml) { 194 | var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 195 | _ref2$scale = _ref2.scale, 196 | scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale, 197 | _ref2$mimeType = _ref2.mimeType, 198 | mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType, 199 | _ref2$quality = _ref2.quality, 200 | quality = _ref2$quality === undefined ? 1 : _ref2$quality; 201 | 202 | var multiplier = scale; 203 | 204 | var format = void 0; 205 | if (mimeType == 'image/jpeg') { 206 | format = 'jpeg'; 207 | } else if (mimeType == 'image/png') { 208 | format = 'png'; 209 | } 210 | 211 | return new Promise(function (resolve, reject) { 212 | fabric.loadSVGFromString(svgXml, function (objects, options) { 213 | // If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one element back even given an empty graph, so we will assume an error in this case. 214 | if (objects.length == 0) { 215 | reject(new Error('Error loading SVG with Fabric')); 216 | } 217 | 218 | var element = document.createElement("canvas"); 219 | element.width = options.width; 220 | element.height = options.height; 221 | 222 | var canvas = new fabric.Canvas(element, { enableRetinaScaling: false }); 223 | var obj = fabric.util.groupSVGElements(objects, options); 224 | canvas.add(obj).renderAll(); 225 | 226 | var image = new Image(); 227 | image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality }); 228 | image.width = options.width; 229 | image.height = options.height; 230 | 231 | resolve(image); 232 | }); 233 | }); 234 | } 235 | 236 | var Viz = function () { 237 | function Viz() { 238 | var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 239 | workerURL = _ref3.workerURL, 240 | worker = _ref3.worker, 241 | Module = _ref3.Module, 242 | render = _ref3.render; 243 | 244 | classCallCheck(this, Viz); 245 | 246 | if (typeof workerURL !== 'undefined') { 247 | this.wrapper = new WorkerWrapper(new Worker(workerURL)); 248 | } else if (typeof worker !== 'undefined') { 249 | this.wrapper = new WorkerWrapper(worker); 250 | } else if (typeof Module !== 'undefined' && typeof render !== 'undefined') { 251 | this.wrapper = new ModuleWrapper(Module, render); 252 | } else if (typeof Viz.Module !== 'undefined' && typeof Viz.render !== 'undefined') { 253 | this.wrapper = new ModuleWrapper(Viz.Module, Viz.render); 254 | } else { 255 | throw new Error('Must specify workerURL or worker option, Module and render options, or include one of full.render.js or lite.render.js after viz.js.'); 256 | } 257 | } 258 | 259 | createClass(Viz, [{ 260 | key: 'renderString', 261 | value: function renderString(src) { 262 | var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 263 | _ref4$format = _ref4.format, 264 | format = _ref4$format === undefined ? 'svg' : _ref4$format, 265 | _ref4$engine = _ref4.engine, 266 | engine = _ref4$engine === undefined ? 'dot' : _ref4$engine, 267 | _ref4$files = _ref4.files, 268 | files = _ref4$files === undefined ? [] : _ref4$files, 269 | _ref4$images = _ref4.images, 270 | images = _ref4$images === undefined ? [] : _ref4$images, 271 | _ref4$yInvert = _ref4.yInvert, 272 | yInvert = _ref4$yInvert === undefined ? false : _ref4$yInvert, 273 | _ref4$nop = _ref4.nop, 274 | nop = _ref4$nop === undefined ? 0 : _ref4$nop; 275 | 276 | for (var i = 0; i < images.length; i++) { 277 | files.push({ 278 | path: images[i].path, 279 | data: '\n\n' 280 | }); 281 | } 282 | 283 | return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop }); 284 | } 285 | }, { 286 | key: 'renderSVGElement', 287 | value: function renderSVGElement(src) { 288 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 289 | 290 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 291 | var parser = new DOMParser(); 292 | return parser.parseFromString(str, 'image/svg+xml').documentElement; 293 | }); 294 | } 295 | }, { 296 | key: 'renderImageElement', 297 | value: function renderImageElement(src) { 298 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 299 | var scale = options.scale, 300 | mimeType = options.mimeType, 301 | quality = options.quality; 302 | 303 | 304 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 305 | if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) { 306 | return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality }); 307 | } else { 308 | return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality }); 309 | } 310 | }); 311 | } 312 | }, { 313 | key: 'renderJSONObject', 314 | value: function renderJSONObject(src) { 315 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 316 | var format = options.format; 317 | 318 | 319 | if (format !== 'json' || format !== 'json0') { 320 | format = 'json'; 321 | } 322 | 323 | return this.renderString(src, _extends({}, options, { format: format })).then(function (str) { 324 | return JSON.parse(str); 325 | }); 326 | } 327 | }]); 328 | return Viz; 329 | }(); 330 | 331 | return Viz; 332 | 333 | }))); 334 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/file": "1.0.5", 12 | "elm/html": "1.0.0", 13 | "elm/json": "1.1.3", 14 | "elm/parser": "1.1.0", 15 | "elm/svg": "1.0.1", 16 | "elm-community/graph": "6.0.0", 17 | "elm-community/intdict": "3.0.0", 18 | "elm-explorations/markdown": "1.0.0" 19 | }, 20 | "indirect": { 21 | "avh4/elm-fifo": "1.0.4", 22 | "elm/bytes": "1.0.8", 23 | "elm/time": "1.0.0", 24 | "elm/url": "1.0.0", 25 | "elm/virtual-dom": "1.0.3" 26 | } 27 | }, 28 | "test-dependencies": { 29 | "direct": { 30 | "elm-explorations/test": "1.2.2" 31 | }, 32 | "indirect": { 33 | "elm/random": "1.0.0" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/BoundingBox.elm: -------------------------------------------------------------------------------- 1 | module BoundingBox exposing 2 | ( forAllNodeAndEdgeTexts 3 | , forEdgeText 4 | , forNodeContext 5 | , forNodeText 6 | ) 7 | 8 | import Browser.Dom as Dom 9 | import Graph exposing (NodeContext, NodeId) 10 | import IntDict 11 | import Task 12 | import Types exposing (BBox, EdgeLabel, ModelGraph, Msg(..), NodeLabel, elementToBBox) 13 | 14 | 15 | {-| Request bounding box of svg element with given String id 16 | -} 17 | requestBoundingBox : String -> (BBox -> Msg) -> Cmd Msg 18 | requestBoundingBox elementId bboxToMsg = 19 | Dom.getElement elementId 20 | |> Task.attempt (Result.map (elementToBBox >> bboxToMsg) >> Result.withDefault NoOp) 21 | 22 | 23 | forNodeText : NodeId -> Cmd Msg 24 | forNodeText nodeId = 25 | requestBoundingBox 26 | (String.fromInt nodeId) 27 | (SetNodeBoundingBox nodeId) 28 | 29 | 30 | forEdgeText : NodeId -> NodeId -> Cmd Msg 31 | forEdgeText fromId toId = 32 | requestBoundingBox 33 | (String.fromInt fromId ++ ":" ++ String.fromInt toId) 34 | (SetEdgeBoundingBox fromId toId) 35 | 36 | 37 | {-| Request bounding boxes for all node and edges in the entire graph 38 | -} 39 | forAllNodeAndEdgeTexts : ModelGraph -> Cmd Msg 40 | forAllNodeAndEdgeTexts graph = 41 | let 42 | nodeBbReqs = 43 | Graph.nodeIds graph 44 | |> List.map forNodeText 45 | |> Cmd.batch 46 | 47 | edgeBbReqs = 48 | Graph.edges graph 49 | |> List.map (\e -> forEdgeText e.from e.to) 50 | |> Cmd.batch 51 | in 52 | Cmd.batch [ nodeBbReqs, edgeBbReqs ] 53 | 54 | 55 | forNodeContext : Maybe (NodeContext NodeLabel EdgeLabel) -> Cmd Msg 56 | forNodeContext = 57 | Maybe.withDefault Cmd.none << Maybe.map forNodeContextHelper 58 | 59 | 60 | forNodeContextHelper : NodeContext NodeLabel EdgeLabel -> Cmd Msg 61 | forNodeContextHelper ctx = 62 | let 63 | nodeId = 64 | ctx.node.id 65 | in 66 | Cmd.batch 67 | [ forNodeText nodeId 68 | , IntDict.keys ctx.outgoing |> List.map (\to -> forEdgeText nodeId to) |> Cmd.batch 69 | , IntDict.keys ctx.incoming |> List.map (\from -> forEdgeText from nodeId) |> Cmd.batch 70 | ] 71 | -------------------------------------------------------------------------------- /src/Canvas.elm: -------------------------------------------------------------------------------- 1 | module Canvas exposing (boxedText, drawEdge, edgeArrow, positionedText, svgDefs) 2 | 3 | import Graph exposing (NodeId) 4 | import Html.Attributes exposing (style) 5 | import Html.Events 6 | import Svg exposing (Svg, g, rect, text, text_) 7 | import Svg.Attributes as Sattr exposing (d, dominantBaseline, fill, fillOpacity, fontFamily, fontSize, height, markerEnd, markerHeight, markerUnits, markerWidth, orient, refX, refY, rx, ry, stroke, strokeWidth, textAnchor, transform, width, x1, x2, y1, y2) 8 | import SvgMouse 9 | import Types exposing (DragMsg(..), EdgeLabel(..), EditState(..), EditorMode(..), GraphEdge, GraphNode, Msg(..), NodeLabel, NodeText(..), nodeLabelToString) 10 | 11 | 12 | boxedText : GraphNode -> EditorMode -> Svg Msg 13 | boxedText ({ id, label } as node) editorMode = 14 | let 15 | tranformValue = 16 | "translate(" ++ String.fromInt (label.x - boxCenterX) ++ "," ++ String.fromInt (label.y - boxCenterY) ++ ")" 17 | 18 | boxWidth = 19 | getBoxWidth label.nodeText 20 | 21 | boxCenterX = 22 | boxWidth // 2 23 | 24 | boxCenterY = 25 | boxHeight // 2 26 | 27 | modeDependentAttributes = 28 | case editorMode of 29 | LayoutMode -> 30 | [ onClickStartDrag id 31 | , style "cursor" "move" 32 | ] 33 | 34 | EditMode editState -> 35 | case editState of 36 | EditingNothing -> 37 | [ onMouseDownSelectStartingNode id 38 | , onDoubleClickStartNodeLabelEdit node 39 | ] 40 | 41 | CreatingEdge selectedNodeId _ -> 42 | [ if id == selectedNodeId then 43 | SvgMouse.onMouseUpUnselectStartNode 44 | 45 | else 46 | onMouseUpCreateEdge id 47 | ] 48 | 49 | EditingEdgeLabel _ -> 50 | [ onDoubleClickStartNodeLabelEdit node ] 51 | 52 | EditingNodeLabel _ -> 53 | [ onDoubleClickStartNodeLabelEdit node ] 54 | 55 | DeletionMode -> 56 | [ onClickDeleteNode id 57 | , style "cursor" "not-allowed" 58 | ] 59 | 60 | nodeTextId = 61 | String.fromInt id 62 | in 63 | g 64 | [ transform tranformValue ] 65 | [ rect 66 | ([ width (String.fromInt boxWidth) 67 | , height (String.fromInt boxHeight) 68 | , rx "4" 69 | , ry "4" 70 | , stroke "black" 71 | , strokeWidth "1" 72 | , fill "white" 73 | ] 74 | ++ modeDependentAttributes 75 | ) 76 | [] 77 | , positionedText boxCenterX boxCenterY nodeTextId (nodeLabelToString label) modeDependentAttributes 78 | ] 79 | 80 | 81 | positionedText : Int -> Int -> String -> String -> List (Svg.Attribute Msg) -> Svg Msg 82 | positionedText xCoord yCoord elementId textContent additionalAttributes = 83 | text_ 84 | ([ Sattr.id elementId 85 | , Sattr.x <| String.fromInt xCoord 86 | , Sattr.y <| String.fromInt yCoord 87 | , fill "black" 88 | , textAnchor "middle" 89 | , dominantBaseline "central" -- alignmentBaseline not working on FF - see https://stackoverflow.com/questions/19212498/firefox-support-for-alignment-baseline-property#answer-21373135 90 | , fontSize "16px" 91 | , fontFamily "sans-serif, monospace" 92 | 93 | --prevent text to be selectable by click+dragging 94 | , style "user-select" "none" 95 | 96 | --prevent text to be selectable by click+dragging 97 | , style "-moz-user-select" "none" 98 | ] 99 | ++ additionalAttributes 100 | ) 101 | [ text textContent ] 102 | 103 | 104 | getBoxWidth : NodeText -> Int 105 | getBoxWidth (NodeText mBBox str) = 106 | -- When there's no text in the node, render it as a square 107 | if String.isEmpty str then 108 | boxHeight 109 | 110 | else 111 | case mBBox of 112 | Nothing -> 113 | characterWidthPixels * String.length str 114 | 115 | Just bbox -> 116 | characterWidthPixels + round bbox.width 117 | 118 | 119 | edgeArrow : GraphEdge -> GraphNode -> GraphNode -> EditorMode -> Svg Msg 120 | edgeArrow edge fromNode toNode editorMode = 121 | let 122 | { x, y, nodeText } = 123 | toNode.label 124 | 125 | edgeIncomingAngle = 126 | atan2 (toFloat (y - fromNode.label.y)) (toFloat (fromNode.label.x - x)) 127 | 128 | boxWidth = 129 | getBoxWidth nodeText 130 | 131 | criticalAngle = 132 | determineAngle boxWidth boxHeight 133 | 134 | whichSideOfTargetBoxToPointTo = 135 | determineSide criticalAngle edgeIncomingAngle 136 | 137 | ( arrowHeadX, arrowHeadY ) = 138 | let 139 | yCorrection = 140 | round (toFloat (boxWidth // 2) * tan edgeIncomingAngle) 141 | 142 | xCorrection = 143 | round (toFloat (boxHeight // 2) / tan edgeIncomingAngle) 144 | in 145 | case whichSideOfTargetBoxToPointTo of 146 | BRight -> 147 | ( x + boxWidth // 2, y - yCorrection ) 148 | 149 | BLeft -> 150 | ( x - boxWidth // 2, y + yCorrection ) 151 | 152 | BTop -> 153 | ( x + xCorrection, y - boxHeight // 2 ) 154 | 155 | BBottom -> 156 | ( x - xCorrection, y + boxHeight // 2 ) 157 | 158 | modeDependentAttributes = 159 | case editorMode of 160 | DeletionMode -> 161 | [ onClickDeleteEdge fromNode.id toNode.id, style "cursor" "not-allowed" ] 162 | 163 | EditMode _ -> 164 | [ onDoubleClickStartEdgeLabelEdit edge, SvgMouse.onMouseDownStopPropagation NoOp ] 165 | 166 | _ -> 167 | [ SvgMouse.onMouseDownStopPropagation NoOp ] 168 | 169 | edgeTextId = 170 | String.fromInt fromNode.id ++ ":" ++ String.fromInt toNode.id 171 | in 172 | drawEdge fromNode.label arrowHeadX arrowHeadY edgeTextId edge modeDependentAttributes 173 | 174 | 175 | drawEdge : NodeLabel -> Int -> Int -> String -> GraphEdge -> List (Svg.Attribute Msg) -> Svg Msg 176 | drawEdge fromLabel xTo yTo edgeTextId edge attrList = 177 | let 178 | coordAttrs = 179 | [ x1 (String.fromInt fromLabel.x) 180 | , y1 (String.fromInt fromLabel.y) 181 | , x2 (String.fromInt xTo) 182 | , y2 (String.fromInt yTo) 183 | ] 184 | 185 | edgeCenterX = 186 | (fromLabel.x + xTo) // 2 187 | 188 | edgeCenterY = 189 | (fromLabel.y + yTo) // 2 190 | 191 | (EdgeLabel mBBox edgeText) = 192 | edge.label 193 | 194 | backgroundRect attrs = 195 | case mBBox of 196 | Nothing -> 197 | Svg.text "" 198 | 199 | Just bbox -> 200 | rect 201 | ([ Sattr.x (String.fromFloat bbox.x) 202 | , Sattr.y (String.fromFloat bbox.y) 203 | , width (String.fromFloat bbox.width) 204 | , height (String.fromFloat bbox.height) 205 | , fill "white" 206 | , fillOpacity "0.8" 207 | ] 208 | ++ attrs 209 | ) 210 | [] 211 | in 212 | g attrList 213 | [ Svg.line (coordAttrs ++ [ stroke "transparent", strokeWidth "6" ]) [] 214 | , Svg.line (coordAttrs ++ [ stroke "black", strokeWidth "1", markerEnd "url(#arrow)" ]) [] 215 | , backgroundRect attrList 216 | , positionedText edgeCenterX edgeCenterY edgeTextId edgeText attrList 217 | ] 218 | 219 | 220 | svgDefs : Svg Msg 221 | svgDefs = 222 | Svg.defs [] [ arrowHeadMarkerDef ] 223 | 224 | 225 | {-| Arrowhead to be reused by all edges, inspired by 226 | -} 227 | arrowHeadMarkerDef : Svg a 228 | arrowHeadMarkerDef = 229 | Svg.marker [ Sattr.id "arrow", markerWidth "15", markerHeight "6", refX "15", refY "3", orient "auto", markerUnits "strokeWidth" ] 230 | [ Svg.path [ d "M0,0 L0,6 L15,3 z", fill "black" ] [] 231 | ] 232 | 233 | 234 | onDoubleClickStartNodeLabelEdit : GraphNode -> Svg.Attribute Msg 235 | onDoubleClickStartNodeLabelEdit node = 236 | SvgMouse.onDoubleClickStopPropagation (NodeLabelEditStart node) 237 | 238 | 239 | onDoubleClickStartEdgeLabelEdit : GraphEdge -> Svg.Attribute Msg 240 | onDoubleClickStartEdgeLabelEdit edge = 241 | SvgMouse.onDoubleClickStopPropagation (EdgeLabelEditStart edge) 242 | 243 | 244 | onClickStartDrag : NodeId -> Svg.Attribute Msg 245 | onClickStartDrag nodeId = 246 | SvgMouse.onMouseDownGetPosition (NodeDrag << DragStart nodeId) 247 | 248 | 249 | onClickDeleteNode : NodeId -> Svg.Attribute Msg 250 | onClickDeleteNode nodeId = 251 | SvgMouse.onClickStopPropagation (DeleteNode nodeId) 252 | 253 | 254 | onClickDeleteEdge : NodeId -> NodeId -> Svg.Attribute Msg 255 | onClickDeleteEdge from to = 256 | SvgMouse.onClickStopPropagation (DeleteEdge from to) 257 | 258 | 259 | onMouseDownSelectStartingNode : NodeId -> Svg.Attribute Msg 260 | onMouseDownSelectStartingNode nodeId = 261 | SvgMouse.onMouseDownStopPropagation (StartNodeOfEdgeSelected nodeId) 262 | 263 | 264 | onMouseUpCreateEdge : NodeId -> Svg.Attribute Msg 265 | onMouseUpCreateEdge nodeId = 266 | Html.Events.onMouseUp (EndNodeOfEdgeSelected nodeId) 267 | 268 | 269 | characterWidthPixels : Int 270 | characterWidthPixels = 271 | 9 272 | 273 | 274 | characterHeightPixels : Int 275 | characterHeightPixels = 276 | 15 277 | 278 | 279 | boxHeight : Int 280 | boxHeight = 281 | characterHeightPixels + 10 282 | 283 | 284 | {-| Determine which side of the box we should draw arrowhead of incomming edge 285 | We first find "critical angle" (angle between x axis and line from box center to its upper right corner. 286 | Based on that we determine which side of the box the arrowhead should point. 287 | -} 288 | type BoxSide 289 | = BRight 290 | | BTop 291 | | BLeft 292 | | BBottom 293 | 294 | 295 | determineAngle : Int -> Int -> Float 296 | determineAngle boxWidth boxHeight_ = 297 | atan2 (toFloat boxHeight_) (toFloat boxWidth) 298 | 299 | 300 | determineSide : Float -> Float -> BoxSide 301 | determineSide boxCriticalAngle edgeIncomingAngle = 302 | if -boxCriticalAngle < edgeIncomingAngle && edgeIncomingAngle <= boxCriticalAngle then 303 | BRight 304 | 305 | else if boxCriticalAngle < edgeIncomingAngle && edgeIncomingAngle <= (Basics.pi - boxCriticalAngle) then 306 | BTop 307 | 308 | else if (-Basics.pi + boxCriticalAngle) < edgeIncomingAngle && edgeIncomingAngle <= -boxCriticalAngle then 309 | BBottom 310 | 311 | else 312 | BLeft 313 | -------------------------------------------------------------------------------- /src/Data/Layout.elm: -------------------------------------------------------------------------------- 1 | module Data.Layout exposing 2 | ( LayoutEngine(..) 3 | , engineToString 4 | ) 5 | 6 | {-| GraphViz layout engines available through viz-js 7 | 8 | -} 9 | 10 | 11 | type LayoutEngine 12 | = Circo 13 | | Dot 14 | | Fdp 15 | | Neato 16 | | Osage 17 | | Twopi 18 | 19 | 20 | engineToString : LayoutEngine -> String 21 | engineToString layoutEngine = 22 | case layoutEngine of 23 | Circo -> 24 | "circo" 25 | 26 | Dot -> 27 | "dot" 28 | 29 | Fdp -> 30 | "fdp" 31 | 32 | Neato -> 33 | "neato" 34 | 35 | Osage -> 36 | "osage" 37 | 38 | Twopi -> 39 | "twopi" 40 | -------------------------------------------------------------------------------- /src/Export.elm: -------------------------------------------------------------------------------- 1 | module Export exposing (toDot, toTgf) 2 | 3 | import Dict exposing (Dict) 4 | import Graph.DOT as DOT 5 | import Graph.TGF as TGF 6 | import Types exposing (EdgeLabel, ModelGraph, NodeLabel, edgeLabelToString, nodeLabelToString) 7 | 8 | 9 | toTgf : ModelGraph -> String 10 | toTgf = 11 | TGF.output nodeLabelToString edgeLabelToString 12 | 13 | 14 | toDot : ModelGraph -> String 15 | toDot = 16 | DOT.outputWithStylesAndAttributes DOT.defaultStyles nodeLabelToAttrs edgeLabelToAttrs 17 | 18 | 19 | nodeLabelToAttrs : NodeLabel -> Dict String String 20 | nodeLabelToAttrs = 21 | Dict.singleton "label" << nodeLabelToString 22 | 23 | 24 | edgeLabelToAttrs : EdgeLabel -> Dict String String 25 | edgeLabelToAttrs = 26 | Dict.singleton "label" << edgeLabelToString 27 | -------------------------------------------------------------------------------- /src/GraphUtil.elm: -------------------------------------------------------------------------------- 1 | module GraphUtil exposing 2 | ( getNode 3 | , insertEdge 4 | , insertNode 5 | , removeEdge 6 | , setEdgeBoundingBox 7 | , setNodeBoundingBox 8 | , setNodeText 9 | , updateDraggedNode 10 | , updateNodeLabel 11 | , updateNodePositions 12 | ) 13 | 14 | import Graph exposing (Adjacency, Node, NodeContext, NodeId) 15 | import IntDict exposing (IntDict) 16 | import Types exposing (BBox, Drag, EdgeLabel(..), GraphEdge, GraphNode, ModelGraph, NodeLabel, NodeText(..), setBBoxOfEdgeLabel, setBBoxOfNodeText) 17 | 18 | 19 | updateNodeInContext : (Node n -> Node n) -> NodeContext n e -> NodeContext n e 20 | updateNodeInContext nodeUpdater ({ node } as ctx) = 21 | { ctx | node = nodeUpdater node } 22 | 23 | 24 | updateOutgoingAdjacency : (Adjacency e -> Adjacency e) -> NodeContext n e -> NodeContext n e 25 | updateOutgoingAdjacency adjacencyUpdater ({ outgoing } as ctx) = 26 | { ctx | outgoing = adjacencyUpdater outgoing } 27 | 28 | 29 | updateLabelInNode : (lab -> lab) -> Node lab -> Node lab 30 | updateLabelInNode labelUpdater node = 31 | { node | label = labelUpdater node.label } 32 | 33 | 34 | setNodeText : NodeText -> GraphNode -> GraphNode 35 | setNodeText newText node = 36 | updateLabelInNode (\label -> { label | nodeText = newText }) node 37 | 38 | 39 | updateNodeTextInLabel : (NodeText -> NodeText) -> NodeLabel -> NodeLabel 40 | updateNodeTextInLabel f nodeLabel = 41 | { nodeLabel | nodeText = f nodeLabel.nodeText } 42 | 43 | 44 | insertNode : GraphNode -> ModelGraph -> ModelGraph 45 | insertNode node = 46 | Graph.insert 47 | { node = node 48 | , incoming = IntDict.empty 49 | , outgoing = IntDict.empty 50 | } 51 | 52 | 53 | updateNodeLabel : NodeId -> NodeText -> ModelGraph -> ModelGraph 54 | updateNodeLabel nodeId newNodeText graph = 55 | Graph.update nodeId (Maybe.map (updateNodeInContext (setNodeText newNodeText))) graph 56 | 57 | 58 | updateDraggedNode : Drag -> ModelGraph -> ModelGraph 59 | updateDraggedNode drag graph = 60 | Graph.update drag.nodeId (updateDraggedNodeInContext drag) graph 61 | 62 | 63 | updateDraggedNodeInContext : Drag -> Maybe (NodeContext NodeLabel e) -> Maybe (NodeContext NodeLabel e) 64 | updateDraggedNodeInContext drag = 65 | Maybe.map (updateNodeInContext (updateLabelInNode (Types.getDraggedNodePosition drag))) 66 | 67 | 68 | insertEdge : GraphEdge -> ModelGraph -> ModelGraph 69 | insertEdge edge graph = 70 | Graph.update edge.from (Maybe.map (updateOutgoingAdjacency (IntDict.insert edge.to edge.label))) graph 71 | 72 | 73 | removeEdge : NodeId -> NodeId -> ModelGraph -> ModelGraph 74 | removeEdge from to gr = 75 | let 76 | removeOutgoingEdge : NodeId -> NodeContext NodeLabel EdgeLabel -> NodeContext NodeLabel EdgeLabel 77 | removeOutgoingEdge toId oldContext = 78 | { oldContext 79 | | outgoing = IntDict.remove toId oldContext.outgoing 80 | } 81 | in 82 | Graph.update from (Maybe.map (removeOutgoingEdge to)) gr 83 | 84 | 85 | getNode : NodeId -> ModelGraph -> GraphNode 86 | getNode nodeId graph = 87 | Graph.get nodeId graph |> Maybe.map .node |> crashIfNodeNotInGraph nodeId 88 | 89 | 90 | crashIfNodeNotInGraph : NodeId -> Maybe GraphNode -> GraphNode 91 | crashIfNodeNotInGraph nodeId mGraphNode = 92 | case mGraphNode of 93 | Nothing -> 94 | crashWorkAround nodeId 95 | 96 | Just node -> 97 | node 98 | 99 | 100 | {-| TODO avoid having to use this 101 | -} 102 | crashWorkAround : NodeId -> GraphNode 103 | crashWorkAround nodeId = 104 | { id = -1 105 | , label = 106 | { nodeText = NodeText Nothing ("Node with id " ++ String.fromInt nodeId ++ " was not in the graph") 107 | , x = 0 108 | , y = 0 109 | } 110 | } 111 | 112 | 113 | setNodeBoundingBox : NodeId -> BBox -> ModelGraph -> ModelGraph 114 | setNodeBoundingBox nodeId bbox = 115 | Graph.update nodeId 116 | (Maybe.map <| updateNodeInContext <| updateLabelInNode <| updateNodeTextInLabel <| setBBoxOfNodeText bbox) 117 | 118 | 119 | setEdgeBoundingBox : NodeId -> NodeId -> BBox -> ModelGraph -> ModelGraph 120 | setEdgeBoundingBox fromId toId bbox = 121 | Graph.update fromId 122 | (Maybe.map <| updateOutgoingAdjacency <| IntDict.update toId <| Maybe.map <| setBBoxOfEdgeLabel bbox) 123 | 124 | 125 | updateNodePositions : IntDict { x : Float, y : Float } -> ModelGraph -> ModelGraph 126 | updateNodePositions positionDict graph = 127 | Graph.mapContexts (setNewPositionForNode positionDict) graph 128 | 129 | 130 | setNewPositionForNode : 131 | IntDict { x : Float, y : Float } 132 | -> Graph.NodeContext NodeLabel EdgeLabel 133 | -> Graph.NodeContext NodeLabel EdgeLabel 134 | setNewPositionForNode positionDict nodeContext = 135 | case IntDict.get nodeContext.node.id positionDict of 136 | Just newPos -> 137 | updateNodeInContext 138 | (updateLabelInNode (\nodeLabel -> { nodeLabel | x = round newPos.x, y = round newPos.y })) 139 | nodeContext 140 | 141 | Nothing -> 142 | nodeContext 143 | -------------------------------------------------------------------------------- /src/GraphViz/VizJs.elm: -------------------------------------------------------------------------------- 1 | module GraphViz.VizJs exposing 2 | ( NodePositions 3 | , PlainGraph 4 | , PlainSource(..) 5 | , parsePlainSource 6 | , processGraphVizResponse 7 | ) 8 | 9 | import IntDict exposing (IntDict) 10 | import Json.Decode as Decode exposing (Decoder) 11 | import Parser exposing ((|.), (|=), Parser) 12 | import Types exposing (WindowSize) 13 | 14 | 15 | type alias NodePositions = 16 | IntDict 17 | { x : Float 18 | , y : Float 19 | } 20 | 21 | 22 | processGraphVizResponse : Decode.Value -> WindowSize -> Result String NodePositions 23 | processGraphVizResponse value windowSize = 24 | decodeGraphvizResponse value 25 | |> Result.andThen parsePlainSource 26 | |> Result.map (scaleToWindow windowSize) 27 | 28 | 29 | 30 | -- JSON to PlainSource 31 | 32 | 33 | decodeGraphvizResponse : Decode.Value -> Result String PlainSource 34 | decodeGraphvizResponse value = 35 | case Decode.decodeValue graphVizResponseDecoder value of 36 | Err decodeErr -> 37 | Err <| Decode.errorToString decodeErr 38 | 39 | Ok result -> 40 | result 41 | 42 | 43 | graphVizResponseDecoder : Decoder (Result String PlainSource) 44 | graphVizResponseDecoder = 45 | Decode.oneOf 46 | [ Decode.field "GraphViz_PlainOutput" Decode.string |> Decode.map (Ok << PlainSource) 47 | , Decode.field "GraphViz_Error" Decode.string |> Decode.map Err 48 | ] 49 | 50 | 51 | 52 | -- Plain Source to NodePositions 53 | 54 | 55 | type PlainSource 56 | = PlainSource String 57 | 58 | 59 | {-| Represents the part of data parsed from GraphViz 60 | [plain](https://www.graphviz.org/doc/info/output.html#d:plain) 61 | format, which is related to layout of nodes. 62 | -} 63 | type alias PlainGraph = 64 | { scale : Float 65 | , width : Float 66 | , height : Float 67 | , nodes : List PlainNode 68 | } 69 | 70 | 71 | type alias PlainNode = 72 | { nodeId : Int 73 | , x : Float 74 | , y : Float 75 | } 76 | 77 | 78 | parsePlainSource : PlainSource -> Result String PlainGraph 79 | parsePlainSource (PlainSource plainSource) = 80 | Parser.run plainGraphParser plainSource 81 | |> Result.mapError Parser.deadEndsToString 82 | 83 | 84 | plainGraphParser : Parser PlainGraph 85 | plainGraphParser = 86 | Parser.succeed PlainGraph 87 | |. Parser.keyword "graph" 88 | |. Parser.spaces 89 | --scale 90 | |= Parser.float 91 | |. Parser.spaces 92 | --width 93 | |= Parser.float 94 | |. Parser.spaces 95 | --height 96 | |= Parser.float 97 | |. Parser.chompIf (\char -> char == '\n') 98 | |= listOfNodesParser 99 | |. Parser.chompUntil "stop" 100 | 101 | 102 | listOfNodesParser : Parser (List PlainNode) 103 | listOfNodesParser = 104 | Parser.sequence 105 | { start = "" 106 | , separator = "" 107 | , end = "" 108 | , spaces = Parser.spaces 109 | , item = plainNodeParser 110 | , trailing = Parser.Forbidden 111 | } 112 | 113 | 114 | {-| Parse nodeId, x and y coordinate from GraphViz plain format's node line which has the format 115 | "node name x y width height label style shape color fillcolor" 116 | -} 117 | plainNodeParser : Parser PlainNode 118 | plainNodeParser = 119 | Parser.succeed PlainNode 120 | |. Parser.keyword "node" 121 | |. Parser.spaces 122 | -- nodeId 123 | |= Parser.int 124 | |. Parser.spaces 125 | -- x 126 | |= Parser.float 127 | |. Parser.spaces 128 | -- y 129 | |= Parser.float 130 | |. Parser.chompUntil "\n" 131 | 132 | 133 | 134 | -- Scaling to window Size 135 | 136 | 137 | scaleToWindow : WindowSize -> PlainGraph -> NodePositions 138 | scaleToWindow windowSize plainGraph = 139 | let 140 | scaleX = 141 | windowSize.width / plainGraph.width 142 | 143 | scaleY = 144 | windowSize.height / plainGraph.height 145 | in 146 | plainGraph.nodes 147 | |> List.map 148 | (\plainNode -> 149 | ( plainNode.nodeId 150 | , { x = scaleX * plainNode.x 151 | , y = scaleY * plainNode.y 152 | } 153 | ) 154 | ) 155 | |> IntDict.fromList 156 | -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import BoundingBox 4 | import Browser 5 | import Browser.Dom as Dom 6 | import Browser.Events 7 | import Export 8 | import File.Download 9 | import Graph exposing (NodeId) 10 | import GraphUtil 11 | import GraphViz.VizJs 12 | import Json.Decode as Decode 13 | import Ports 14 | import Task 15 | import Types 16 | exposing 17 | ( Drag 18 | , DragMsg(..) 19 | , EdgeLabel(..) 20 | , EditState(..) 21 | , EditorMode(..) 22 | , GraphNode 23 | , ModalState(..) 24 | , Model 25 | , ModelGraph 26 | , MousePosition 27 | , Msg(..) 28 | , NodeLabel 29 | , NodeText(..) 30 | , WindowSize 31 | , mousePositionDecoder 32 | , nodeLabelToString 33 | , setEdgeText 34 | ) 35 | import View 36 | 37 | 38 | initialGraph : ModelGraph 39 | initialGraph = 40 | Graph.empty 41 | 42 | 43 | init : () -> ( Model, Cmd Msg ) 44 | init _ = 45 | ( { graph = initialGraph 46 | , newNodeId = Graph.size initialGraph 47 | , draggedNode = Nothing 48 | , editorMode = EditMode EditingNothing 49 | , modalState = Hidden 50 | , windowSize = { width = 800, height = 600 } 51 | } 52 | , Cmd.batch 53 | [ BoundingBox.forAllNodeAndEdgeTexts initialGraph 54 | , Task.perform GotViewport Dom.getViewport 55 | ] 56 | ) 57 | 58 | 59 | update : Msg -> Model -> ( Model, Cmd Msg ) 60 | update msg model = 61 | case msg of 62 | CreateNode { x, y } -> 63 | let 64 | newNode = 65 | makeNode model.newNodeId x y "" 66 | in 67 | ( { model 68 | | graph = GraphUtil.insertNode newNode model.graph 69 | , newNodeId = model.newNodeId + 1 70 | } 71 | , BoundingBox.forNodeText model.newNodeId 72 | ) 73 | 74 | NodeDrag dragMsg -> 75 | processDragMsg dragMsg model 76 | 77 | NodeLabelEditStart node -> 78 | ( { model | editorMode = EditMode (EditingNodeLabel node) } 79 | , View.focusLabelInput 80 | ) 81 | 82 | NodeLabelEdit newNodeText -> 83 | let 84 | newEditorMode = 85 | case model.editorMode of 86 | EditMode (EditingNodeLabel node) -> 87 | EditMode <| EditingNodeLabel <| GraphUtil.setNodeText (NodeText Nothing newNodeText) node 88 | 89 | _ -> 90 | model.editorMode 91 | in 92 | ( { model | editorMode = newEditorMode } 93 | , Cmd.none 94 | ) 95 | 96 | NodeLabelEditConfirm -> 97 | let 98 | ( newGraph, command ) = 99 | case model.editorMode of 100 | EditMode (EditingNodeLabel node) -> 101 | ( GraphUtil.updateNodeLabel node.id (NodeText Nothing (nodeLabelToString node.label)) model.graph 102 | , Graph.get node.id model.graph |> BoundingBox.forNodeContext 103 | ) 104 | 105 | _ -> 106 | ( model.graph, Cmd.none ) 107 | in 108 | ( { model | graph = newGraph, editorMode = EditMode EditingNothing } 109 | , command 110 | ) 111 | 112 | EdgeLabelEditStart edge -> 113 | ( { model | editorMode = EditMode (EditingEdgeLabel edge) } 114 | , View.focusLabelInput 115 | ) 116 | 117 | EdgeLabelEdit newText -> 118 | let 119 | newEditorMode = 120 | case model.editorMode of 121 | EditMode (EditingEdgeLabel edge) -> 122 | EditMode (EditingEdgeLabel { edge | label = setEdgeText newText edge.label }) 123 | 124 | _ -> 125 | model.editorMode 126 | in 127 | ( { model | editorMode = newEditorMode } 128 | , Cmd.none 129 | ) 130 | 131 | EdgeLabelEditConfirm -> 132 | let 133 | ( newGraph, command ) = 134 | case model.editorMode of 135 | EditMode (EditingEdgeLabel edge) -> 136 | ( GraphUtil.insertEdge edge model.graph, BoundingBox.forEdgeText edge.from edge.to ) 137 | 138 | _ -> 139 | ( model.graph, Cmd.none ) 140 | in 141 | ( { model | graph = newGraph, editorMode = EditMode EditingNothing } 142 | , command 143 | ) 144 | 145 | StartNodeOfEdgeSelected nodeId -> 146 | let 147 | selectedNodeLabel = 148 | GraphUtil.getNode nodeId model.graph |> .label 149 | in 150 | ( { model | editorMode = EditMode (CreatingEdge nodeId { x = selectedNodeLabel.x, y = selectedNodeLabel.y }) } 151 | , Cmd.none 152 | ) 153 | 154 | EndNodeOfEdgeSelected endNodeId -> 155 | let 156 | ( newGraph, command ) = 157 | case model.editorMode of 158 | EditMode (CreatingEdge startNodeId _) -> 159 | ( GraphUtil.insertEdge { from = startNodeId, to = endNodeId, label = EdgeLabel Nothing "" } model.graph 160 | , BoundingBox.forEdgeText startNodeId endNodeId 161 | ) 162 | 163 | _ -> 164 | ( model.graph, Cmd.none ) 165 | in 166 | ( { model | graph = newGraph, editorMode = EditMode EditingNothing } 167 | , command 168 | ) 169 | 170 | UnselectStartNodeOfEdge -> 171 | ( { model | editorMode = EditMode EditingNothing } 172 | , Cmd.none 173 | ) 174 | 175 | PreviewEdgeEndpointPositionChanged mousePosition -> 176 | ( setMousePositionIfCreatingEdge mousePosition model 177 | , Cmd.none 178 | ) 179 | 180 | DeleteNode nodeId -> 181 | let 182 | newGraph = 183 | Graph.remove nodeId model.graph 184 | 185 | newEditorMode = 186 | --after last node deleted switch back to node creation mode 187 | if Graph.isEmpty newGraph then 188 | EditMode EditingNothing 189 | 190 | else 191 | model.editorMode 192 | in 193 | ( { model | graph = newGraph, editorMode = newEditorMode } 194 | , Cmd.none 195 | ) 196 | 197 | DeleteEdge fromId toId -> 198 | ( { model | graph = GraphUtil.removeEdge fromId toId model.graph } 199 | , Cmd.none 200 | ) 201 | 202 | SetMode mode -> 203 | ( { model | editorMode = mode } 204 | , Cmd.none 205 | ) 206 | 207 | SetNodeBoundingBox nodeId bbox -> 208 | ( { model | graph = GraphUtil.setNodeBoundingBox nodeId bbox model.graph } 209 | , Cmd.none 210 | ) 211 | 212 | SetEdgeBoundingBox fromNode toNode bbox -> 213 | ( { model | graph = GraphUtil.setEdgeBoundingBox fromNode toNode bbox model.graph } 214 | , Cmd.none 215 | ) 216 | 217 | ModalStateChange newModalState -> 218 | ( { model | modalState = newModalState } 219 | , Cmd.none 220 | ) 221 | 222 | GotViewport viewport -> 223 | ( { model | windowSize = extractWindowSize viewport } 224 | , Cmd.none 225 | ) 226 | 227 | WindowResized w h -> 228 | ( { model 229 | | windowSize = 230 | { width = toFloat w 231 | , height = toFloat h 232 | } 233 | } 234 | , Cmd.none 235 | ) 236 | 237 | Download exportFormat -> 238 | let 239 | ( graphToString, fileName ) = 240 | case exportFormat of 241 | Types.Dot -> 242 | ( Export.toDot, "graph.dot" ) 243 | 244 | Types.Tgf -> 245 | ( Export.toTgf, "graph.tgf" ) 246 | in 247 | ( model 248 | , File.Download.string fileName "text/plain" (graphToString model.graph) 249 | ) 250 | 251 | PerformAutomaticLayout layoutEngine -> 252 | ( model 253 | , Ports.requestGraphVizPlain layoutEngine model.graph 254 | ) 255 | 256 | ReceiveLayoutInfoFromGraphviz jsonVal -> 257 | let 258 | newModel = 259 | GraphViz.VizJs.processGraphVizResponse jsonVal model.windowSize 260 | |> Result.map (\newNodePositions -> { model | graph = GraphUtil.updateNodePositions newNodePositions model.graph }) 261 | |> Result.withDefault model 262 | in 263 | ( newModel 264 | , BoundingBox.forAllNodeAndEdgeTexts model.graph 265 | ) 266 | 267 | Resize shrinkFactor -> 268 | ( { model | graph = resizeGraph shrinkFactor model.windowSize model.graph } 269 | , Cmd.none 270 | ) 271 | 272 | NoOp -> 273 | ( model, Cmd.none ) 274 | 275 | 276 | resizeGraph : Float -> WindowSize -> ModelGraph -> ModelGraph 277 | resizeGraph shrinkFactor { width, height } graph = 278 | let 279 | halfWidth = 280 | width / 2 281 | 282 | halfHeight = 283 | height / 2 284 | in 285 | Graph.mapNodes 286 | (\n -> 287 | { n 288 | | x = round <| (toFloat n.x - halfWidth) * shrinkFactor + halfWidth 289 | , y = round <| (toFloat n.y - halfHeight) * shrinkFactor + halfHeight 290 | } 291 | ) 292 | graph 293 | 294 | 295 | setMousePositionIfCreatingEdge : MousePosition -> Model -> Model 296 | setMousePositionIfCreatingEdge mousePosition model = 297 | let 298 | newEditorMode = 299 | case model.editorMode of 300 | EditMode (CreatingEdge nodeId _) -> 301 | EditMode (CreatingEdge nodeId mousePosition) 302 | 303 | _ -> 304 | model.editorMode 305 | in 306 | { model | editorMode = newEditorMode } 307 | 308 | 309 | processDragMsg : DragMsg -> Model -> ( Model, Cmd Msg ) 310 | processDragMsg msg model = 311 | case msg of 312 | DragStart nodeId xy -> 313 | ( { model | draggedNode = Just (Drag nodeId xy xy) } 314 | , Cmd.none 315 | ) 316 | 317 | DragAt xy -> 318 | ( { model | draggedNode = Maybe.map (\{ nodeId, start } -> Drag nodeId start xy) model.draggedNode } 319 | , Cmd.none 320 | ) 321 | 322 | DragEnd _ -> 323 | let 324 | ( newGraph, command ) = 325 | Maybe.map 326 | (\drag -> 327 | ( GraphUtil.updateDraggedNode drag model.graph 328 | -- After dnd completed, only update bounding boxes of 1. node 2. its incoming and 3. its outgoing edges. 329 | , BoundingBox.forNodeContext <| Graph.get drag.nodeId model.graph 330 | ) 331 | ) 332 | model.draggedNode 333 | |> Maybe.withDefault ( model.graph, Cmd.none ) 334 | in 335 | ( { model | graph = newGraph, draggedNode = Nothing } 336 | , command 337 | ) 338 | 339 | 340 | makeNode : NodeId -> Int -> Int -> String -> GraphNode 341 | makeNode id x y nodeText = 342 | { id = id 343 | , label = makeNodeLabel x y nodeText 344 | } 345 | 346 | 347 | makeNodeLabel : Int -> Int -> String -> NodeLabel 348 | makeNodeLabel x y nodeText = 349 | { nodeText = NodeText Nothing nodeText 350 | , x = x 351 | , y = y 352 | } 353 | 354 | 355 | subscriptions : Model -> Sub Msg 356 | subscriptions model = 357 | Sub.batch 358 | [ nodeDragDropSubscriptions model.draggedNode 359 | , edgeCreationSubscriptions model.editorMode 360 | , Ports.receiveGraphVizPlain ReceiveLayoutInfoFromGraphviz 361 | , Browser.Events.onResize WindowResized 362 | ] 363 | 364 | 365 | nodeDragDropSubscriptions : Maybe Drag -> Sub Msg 366 | nodeDragDropSubscriptions maybeDrag = 367 | case maybeDrag of 368 | Nothing -> 369 | Sub.none 370 | 371 | Just _ -> 372 | Sub.batch 373 | [ Browser.Events.onMouseMove <| 374 | Decode.map (NodeDrag << DragAt) mousePositionDecoder 375 | , Browser.Events.onMouseUp <| 376 | Decode.map (NodeDrag << DragEnd) mousePositionDecoder 377 | ] 378 | 379 | 380 | edgeCreationSubscriptions : EditorMode -> Sub Msg 381 | edgeCreationSubscriptions editorMode = 382 | case editorMode of 383 | EditMode (CreatingEdge _ _) -> 384 | Browser.Events.onMouseMove <| 385 | Decode.map PreviewEdgeEndpointPositionChanged mousePositionDecoder 386 | 387 | _ -> 388 | Sub.none 389 | 390 | 391 | main : Program () Model Msg 392 | main = 393 | Browser.element 394 | { init = init 395 | , view = View.view 396 | , update = update 397 | , subscriptions = subscriptions 398 | } 399 | 400 | 401 | extractWindowSize : Dom.Viewport -> WindowSize 402 | extractWindowSize { viewport } = 403 | { width = viewport.width 404 | , height = viewport.height 405 | } 406 | -------------------------------------------------------------------------------- /src/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing 2 | ( receiveGraphVizPlain 3 | , requestGraphVizPlain 4 | ) 5 | 6 | import Data.Layout as Layout 7 | import Export 8 | import Json.Decode as Decode 9 | import Types exposing (ModelGraph) 10 | 11 | 12 | requestGraphVizPlain : Layout.LayoutEngine -> ModelGraph -> Cmd msg 13 | requestGraphVizPlain layoutEngine graph = 14 | requestGraphVizPlain_Impl 15 | { layoutEngine = Layout.engineToString layoutEngine 16 | , graphvizSource = Export.toDot graph 17 | } 18 | 19 | 20 | port receiveGraphVizPlain : (Decode.Value -> msg) -> Sub msg 21 | 22 | 23 | {-| Request to transform Graphviz dot String to Graphviz plain. 24 | The resulting plain output will contain additional info about node layout which we'll parse out and adjust node positions for nicer layout. 25 | -} 26 | port requestGraphVizPlain_Impl : { layoutEngine : String, graphvizSource : String } -> Cmd msg 27 | -------------------------------------------------------------------------------- /src/SvgMouse.elm: -------------------------------------------------------------------------------- 1 | module SvgMouse exposing 2 | ( onCanvasMouseDown 3 | , onClickStopPropagation 4 | , onDoubleClickStopPropagation 5 | , onMouseDownGetPosition 6 | , onMouseDownStopPropagation 7 | , onMouseUpUnselectStartNode 8 | ) 9 | 10 | import Html exposing (Attribute) 11 | import Html.Events exposing (on, stopPropagationOn) 12 | import Json.Decode as Json exposing (field, int) 13 | import Types exposing (MousePosition, Msg(..), mousePositionDecoder) 14 | 15 | 16 | 17 | {- The aim of this module is to work around the fact, that click events fired 18 | from SVG elements don't have target.offsetLeft/offsetTop unlike Html DOM elements. 19 | This means that packages like Elm-Canvas/element-relative-mouse-events don't work 20 | with SVG elements. 21 | 22 | The workaround is based on https://github.com/fredcy/elm-svg-mouse-offset 23 | 24 | The use of stopPropagationOn is to prevent clicks on canvas nodes triggering click events on canvas itself 25 | 26 | -} 27 | 28 | 29 | onCanvasMouseDown : (MousePosition -> msg) -> Html.Attribute msg 30 | onCanvasMouseDown tagger = 31 | on "mousedown" <| Json.map tagger offsetPosition 32 | 33 | 34 | offsetPosition : Json.Decoder MousePosition 35 | offsetPosition = 36 | Json.map2 MousePosition 37 | (field "offsetX" int) 38 | (field "offsetY" int) 39 | 40 | 41 | onClickStopPropagation : Msg -> Attribute Msg 42 | onClickStopPropagation msg = 43 | stopPropagationOn "click" <| Json.succeed ( msg, True ) 44 | 45 | 46 | onMouseDownGetPosition : (MousePosition -> Msg) -> Attribute Msg 47 | onMouseDownGetPosition tagger = 48 | stopPropagationOn "mousedown" <| Json.map (\pos -> ( tagger pos, True )) mousePositionDecoder 49 | 50 | 51 | onMouseDownStopPropagation : Msg -> Attribute Msg 52 | onMouseDownStopPropagation msg = 53 | stopPropagationOn "mousedown" <| Json.succeed ( msg, True ) 54 | 55 | 56 | onDoubleClickStopPropagation : Msg -> Attribute Msg 57 | onDoubleClickStopPropagation msg = 58 | stopPropagationOn "dblclick" <| Json.succeed ( msg, True ) 59 | 60 | 61 | onMouseUpUnselectStartNode : Attribute Msg 62 | onMouseUpUnselectStartNode = 63 | stopPropagationOn "mouseup" <| Json.succeed ( UnselectStartNodeOfEdge, True ) 64 | -------------------------------------------------------------------------------- /src/Types.elm: -------------------------------------------------------------------------------- 1 | module Types exposing 2 | ( BBox 3 | , Drag 4 | , DragMsg(..) 5 | , EdgeLabel(..) 6 | , EditState(..) 7 | , EditorMode(..) 8 | , ExportFormat(..) 9 | , GraphEdge 10 | , GraphNode 11 | , ModalState(..) 12 | , Model 13 | , ModelGraph 14 | , MousePosition 15 | , Msg(..) 16 | , NodeLabel 17 | , NodeText(..) 18 | , WindowSize 19 | , edgeLabelToString 20 | , elementToBBox 21 | , exportFormatToString 22 | , getDraggedNodePosition 23 | , mousePositionDecoder 24 | , nodeLabelToString 25 | , setBBoxOfEdgeLabel 26 | , setBBoxOfNodeText 27 | , setEdgeText 28 | ) 29 | 30 | import Browser.Dom as Dom 31 | import Data.Layout exposing (LayoutEngine) 32 | import Graph exposing (Graph, NodeId) 33 | import Json.Decode as Decode exposing (Decoder) 34 | import Json.Encode 35 | 36 | 37 | type alias Model = 38 | { graph : ModelGraph 39 | , newNodeId : Int 40 | , draggedNode : Maybe Drag 41 | , editorMode : EditorMode 42 | , modalState : ModalState 43 | , windowSize : WindowSize 44 | } 45 | 46 | 47 | type alias WindowSize = 48 | { width : Float 49 | , height : Float 50 | } 51 | 52 | 53 | type alias MousePosition = 54 | { x : Int 55 | , y : Int 56 | } 57 | 58 | 59 | mousePositionDecoder : Decoder MousePosition 60 | mousePositionDecoder = 61 | Decode.map2 MousePosition 62 | (Decode.field "pageX" Decode.int) 63 | (Decode.field "pageY" Decode.int) 64 | 65 | 66 | type alias ModelGraph = 67 | Graph NodeLabel EdgeLabel 68 | 69 | 70 | type alias GraphNode = 71 | Graph.Node NodeLabel 72 | 73 | 74 | type alias GraphEdge = 75 | Graph.Edge EdgeLabel 76 | 77 | 78 | type Msg 79 | = CreateNode MousePosition 80 | | NodeDrag DragMsg 81 | -- Editing Node label 82 | | NodeLabelEditStart GraphNode 83 | | NodeLabelEdit String 84 | | NodeLabelEditConfirm 85 | -- Editing Edge label 86 | | EdgeLabelEditStart GraphEdge 87 | | EdgeLabelEdit String 88 | | EdgeLabelEditConfirm 89 | -- Creating Edges 90 | | StartNodeOfEdgeSelected NodeId 91 | | EndNodeOfEdgeSelected NodeId 92 | | UnselectStartNodeOfEdge 93 | | PreviewEdgeEndpointPositionChanged MousePosition 94 | -- Deleting Nodes and Edges 95 | | DeleteNode NodeId 96 | | DeleteEdge NodeId NodeId 97 | -- Switching between modes 98 | | SetMode EditorMode 99 | | PerformAutomaticLayout LayoutEngine 100 | | SetNodeBoundingBox NodeId BBox 101 | | SetEdgeBoundingBox NodeId NodeId BBox 102 | | ModalStateChange ModalState 103 | | GotViewport Dom.Viewport 104 | | WindowResized Int Int 105 | | Download ExportFormat 106 | | ReceiveLayoutInfoFromGraphviz Json.Encode.Value 107 | | Resize Float 108 | | NoOp 109 | 110 | 111 | type DragMsg 112 | = DragStart NodeId MousePosition 113 | | DragAt MousePosition 114 | | DragEnd MousePosition 115 | 116 | 117 | type ModalState 118 | = Hidden 119 | | Help 120 | | About 121 | | Export ExportFormat 122 | 123 | 124 | type ExportFormat 125 | = Dot 126 | | Tgf 127 | 128 | 129 | exportFormatToString : ExportFormat -> String 130 | exportFormatToString exportFormat = 131 | case exportFormat of 132 | Dot -> 133 | "DOT" 134 | 135 | Tgf -> 136 | "TGF" 137 | 138 | 139 | type alias Drag = 140 | { nodeId : NodeId 141 | , start : MousePosition 142 | , current : MousePosition 143 | } 144 | 145 | 146 | type alias NodeLabel = 147 | { nodeText : NodeText 148 | , x : Int 149 | , y : Int 150 | } 151 | 152 | 153 | type NodeText 154 | = NodeText (Maybe BBox) String 155 | 156 | 157 | setBBoxOfNodeText : BBox -> NodeText -> NodeText 158 | setBBoxOfNodeText bbox (NodeText _ text) = 159 | NodeText (Just bbox) text 160 | 161 | 162 | nodeLabelToString : NodeLabel -> String 163 | nodeLabelToString nodeLabel = 164 | nodeTextToString nodeLabel.nodeText 165 | 166 | 167 | nodeTextToString : NodeText -> String 168 | nodeTextToString (NodeText _ string) = 169 | string 170 | 171 | 172 | type EdgeLabel 173 | = EdgeLabel (Maybe BBox) String 174 | 175 | 176 | setBBoxOfEdgeLabel : BBox -> EdgeLabel -> EdgeLabel 177 | setBBoxOfEdgeLabel bbox (EdgeLabel _ text) = 178 | EdgeLabel (Just bbox) text 179 | 180 | 181 | setEdgeText : String -> EdgeLabel -> EdgeLabel 182 | setEdgeText newText (EdgeLabel mbbox _) = 183 | EdgeLabel mbbox newText 184 | 185 | 186 | edgeLabelToString : EdgeLabel -> String 187 | edgeLabelToString (EdgeLabel _ string) = 188 | string 189 | 190 | 191 | type EditorMode 192 | = LayoutMode 193 | | EditMode EditState 194 | | DeletionMode 195 | 196 | 197 | type EditState 198 | = EditingNothing 199 | | CreatingEdge NodeId MousePosition 200 | | EditingEdgeLabel GraphEdge 201 | | EditingNodeLabel GraphNode 202 | 203 | 204 | getDraggedNodePosition : Drag -> NodeLabel -> NodeLabel 205 | getDraggedNodePosition { start, current } nodeLabel = 206 | { nodeLabel 207 | | x = nodeLabel.x + current.x - start.x 208 | 209 | -- Make it impossible to drag node outside of the canvas. 210 | -- For some reason this is only an issue when dragging past the TOP of viewport 211 | , y = max 0 (nodeLabel.y + current.y - start.y) 212 | } 213 | 214 | 215 | type alias BBox = 216 | { x : Float 217 | , y : Float 218 | , width : Float 219 | , height : Float 220 | } 221 | 222 | 223 | elementToBBox : Dom.Element -> BBox 224 | elementToBBox { element } = 225 | element 226 | -------------------------------------------------------------------------------- /src/View.elm: -------------------------------------------------------------------------------- 1 | module View exposing (focusLabelInput, view) 2 | 3 | import Browser.Dom as Dom 4 | import Canvas 5 | import Data.Layout as Layout 6 | import Export 7 | import Graph 8 | import GraphUtil 9 | import Html exposing (Html, button, div, form, h3, input, text, textarea) 10 | import Html.Attributes exposing (checked, class, classList, id, name, placeholder, readonly, rows, size, style, title, type_, value) 11 | import Html.Events exposing (onClick, onInput, onSubmit) 12 | import Markdown 13 | import Svg exposing (Svg) 14 | import Svg.Attributes exposing (fill) 15 | import SvgMouse 16 | import Task 17 | import Types exposing (Drag, EdgeLabel(..), EditState(..), EditorMode(..), ExportFormat(..), GraphEdge, GraphNode, ModalState(..), Model, ModelGraph, Msg(..), WindowSize, exportFormatToString, nodeLabelToString) 18 | 19 | 20 | view : Model -> Html Msg 21 | view { graph, draggedNode, editorMode, windowSize, modalState } = 22 | Html.div [] 23 | [ viewCanvas editorMode graph draggedNode windowSize 24 | , controlsPanel editorMode 25 | , viewNodeForm editorMode 26 | , viewEdgeForm editorMode graph 27 | , viewModal modalState graph 28 | ] 29 | 30 | 31 | viewCanvas : EditorMode -> ModelGraph -> Maybe Drag -> WindowSize -> Html Msg 32 | viewCanvas editorMode graph maybeDrag winSize = 33 | let 34 | canvasEventListeners = 35 | case editorMode of 36 | EditMode EditingNothing -> 37 | [ SvgMouse.onCanvasMouseDown CreateNode ] 38 | 39 | EditMode (CreatingEdge _ _) -> 40 | [ SvgMouse.onMouseUpUnselectStartNode ] 41 | 42 | _ -> 43 | [] 44 | 45 | svgElemAttributes = 46 | canvasEventListeners 47 | ++ [ style "width" (String.fromFloat winSize.width ++ "px") 48 | , style "height" (String.fromFloat winSize.height ++ "px") 49 | ] 50 | in 51 | Svg.svg svgElemAttributes <| 52 | if Graph.isEmpty graph then 53 | [ Canvas.positionedText 54 | (round <| winSize.width / 2) 55 | (round <| winSize.height / 2) 56 | "emptyGraphText" 57 | "Click anywhere to create the first node" 58 | [ fill "grey" ] 59 | ] 60 | 61 | else 62 | nonEmptyGraphView editorMode maybeDrag graph 63 | 64 | 65 | nonEmptyGraphView : EditorMode -> Maybe Drag -> ModelGraph -> List (Svg Msg) 66 | nonEmptyGraphView editorMode maybeDrag graph = 67 | let 68 | nodesView = 69 | Graph.nodes graph |> List.map (viewNode maybeDrag editorMode) 70 | 71 | edgesView = 72 | Graph.edges graph |> List.map (viewEdge graph maybeDrag editorMode) 73 | 74 | edgeBeingCreated = 75 | getEdgeBeingCreated editorMode graph 76 | in 77 | Canvas.svgDefs :: edgeBeingCreated :: edgesView ++ nodesView 78 | 79 | 80 | getEdgeBeingCreated : EditorMode -> ModelGraph -> Svg Msg 81 | getEdgeBeingCreated editorMode graph = 82 | case editorMode of 83 | EditMode (CreatingEdge nodeId mousePosition) -> 84 | let 85 | fromNode = 86 | GraphUtil.getNode nodeId graph 87 | in 88 | Canvas.drawEdge fromNode.label mousePosition.x mousePosition.y "edgeTextIdNotNeeded" dummyEdge [] 89 | 90 | _ -> 91 | Svg.text "" 92 | 93 | 94 | dummyEdge : GraphEdge 95 | dummyEdge = 96 | { from = 0, to = 0, label = EdgeLabel Nothing "" } 97 | 98 | 99 | viewNode : Maybe Drag -> EditorMode -> GraphNode -> Svg Msg 100 | viewNode mDrag editorMode node = 101 | let 102 | nodeMaybeAffectedByDrag = 103 | applyDrag mDrag node 104 | in 105 | Canvas.boxedText nodeMaybeAffectedByDrag editorMode 106 | 107 | 108 | viewEdge : ModelGraph -> Maybe Drag -> EditorMode -> GraphEdge -> Html Msg 109 | viewEdge graph mDrag editorMode edge = 110 | let 111 | fromNode = 112 | GraphUtil.getNode edge.from graph |> applyDrag mDrag 113 | 114 | toNode = 115 | GraphUtil.getNode edge.to graph |> applyDrag mDrag 116 | in 117 | Canvas.edgeArrow edge fromNode toNode editorMode 118 | 119 | 120 | applyDrag : Maybe Drag -> GraphNode -> GraphNode 121 | applyDrag mDrag node = 122 | case mDrag of 123 | Just drag -> 124 | if drag.nodeId == node.id then 125 | { node | label = Types.getDraggedNodePosition drag node.label } 126 | 127 | else 128 | node 129 | 130 | Nothing -> 131 | node 132 | 133 | 134 | viewNodeForm : EditorMode -> Html Msg 135 | viewNodeForm editorMode = 136 | case editorMode of 137 | EditMode (EditingNodeLabel node) -> 138 | nodeForm node 139 | 140 | _ -> 141 | Html.text "" 142 | 143 | 144 | viewEdgeForm : EditorMode -> ModelGraph -> Html Msg 145 | viewEdgeForm editorMode graph = 146 | case editorMode of 147 | EditMode (EditingEdgeLabel edge) -> 148 | edgeForm edge graph 149 | 150 | _ -> 151 | Html.text "" 152 | 153 | 154 | nodeForm : GraphNode -> Html Msg 155 | nodeForm { label } = 156 | let 157 | currentText = 158 | nodeLabelToString label 159 | in 160 | labelForm NodeLabelEdit NodeLabelEditConfirm "Node text" currentText label.x label.y 161 | 162 | 163 | edgeForm : GraphEdge -> ModelGraph -> Html Msg 164 | edgeForm ({ from, to } as edge) graph = 165 | let 166 | (EdgeLabel _ currentText) = 167 | edge.label 168 | 169 | -- The form edge is to be rendered at the center of the edge 170 | ( formX, formY ) = 171 | let 172 | fromNodeLabel = 173 | GraphUtil.getNode from graph |> .label 174 | 175 | toNodeLabel = 176 | GraphUtil.getNode to graph |> .label 177 | in 178 | ( (fromNodeLabel.x + toNodeLabel.x) // 2 179 | , (fromNodeLabel.y + toNodeLabel.y) // 2 180 | ) 181 | in 182 | labelForm EdgeLabelEdit EdgeLabelEditConfirm "Edge text" currentText formX formY 183 | 184 | 185 | labelForm : (String -> Msg) -> Msg -> String -> String -> Int -> Int -> Html Msg 186 | labelForm editMsg confirmMsg placeholderVal currentValue x y = 187 | form 188 | [ onSubmit confirmMsg 189 | , style "position" "absolute" 190 | 191 | -- 95 and 13 are hardcoded halves of the form size 190 x 30 192 | , style "left" (String.fromInt (x - 95) ++ "px") 193 | , style "top" (String.fromInt (y - 13) ++ "px") 194 | ] 195 | [ input [ type_ "text", placeholder placeholderVal, value currentValue, onInput editMsg, id labelInputId, size 15 ] [] 196 | , button [ onClick confirmMsg ] [ text "OK" ] 197 | ] 198 | 199 | 200 | controlsPanel : EditorMode -> Html Msg 201 | controlsPanel editorMode = 202 | div [ style "position" "absolute", style "top" "0", style "width" "100%" ] 203 | [ modeButtons editorMode 204 | , helpAndAboutButtons 205 | , if editorMode == LayoutMode then 206 | div [] 207 | [ layoutEngineButtons 208 | , zoomButtons 209 | ] 210 | 211 | else 212 | text "" 213 | ] 214 | 215 | 216 | modeButtons : EditorMode -> Html Msg 217 | modeButtons editorMode = 218 | let 219 | ( isEdit, isLayout, isDeletion ) = 220 | case editorMode of 221 | EditMode _ -> 222 | ( True, False, False ) 223 | 224 | LayoutMode -> 225 | ( False, True, False ) 226 | 227 | DeletionMode -> 228 | ( False, False, True ) 229 | in 230 | div [ class "btn-group m-2" ] 231 | [ modeButton isEdit "Create/Edit" (EditMode EditingNothing) 232 | , modeButton isLayout "Layout" LayoutMode 233 | , modeButton isDeletion "Delete" DeletionMode 234 | ] 235 | 236 | 237 | modeButton : Bool -> String -> EditorMode -> Html Msg 238 | modeButton isActive modeText mode = 239 | button 240 | [ type_ "button" 241 | , onClick (SetMode mode) 242 | , classList [ ( "btn", True ), ( "btn-primary", isActive ), ( "btn-secondary", not isActive ) ] 243 | ] 244 | [ text modeText ] 245 | 246 | 247 | layoutEngineButtons : Html Msg 248 | layoutEngineButtons = 249 | div [ class "btn-group btn-group-sm ml-2" ] <| 250 | List.map layoutEngineButton [ Layout.Dot, Layout.Circo, Layout.Fdp, Layout.Neato, Layout.Osage, Layout.Twopi ] 251 | 252 | 253 | layoutEngineButton : Layout.LayoutEngine -> Html Msg 254 | layoutEngineButton layoutEngine = 255 | button [ class "btn btn-secondary", onClick (PerformAutomaticLayout layoutEngine) ] 256 | [ text (Layout.engineToString layoutEngine) ] 257 | 258 | 259 | zoomButtons : Html Msg 260 | zoomButtons = 261 | div [ class "btn-group btn-group-sm ml-2" ] 262 | [ button [ class "btn btn-secondary", title "Shrink graph", onClick (Resize 0.9) ] [ text "➖" ] 263 | , button [ class "btn btn-secondary", title "Expand graph", onClick (Resize 1.1) ] [ text "➕" ] 264 | ] 265 | 266 | 267 | helpAndAboutButtons : Html Msg 268 | helpAndAboutButtons = 269 | div [ class "btn-group m-2", style "position" "absolute", style "right" "0px" ] 270 | [ button [ type_ "button", class "btn btn-secondary", onClick (ModalStateChange (Export Dot)) ] [ text "Export" ] 271 | , button [ type_ "button", class "btn btn-secondary", onClick (ModalStateChange Help) ] [ text "Help" ] 272 | , button [ type_ "button", class "btn btn-secondary", onClick (ModalStateChange About) ] [ text "About" ] 273 | ] 274 | 275 | 276 | viewModal : ModalState -> ModelGraph -> Html Msg 277 | viewModal modalState graph = 278 | case modalState of 279 | Hidden -> 280 | text "" 281 | 282 | Help -> 283 | modal "Help" helpContent 284 | 285 | About -> 286 | modal "About" aboutContent 287 | 288 | Export exportFormat -> 289 | modal "Export" <| exportModalBody exportFormat graph 290 | 291 | 292 | modal : String -> Html Msg -> Html Msg 293 | modal title body = 294 | div [] 295 | [ div [ class "modal fade show", style "display" "block" ] 296 | [ div [ class "modal-dialog" ] 297 | [ div [ class "modal-content" ] 298 | [ div [ class "modal-header" ] 299 | [ h3 [ class "modal-title" ] [ text title ] 300 | , button [ class "close", type_ "button", onClick (ModalStateChange Hidden) ] [ text "×" ] 301 | ] 302 | , div [ class "modal-body" ] 303 | [ body ] 304 | ] 305 | ] 306 | ] 307 | , div [ class "modal-backdrop fade show" ] [] 308 | ] 309 | 310 | 311 | exportModalBody : ExportFormat -> ModelGraph -> Html Msg 312 | exportModalBody currentFormat graph = 313 | let 314 | exportPreview = 315 | case currentFormat of 316 | Tgf -> 317 | Export.toTgf graph 318 | 319 | Dot -> 320 | Export.toDot graph 321 | in 322 | div [] 323 | [ div [] 324 | [ text "Format" 325 | , formatRadio currentFormat Dot 326 | , formatRadio currentFormat Tgf 327 | ] 328 | , div [] [ textarea [ style "width" "100%", readonly True, rows 15 ] [ text exportPreview ] ] 329 | , div [] [ button [ class "btn btn-primary float-right", onClick (Download currentFormat) ] [ text "Download" ] ] 330 | ] 331 | 332 | 333 | formatRadio : ExportFormat -> ExportFormat -> Html Msg 334 | formatRadio currentFormat desiredFormat = 335 | Html.label [] 336 | [ input 337 | [ type_ "radio" 338 | , name "exportFormat" 339 | , checked (currentFormat == desiredFormat) 340 | , onClick (ModalStateChange (Export desiredFormat)) 341 | , class "mx-1" 342 | ] 343 | [] 344 | , text <| exportFormatToString desiredFormat 345 | ] 346 | 347 | 348 | helpContent : Html a 349 | helpContent = 350 | Markdown.toHtml [] """ 351 | **Elm Graph Editor** is simple editor for creating [directed graphs](https://en.wikipedia.org/wiki/Directed_graph). 352 | It has three modes: *Create/Edit*, *Move* and *Delete*. 353 | Each modes makes different graph editing actions available. 354 | 355 | In **Create/Edit** mode you can 356 | * create new nodes by clicking on the canvas 357 | * edit node text by double clicking nodes. Enter confirms the edit. 358 | * create new edges by click & holding mouse button on a node, dragging and releasing mouse on target node. 359 | * edit edge text by double clicking edges. Enter confirms the edit. 360 | 361 | In **Layout** mode you can 362 | * move nodes by drag and drop 363 | * layout the entire graph automatically using one of the provided layout engines (based on [GraphViz](https://graphviz.gitlab.io/)). 364 | * move nodes closer together / further away from each other using - / + buttons. 365 | 366 | In **Delete** mode you can remove nodes and edges from the graph by just clicking them. Removing a node removes all its incoming and outgoing edges. 367 | """ 368 | 369 | 370 | aboutContent : Html a 371 | aboutContent = 372 | Markdown.toHtml [] """ 373 | **Elm Graph Editor** version 1.0.0 374 | 375 | Created by [Jan Hrček](http://janhrcek.cz) 376 | 377 | Implemented in [Elm](http://elm-lang.org/) 378 | 379 | Source code available on [GitHub](https://github.com/jhrcek/graph-editor) 380 | """ 381 | 382 | 383 | labelInputId : String 384 | labelInputId = 385 | "labelForm" 386 | 387 | 388 | focusLabelInput : Cmd Msg 389 | focusLabelInput = 390 | Task.attempt 391 | (\_ -> NoOp) 392 | (Dom.focus labelInputId) 393 | -------------------------------------------------------------------------------- /tests/VizJs.elm: -------------------------------------------------------------------------------- 1 | module VizJs exposing (suite) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import GraphViz.VizJs exposing (PlainGraph, PlainSource(..), parsePlainSource) 6 | import Parser exposing ((|.)) 7 | import Test exposing (..) 8 | 9 | 10 | suite : Test 11 | suite = 12 | describe "GraphViz.VizJs" 13 | [ describe "parsePlainSource" <| 14 | List.indexedMap mkTest testCases 15 | ] 16 | 17 | 18 | mkTest : Int -> ( PlainSource, PlainGraph ) -> Test 19 | mkTest testIndex ( input, expectedOutput ) = 20 | test ("Graph " ++ String.fromInt testIndex) <| 21 | \_ -> Expect.equal (Ok expectedOutput) (parsePlainSource input) 22 | 23 | 24 | testCases : List ( PlainSource, PlainGraph ) 25 | testCases = 26 | [ ( PlainSource 27 | """graph 1 0.75 1.5 28 | node 0 0.375 1.25 0.75 0.5 "" solid ellipse black lightgrey 29 | node 1 0.375 0.25 0.75 0.5 "" solid ellipse black lightgrey 30 | edge 0 1 4 0.375 0.99766 0.375 0.89071 0.375 0.76353 0.375 0.64468 solid black 31 | stop""" 32 | , { scale = 1 33 | , width = 0.75 34 | , height = 1.5 35 | , nodes = 36 | [ { nodeId = 0, x = 0.375, y = 1.25 } 37 | , { nodeId = 1, x = 0.375, y = 0.25 } 38 | ] 39 | } 40 | ) 41 | ] 42 | --------------------------------------------------------------------------------