├── .gitignore ├── DefaultImage.png ├── FlowSharpWeb.sln ├── PlaceholderProject.csproj ├── README.md ├── article ├── anchors1.png ├── anchors2.png ├── bufferzone.png ├── circles1.png ├── cp1.png ├── cp2.png ├── cp3.png ├── cp4.png ├── diamonds1.png ├── drawings.vsd ├── grid1.png ├── grid2.png ├── grid3.png ├── grid4.png ├── grid5.png ├── index-toc.htm ├── index.htm ├── line1.png ├── line2.png ├── line3.png ├── line4.png ├── line5.png ├── lineArrows.png ├── lineArrows2.png ├── lineArrows3.png ├── mvc1.png ├── rectangles1.png ├── saveload1.png ├── screenshot1.png ├── svgmodel.png ├── text1.png ├── toolbox1.png ├── toolbox2.png └── toolbox3.png ├── bugs.txt ├── controllers ├── anchorController.js ├── anchorGroupController.js ├── circleController.js ├── controller.js ├── diamondController.js ├── imageController.js ├── lineController.js ├── mouseController.js ├── objectsController.js ├── rectangleController.js ├── shapeController.js ├── surfaceController.js ├── textController.js ├── toolboxCircleController.js ├── toolboxDiamondController.js ├── toolboxGroupController.js ├── toolboxImageController.js ├── toolboxLineController.js ├── toolboxRectangleController.js ├── toolboxShapeController.js ├── toolboxSurfaceController.js └── toolboxTextController.js ├── event.js ├── fileSaver.js ├── flowSharpWeb.all.js ├── flowSharpWeb.all.js.min ├── flowSharpWeb.html ├── helpers.js ├── models ├── AnchorModel.js ├── circleModel.js ├── diagramModel.js ├── diamondModel.js ├── imageModel.js ├── lineModel.js ├── model.js ├── objectsModel.js ├── pathModel.js ├── rectangleModel.js ├── shapeModel.js ├── surfaceModel.js └── textModel.js ├── point.js ├── prototypes.js ├── svggrid8.html └── views ├── anchorView.js ├── lineView.js ├── objectsView.js ├── propertyGridView.js ├── shapeView.js ├── surfaceView.js ├── textView.js ├── toolboxSurfaceView.js ├── toolboxView.js └── view.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | /FlowSharpWeb.html-save 290 | /FlowSharpWeb.zip 291 | -------------------------------------------------------------------------------- /DefaultImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/DefaultImage.png -------------------------------------------------------------------------------- /FlowSharpWeb.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlaceholderProject", "PlaceholderProject.csproj", "{068F8DAA-98AD-4F9E-990C-F099BB8F0441}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {068F8DAA-98AD-4F9E-990C-F099BB8F0441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {068F8DAA-98AD-4F9E-990C-F099BB8F0441}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {068F8DAA-98AD-4F9E-990C-F099BB8F0441}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {068F8DAA-98AD-4F9E-990C-F099BB8F0441}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {803894E9-37A4-48F7-A04A-BED6FC2310DD} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /PlaceholderProject.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {068F8DAA-98AD-4F9E-990C-F099BB8F0441} 8 | Exe 9 | PlaceholderProject 10 | PlaceholderProject 11 | v4.6.1 12 | 512 13 | true 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlowSharpWeb 2 | Build a Prototype Web-Based Diagramming App with SVG and Javascript 3 | 4 | ![Screenshot](article/screenshot1.png?raw=true "Screenshot") 5 | 6 | This is an early prototype intended to be a learning excerise for how to create a web-based diagramming tool. 7 | At this stage, learning about how to use SVG dynamically is explored: 8 | 9 | * create a virtual surface 10 | * drop shapes onto the surface 11 | * move shapes around 12 | * resize shapes 13 | * connect shapes with simple lines 14 | 15 | The full article describing the implementation will be posted on CodeProject shortly. 16 | 17 | A JSFiddle of the code base published in the article is here: https://jsfiddle.net/cliftonm/o2ve65c9/ 18 | -------------------------------------------------------------------------------- /article/anchors1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/anchors1.png -------------------------------------------------------------------------------- /article/anchors2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/anchors2.png -------------------------------------------------------------------------------- /article/bufferzone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/bufferzone.png -------------------------------------------------------------------------------- /article/circles1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/circles1.png -------------------------------------------------------------------------------- /article/cp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/cp1.png -------------------------------------------------------------------------------- /article/cp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/cp2.png -------------------------------------------------------------------------------- /article/cp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/cp3.png -------------------------------------------------------------------------------- /article/cp4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/cp4.png -------------------------------------------------------------------------------- /article/diamonds1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/diamonds1.png -------------------------------------------------------------------------------- /article/drawings.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/drawings.vsd -------------------------------------------------------------------------------- /article/grid1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/grid1.png -------------------------------------------------------------------------------- /article/grid2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/grid2.png -------------------------------------------------------------------------------- /article/grid3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/grid3.png -------------------------------------------------------------------------------- /article/grid4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/grid4.png -------------------------------------------------------------------------------- /article/grid5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/grid5.png -------------------------------------------------------------------------------- /article/line1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/line1.png -------------------------------------------------------------------------------- /article/line2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/line2.png -------------------------------------------------------------------------------- /article/line3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/line3.png -------------------------------------------------------------------------------- /article/line4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/line4.png -------------------------------------------------------------------------------- /article/line5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/line5.png -------------------------------------------------------------------------------- /article/lineArrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/lineArrows.png -------------------------------------------------------------------------------- /article/lineArrows2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/lineArrows2.png -------------------------------------------------------------------------------- /article/lineArrows3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/lineArrows3.png -------------------------------------------------------------------------------- /article/mvc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/mvc1.png -------------------------------------------------------------------------------- /article/rectangles1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/rectangles1.png -------------------------------------------------------------------------------- /article/saveload1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/saveload1.png -------------------------------------------------------------------------------- /article/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/screenshot1.png -------------------------------------------------------------------------------- /article/svgmodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/svgmodel.png -------------------------------------------------------------------------------- /article/text1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/text1.png -------------------------------------------------------------------------------- /article/toolbox1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/toolbox1.png -------------------------------------------------------------------------------- /article/toolbox2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/toolbox2.png -------------------------------------------------------------------------------- /article/toolbox3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliftonm/FlowSharpWeb/9a13157d3e495cce1820b3a862e2166b4d4bfc2d/article/toolbox3.png -------------------------------------------------------------------------------- /bugs.txt: -------------------------------------------------------------------------------- 1 | Dragging the toolbox surface causes an offset in the location of a toolbox shape when the shape drag & drop is initiated. 2 | 3 | Circle shouldn't have an empty edit box in the property grid in the second column after the radius. 4 | Same with Text 5 | 6 | FIXED:Dragging text shape doesn't work! 7 | Issue was that the onPropertyChanged event call in textView.js was using the old style call rather than a dictionary format for parameters. 8 | 9 | Dragging shape from toolbox needs to immediately wire up the property grid. 10 | 11 | Properties for shapes that are paths, like diamonds. 12 | 13 | // ======================================== 14 | 15 | Sometimes an anchor (or two) disappears in when sizing the rectangle. 16 | 17 | -width & -height results in no rectangle appearing. 18 | 19 | keyboard l/r/u/d movement sort of works, until shape leaves mouse area and connection points appear in the wrong place after moving a line, so 20 | there's all sorts of weird side effects going on. 21 | 22 | ========================== 23 | 24 | scrolling surface quickly which causes the mouse to go over an element fires the surface leave event which stops scrolling. 25 | 26 | create circles within circles. Since the mouse leave doesn't fire for a particular circle, we only get the anchor points for the outer circle. 27 | Related: If you place a text shape inside another shape and try to move the text shape, the anchors of the outer shape move with the text! 28 | 29 | Why do I get "a is not defined" when I move parseTransform into helpers? 30 | 31 | Negative width/height for diamond shape doesn't render path correctly. Should prevent changing shape when width/height is negative. 32 | 33 | fixed: moving a shape quickly on the surface messes up the anchor translation 34 | fixed: multiple loads doesn't work (the file is already loaded, so it does nothing) 35 | fixed: double event trigger on mouse down on shape after load (but only 2, never more) 36 | fixed: shapes (even new ones dropped) can't be dragged after load 37 | fixed: after load, scrolling surface translates surface but not objects on surface 38 | fixed: inspect the svg element, why is the width and height so large? string/int concat issue? 39 | 40 | -------------------------------------------------------------------------------- /controllers/anchorController.js: -------------------------------------------------------------------------------- 1 | class AnchorController extends Controller { 2 | constructor(mouseController, view, model, shapeController, fncDragAnchor, anchorIdx) { 3 | super(mouseController, view, model); 4 | this.fncDragAnchor = fncDragAnchor; 5 | this.anchorIdx = anchorIdx; 6 | 7 | // Structure: 8 | // { id: shapeId, controller: shapeController, connectionPoints: connectionPoints[] } 9 | this.shapeConnectionPoints = []; 10 | 11 | // Save the controller that is associated with the shape for which we're 12 | // displaying the anchors, so we can later on see if any of the controllers allows 13 | // the anchors to be attached to connection points. Currently only the line 14 | // controller allows this. 15 | this.shapeController = shapeController; 16 | } 17 | 18 | get isAnchorController() { 19 | return true; 20 | } 21 | 22 | get hasConnectionPoints() { 23 | return false; 24 | } 25 | 26 | // We don't show anchors for anchors. 27 | // This wouldn't happen anyways because no anchors are returned, 28 | // but having this flag is a minor performance improvement, maybe. 29 | get shouldShowAnchors() { 30 | return false; 31 | } 32 | 33 | onDrag(dx, dy) { 34 | // Call into the shape controller to handle 35 | // the specific anchor drag. 36 | this.fncDragAnchor(dx, dy); 37 | this.showAnyConnectionPoints(); 38 | } 39 | 40 | onMouseUp(isClick) { 41 | super.onMouseUp(isClick); 42 | this.connectIfCloseToShapeConnectionPoint(); 43 | this.removeConnectionPoints(); 44 | this.shapeConnectionPoints = []; 45 | } 46 | 47 | showAnyConnectionPoints() { 48 | if (this.shapeController.canConnectToShapes) { 49 | var changes = this.getNewNearbyShapes(this.mouseController.x, this.mouseController.y); 50 | this.createConnectionPoints(changes.newShapes); 51 | 52 | // Other interesting approaches: 53 | // https://stackoverflow.com/questions/1885557/simplest-code-for-array-intersection-in-javascript 54 | // [...new Set(a)].filter(x => new Set(b).has(x)); 55 | var currentShapesId = changes.newShapes.concat(changes.existingShapes).map(ns => ns.id); 56 | 57 | var noLongerNearShapes = this.shapeConnectionPoints.filter(s => currentShapesId.indexOf(s.id) < 0); 58 | this.removeExpiredShapeConnectionPoints(noLongerNearShapes); 59 | 60 | // Remove any shapes from the shapeConnectionPoints that do not exist anymore. 61 | var existingShapesId = changes.existingShapes.map(ns => ns.id); 62 | this.shapeConnectionPoints = this.shapeConnectionPoints.filter(s => existingShapesId.indexOf(s.id) >= 0); 63 | 64 | // Add in the new shapes. 65 | this.shapeConnectionPoints = this.shapeConnectionPoints.concat(changes.newShapes); 66 | 67 | console.log("scp: " + this.shapeConnectionPoints.length + ", new: " + changes.newShapes.length + ", existing: " + existingShapesId.length); 68 | } 69 | } 70 | 71 | getNewNearbyShapes(x, y) { 72 | var newShapes = []; 73 | var existingShapes = []; 74 | var p = new Point(x, y); 75 | p = Helpers.translateToScreenCoordinate(p); 76 | var nearbyShapeEls = Helpers.getNearbyShapes(p); // .filter(s => s.outerHTML.split(" ")[0].substring(1) != "line"); 77 | // logging: 78 | // nearbyShapesEls.map(s => console.log(s.outerHTML.split(" ")[0].substring(1))); 79 | 80 | nearbyShapeEls.map(el => { 81 | // We use the parentElement because that's the ID of the shape controller in the mouseController. 82 | var controllers = this.mouseController.getControllersByElement(el.parentElement); 83 | 84 | if (controllers) { 85 | controllers.map(ctrl => { 86 | if (ctrl.hasConnectionPoints) { 87 | var shapeId = ctrl.view.actualId; 88 | 89 | // If it already exists in the list, don't add it again. 90 | if (!this.shapeConnectionPoints.any(cp => cp.id == shapeId)) { 91 | var connectionPoints = ctrl.getConnectionPoints(); 92 | newShapes.push({ id: shapeId, controller: ctrl, connectionPoints: connectionPoints }); 93 | } else { 94 | existingShapes.push({ id: shapeId }); 95 | } 96 | } 97 | }); 98 | } 99 | }); 100 | 101 | return { newShapes : newShapes, existingShapes: existingShapes }; 102 | } 103 | 104 | // "shapes" is a {id, controller, connectionPoints} structure 105 | createConnectionPoints(shapes) { 106 | var cpGroup = Helpers.getElement(Constants.SVG_CONNECTION_POINTS_ID); 107 | 108 | shapes.map(shape => { 109 | shape.connectionPoints.map(cpStruct => { 110 | var cp = cpStruct.connectionPoint; 111 | var el = Helpers.createElement("g", { connectingToShapeId: shape.id }); 112 | el.appendChild(Helpers.createElement("line", { x1: cp.x - 5, y1: cp.y - 5, x2: cp.x + 5, y2: cp.y + 5, fill: "#FFFFFF", stroke: "#000080", "stroke-width": 1 })); 113 | el.appendChild(Helpers.createElement("line", { x1: cp.x + 5, y1: cp.y - 5, x2: cp.x - 5, y2: cp.y + 5, fill: "#FFFFFF", stroke: "#000080", "stroke-width": 1 })); 114 | cpGroup.appendChild(el); 115 | }); 116 | }); 117 | } 118 | 119 | removeConnectionPoints() { 120 | var cpGroup = Helpers.getElement(Constants.SVG_CONNECTION_POINTS_ID); 121 | Helpers.removeChildren(cpGroup); 122 | } 123 | 124 | // "shapes" is a {id, controller, connectionPoints} structure 125 | removeExpiredShapeConnectionPoints(shapes) { 126 | shapes.map(shape => { 127 | // https://stackoverflow.com/a/16775485/2276361 128 | var nodes = document.querySelectorAll('[connectingtoshapeid="' + shape.id + '"]'); 129 | // or: Array.from(nodes); https://stackoverflow.com/a/36249012/2276361 130 | // https://stackoverflow.com/a/33822526/2276361 131 | [...nodes].map(node => { node.parentNode.removeChild(node) }); 132 | }); 133 | } 134 | 135 | connectIfCloseToShapeConnectionPoint() { 136 | var p = new Point(this.mouseController.x, this.mouseController.y); 137 | p = Helpers.translateToScreenCoordinate(p); 138 | 139 | var nearbyConnectionPoints = []; 140 | 141 | this.shapeConnectionPoints.filter(scp => { 142 | for (var i = 0; i < scp.connectionPoints.length; i++) { 143 | var cpStruct = scp.connectionPoints[i]; 144 | if (Helpers.isNear(cpStruct.connectionPoint, p, Constants.MAX_CP_NEAR)) { 145 | nearbyConnectionPoints.push({ shapeController: scp.controller, shapeCPIdx : i, connectionPoint : cpStruct.connectionPoint}); 146 | } 147 | } 148 | }); 149 | 150 | if (nearbyConnectionPoints.length == 1) { 151 | var ncp = nearbyConnectionPoints[0]; 152 | 153 | // The location of the connection point of the shape to which we're connecting. 154 | var p = ncp.connectionPoint; 155 | // Physical location of endpoint is without line and surface translations. 156 | p = p.translate(-this.shapeController.model.tx, -this.shapeController.model.ty); 157 | p = p.translate(-surfaceModel.tx, - surfaceModel.ty); 158 | // Move the endpoint of the shape from which we're connecting (the line) to this point. 159 | this.shapeController.connect(this.anchorIdx, p); 160 | diagramModel.connect(ncp.shapeController.view.id, this.shapeController.view.id, ncp.shapeCPIdx, this.anchorIdx); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /controllers/anchorGroupController.js: -------------------------------------------------------------------------------- 1 | class AnchorGroupController extends Controller { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | this.anchors = []; 5 | this.showingAnchors = false; 6 | } 7 | 8 | get hasConnectionPoints() { 9 | return false; 10 | } 11 | 12 | // Override, as we don't have events on the anchor group. 13 | wireUpEvents() { } 14 | 15 | // We need to set up a partial call so that we can include the anchor being dragged when we call 16 | // the drag method for moving the shape's anchor. At that point we also pass in the event data. 17 | partialOnDrag(anchors, anchorElement, onDrag) { 18 | return (function (anchors, anchorElement, onDrag) { 19 | return function (dx, dy) { onDrag(anchors, anchorElement, dx, dy); } 20 | })(anchors, anchorElement, onDrag); 21 | } 22 | 23 | showAnchors(shapeController) { 24 | this.anchors = shapeController.getAnchors(); 25 | this.anchors.views = []; // add view to the dictionary. 26 | this.showingAnchors = true; 27 | var anchorGroup = Helpers.getElement(Constants.SVG_ANCHORS_ID); 28 | // Reset any translation because the next mouse hover will set the anchors directly over the shape. 29 | this.model._tx = 0; 30 | this.model._ty = 0; 31 | this.model.setTranslate(0, 0); 32 | // We pass in the shape (which is also the surface) mouse controller so we can 33 | // handle when the shape or surface gets the mousemove event, which happens if 34 | // the user moves the mouse too quickly and the pointer leaves the anchor rectangle. 35 | 36 | // this.anchorController = new AnchorController(this); 37 | var anchorElements = []; 38 | var anchorModels = []; 39 | 40 | this.anchors.map(anchorDefinition => { 41 | var anchor = anchorDefinition.anchor; 42 | 43 | var model = new AnchorModel(); 44 | model._x = anchor.x - 5; 45 | model._y = anchor.y - 5; 46 | model._width = 10; 47 | model._height = 10; 48 | // TODO: Set other properties (fill, stroke, stroke-width, etc) 49 | 50 | var el = this.createElement("rect", { x: model.x, y: model.y, width: model.width, height: model.height, fill: "#FFFFFF", stroke: "#808080", "stroke-width": 0.5 }); 51 | 52 | anchorElements.push(el); 53 | anchorModels.push(model); 54 | anchorGroup.appendChild(el); 55 | }); 56 | 57 | // Separate iterator so we can pass in all the anchor elements to the onDrag callback once they've been accumulated. 58 | for (var i = 0; i < this.anchors.length; i++) { 59 | var anchorDefinition = this.anchors[i]; 60 | // Create anchor shape, associate it with a generic model, view, and the supplied shapeController. 61 | // Wire up anchor onDrag event and attach the view-controller to the mouse controller's list of shapes. 62 | // Note that this will now result in the shape receiving onenter/onleave events for the shape itself when the 63 | // user mouses over the anchor shape! The mouse controller handles this. 64 | var el = anchorElements[i]; 65 | 66 | // Helpful for debugging 67 | el.setAttribute("id", "anchor" + i); 68 | 69 | var anchorView = new View(el, anchorModels[i]); 70 | var fncDragAnchor = this.partialOnDrag(anchorModels, anchorModels[i], anchorDefinition.onDrag); 71 | var anchorController = new AnchorController(this.mouseController, anchorView, anchorModels[i], shapeController, fncDragAnchor, i); 72 | this.mouseController.attach(anchorView, anchorController); 73 | this.anchors.views.push(anchorView); // Save the view for when we need to destroy the individual anchors. 74 | } 75 | } 76 | 77 | // TODO: Very similar to SvgToolboxElement.createElement. Refactor for common helper class? 78 | createElement(name, attributes) { 79 | var svgns = "http://www.w3.org/2000/svg"; 80 | var el = document.createElementNS(svgns, name); 81 | el.setAttribute("id", Helpers.uuidv4()); 82 | Object.entries(attributes).map(([key, val]) => el.setAttribute(key, val)); 83 | 84 | return el; 85 | } 86 | 87 | removeAnchors() { 88 | var anchorGroup = Helpers.getElement(Constants.SVG_ANCHORS_ID); 89 | 90 | // https://stackoverflow.com/questions/3955229/remove-all-child-elements-of-a-dom-node-in-javascript 91 | // Will change later. 92 | anchorGroup.innerHTML = ""; 93 | this.anchors.views.map(view => this.mouseController.destroy(view)); 94 | this.anchors = []; 95 | this.showingAnchors = false; 96 | // this.anchorController.destroyAll(); 97 | // Alternatively: 98 | //while (anchorGroup.firstChild) { 99 | // anchorGroup.removeChild(anchorGroup.firstChild); 100 | //} 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /controllers/circleController.js: -------------------------------------------------------------------------------- 1 | class CircleController extends ShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | getAnchors() { 7 | var corners = this.getCorners(); 8 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 9 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 10 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 11 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 12 | 13 | var anchors = [ 14 | { anchor: middleTop, onDrag: this.topMove.bind(this) }, 15 | { anchor: middleBottom, onDrag: this.bottomMove.bind(this) }, 16 | { anchor: middleLeft, onDrag: this.leftMove.bind(this) }, 17 | { anchor: middleRight, onDrag: this.rightMove.bind(this) } 18 | ]; 19 | 20 | return anchors; 21 | } 22 | 23 | getConnectionPoints() { 24 | var corners = this.getCorners(); 25 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 26 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 27 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 28 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 29 | 30 | var connectionPoints = [ 31 | { connectionPoint: middleTop }, 32 | { connectionPoint: middleBottom }, 33 | { connectionPoint: middleLeft }, 34 | { connectionPoint: middleRight } 35 | ]; 36 | 37 | return connectionPoints; 38 | } 39 | 40 | getULCorner() { 41 | var p = new Point(this.model.cx - this.model.r, this.model.cy - this.model.r); 42 | p = this.getAbsoluteLocation(p); 43 | 44 | return p; 45 | } 46 | 47 | getLRCorner() { 48 | var p = new Point(this.model.cx + this.model.r, this.model.cy + this.model.r); 49 | p = this.getAbsoluteLocation(p); 50 | 51 | return p; 52 | } 53 | 54 | topMove(anchors, anchor, dx, dy) { 55 | this.changeRadius(-dy); 56 | this.moveAnchor(anchors[0], 0, dy); 57 | this.moveAnchor(anchors[1], 0, -dy); 58 | this.moveAnchor(anchors[2], dy, 0); 59 | this.moveAnchor(anchors[3], -dy, 0); 60 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 0); 61 | this.adjustConnectorsAttachedToConnectionPoint(0, -dy, 1); 62 | this.adjustConnectorsAttachedToConnectionPoint(dy, 0, 2); 63 | this.adjustConnectorsAttachedToConnectionPoint(-dy, 0, 3); 64 | } 65 | 66 | bottomMove(anchors, anchor, dx, dy) { 67 | this.changeRadius(dy); 68 | this.moveAnchor(anchors[0], 0, -dy); 69 | this.moveAnchor(anchors[1], 0, dy); 70 | this.moveAnchor(anchors[2], -dy, 0); 71 | this.moveAnchor(anchors[3], dy, 0); 72 | this.adjustConnectorsAttachedToConnectionPoint(0, -dy, 0); 73 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 1); 74 | this.adjustConnectorsAttachedToConnectionPoint(-dy, 0, 2); 75 | this.adjustConnectorsAttachedToConnectionPoint(dy, 0, 3); 76 | } 77 | 78 | leftMove(anchors, anchor, dx, dy) { 79 | this.changeRadius(-dx); 80 | this.moveAnchor(anchors[0], 0, dx); 81 | this.moveAnchor(anchors[1], 0, -dx); 82 | this.moveAnchor(anchors[2], dx, 0); 83 | this.moveAnchor(anchors[3], -dx, 0); 84 | this.adjustConnectorsAttachedToConnectionPoint(0, dx, 0); 85 | this.adjustConnectorsAttachedToConnectionPoint(0, -dx, 1); 86 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 2); 87 | this.adjustConnectorsAttachedToConnectionPoint(-dx, 0, 3); 88 | } 89 | 90 | rightMove(anchors, anchor, dx, dy) { 91 | this.changeRadius(dx); 92 | this.moveAnchor(anchors[0], 0, -dx); 93 | this.moveAnchor(anchors[1], 0, dx); 94 | this.moveAnchor(anchors[2], -dx, 0); 95 | this.moveAnchor(anchors[3], dx, 0); 96 | this.adjustConnectorsAttachedToConnectionPoint(0, -dx, 0); 97 | this.adjustConnectorsAttachedToConnectionPoint(0, dx, 1); 98 | this.adjustConnectorsAttachedToConnectionPoint(-dx, 0, 2); 99 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 3); 100 | } 101 | 102 | changeRadius(amt) { 103 | this.model.r = this.model.r + amt; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /controllers/controller.js: -------------------------------------------------------------------------------- 1 | class Controller { 2 | constructor(mouseController, view, model) { 3 | this.mouseController = mouseController; 4 | this.view = view; 5 | this.model = model; 6 | this.events = []; 7 | this.wireUpEvents(); 8 | } 9 | 10 | get isSurfaceController() { 11 | return false; 12 | } 13 | 14 | get isAnchorController() { 15 | return false; 16 | } 17 | 18 | get isToolboxShapeController() { 19 | return false; 20 | } 21 | 22 | get shouldShowAnchors() { 23 | return true; 24 | } 25 | 26 | get hasConnectionPoints() { 27 | return true; 28 | } 29 | 30 | registerEvent(element, eventName, callbackRef) { 31 | this.events.push({ element: element, eventName: eventName, callbackRef: callbackRef }); 32 | } 33 | 34 | destroy() { 35 | this.unhookEvents(); 36 | } 37 | 38 | registerEventListener(element, eventName, callback, self) { 39 | var ref; 40 | 41 | if (self == null || self === undefined) { 42 | self = this; 43 | } 44 | 45 | element.addEventListener(eventName, ref = callback.bind(self)); 46 | this.registerEvent(element, eventName, ref); 47 | } 48 | 49 | unhookEvents() { 50 | for (var i = 0; i < this.events.length; i++) { 51 | var event = this.events[i]; 52 | event.element.removeEventListener(event.eventName, event.callbackRef); 53 | } 54 | 55 | this.events = []; 56 | } 57 | 58 | wireUpEvents() { 59 | this.registerEventListener(this.view.svgElement, "mousedown", this.mouseController.onMouseDown, this.mouseController); 60 | this.registerEventListener(this.view.svgElement, "mouseup", this.mouseController.onMouseUp, this.mouseController); 61 | this.registerEventListener(this.view.svgElement, "mousemove", this.mouseController.onMouseMove, this.mouseController); 62 | this.registerEventListener(this.view.svgElement, "mouseenter", this.mouseController.onMouseEnter, this.mouseController); 63 | this.registerEventListener(this.view.svgElement, "mouseleave", this.mouseController.onMouseLeave, this.mouseController); 64 | } 65 | 66 | getAbsoluteLocation(p) { 67 | p = p.translate(this.model.tx, this.model.ty); 68 | p = p.translate(surfaceModel.tx, surfaceModel.ty); 69 | 70 | return p; 71 | } 72 | 73 | getRelativeLocation(p) { 74 | p = p.translate(-this.model.tx, -this.model.ty); 75 | p = p.translate(-surfaceModel.tx, -surfaceModel.ty); 76 | 77 | return p; 78 | } 79 | 80 | // Routed from mouse controller: 81 | 82 | onMouseEnter() { } 83 | 84 | onMouseLeave() { } 85 | 86 | onMouseDown() { } 87 | 88 | onMouseUp() { } 89 | 90 | // Default behavior 91 | onDrag(dx, dy) 92 | { 93 | this.model.translate(dx, dy); 94 | this.adjustConnections(dx, dy); 95 | } 96 | 97 | // Adjust all connectors connecting to this shape. 98 | adjustConnections(dx, dy) { 99 | var connections = diagramModel.connections.filter(c => c.shapeId == this.view.id); 100 | connections.map(c => { 101 | // TODO: Sort of nasty assumption here that the first controller is the line controller 102 | var lineController = this.mouseController.getControllersById(c.lineId)[0]; 103 | lineController.translateEndpoint(c.lineAnchorIdx, dx, dy); 104 | }); 105 | } 106 | 107 | // Adjust the connectors connecting to this shape's connection point. 108 | adjustConnectorsAttachedToConnectionPoint(dx, dy, cpIdx) { 109 | var connections = diagramModel.connections.filter(c => c.shapeId == this.view.id && c.shapeCPIdx == cpIdx); 110 | connections.map(c => { 111 | // TODO: Sort of nasty assumption here that the first controller is the line controller 112 | var lineController = this.mouseController.getControllersById(c.lineId)[0]; 113 | lineController.translateEndpoint(c.lineAnchorIdx, dx, dy); 114 | }); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /controllers/diamondController.js: -------------------------------------------------------------------------------- 1 | class DiamondController extends ShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | getAnchors() { 7 | var corners = this.getCorners(); 8 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 9 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 10 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 11 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 12 | 13 | var anchors = [ 14 | { anchor: middleTop, onDrag: this.topMove.bind(this) }, 15 | { anchor: middleBottom, onDrag: this.bottomMove.bind(this) }, 16 | { anchor: middleLeft, onDrag: this.leftMove.bind(this) }, 17 | { anchor: middleRight, onDrag: this.rightMove.bind(this) } 18 | ]; 19 | 20 | return anchors; 21 | } 22 | 23 | getConnectionPoints() { 24 | var corners = this.getCorners(); 25 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 26 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 27 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 28 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 29 | 30 | var connectionPoints = [ 31 | { connectionPoint: middleTop }, 32 | { connectionPoint: middleBottom }, 33 | { connectionPoint: middleLeft }, 34 | { connectionPoint: middleRight } 35 | ]; 36 | 37 | return connectionPoints; 38 | } 39 | 40 | getULCorner() { 41 | var rect = this.view.svgElement.getBoundingClientRect(); 42 | var p = new Point(rect.left, rect.top); 43 | p = Helpers.translateToSvgCoordinate(p); 44 | 45 | return p; 46 | } 47 | 48 | getLRCorner() { 49 | var rect = this.view.svgElement.getBoundingClientRect(); 50 | var p = new Point(rect.right, rect.bottom); 51 | p = Helpers.translateToSvgCoordinate(p); 52 | 53 | return p; 54 | } 55 | 56 | topMove(anchors, anchor, dx, dy) { 57 | var ulCorner = this.getULCorner(); 58 | var lrCorner = this.getLRCorner(); 59 | this.changeHeight(ulCorner, lrCorner, -dy); 60 | this.moveAnchor(anchors[0], 0, dy); // top 61 | this.moveAnchor(anchors[1], 0, -dy); // bottom 62 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 0); 63 | this.adjustConnectorsAttachedToConnectionPoint(0, -dy, 1); 64 | } 65 | 66 | bottomMove(anchors, anchor, dx, dy) { 67 | var ulCorner = this.getULCorner(); 68 | var lrCorner = this.getLRCorner(); 69 | this.changeHeight(ulCorner, lrCorner, dy); 70 | this.moveAnchor(anchors[0], 0, -dy); 71 | this.moveAnchor(anchors[1], 0, dy); 72 | this.adjustConnectorsAttachedToConnectionPoint(0, -dy, 0); 73 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 1); 74 | } 75 | 76 | leftMove(anchors, anchor, dx, dy) { 77 | var ulCorner = this.getULCorner(); 78 | var lrCorner = this.getLRCorner(); 79 | this.changeWidth(ulCorner, lrCorner, -dx); 80 | this.moveAnchor(anchors[2], dx, 0); 81 | this.moveAnchor(anchors[3], -dx, 0); 82 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 2); 83 | this.adjustConnectorsAttachedToConnectionPoint(-dx, 0, 3); 84 | } 85 | 86 | rightMove(anchors, anchor, dx, dy) { 87 | var ulCorner = this.getULCorner(); 88 | var lrCorner = this.getLRCorner(); 89 | this.changeWidth(ulCorner, lrCorner, dx); 90 | this.moveAnchor(anchors[2], -dx, 0); 91 | this.moveAnchor(anchors[3], dx, 0); 92 | this.adjustConnectorsAttachedToConnectionPoint(-dx, 0, 2); 93 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 3); 94 | } 95 | 96 | changeWidth(ulCorner, lrCorner, dx) { 97 | ulCorner.x -= dx; 98 | lrCorner.x += dx; 99 | this.updatePath(ulCorner, lrCorner); 100 | } 101 | 102 | changeHeight(ulCorner, lrCorner, dy) { 103 | ulCorner.y -= dy; 104 | lrCorner.y += dy; 105 | this.updatePath(ulCorner, lrCorner); 106 | } 107 | 108 | updatePath(ulCorner, lrCorner) { 109 | // example path: d: "M 240 100 L 210 130 L 240 160 L 270 130 Z" 110 | var ulCorner = this.getRelativeLocation(ulCorner); 111 | var lrCorner = this.getRelativeLocation(lrCorner); 112 | var mx = (ulCorner.x + lrCorner.x) / 2; 113 | var my = (ulCorner.y + lrCorner.y) / 2; 114 | var path = "M " + mx + " " + ulCorner.y; 115 | path = path + " L " + ulCorner.x + " " + my; 116 | path = path + " L " + mx + " " + lrCorner.y; 117 | path = path + " L " + lrCorner.x + " " + my; 118 | path = path + " Z" 119 | this.model.d = path; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /controllers/imageController.js: -------------------------------------------------------------------------------- 1 | class ImageController extends ShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | getAnchors() { 7 | var corners = this.getCorners(); 8 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 9 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 10 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 11 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 12 | //var upperRight = new Point(corners[1].X, corners[0].Y); 13 | //var lowerLeft = new Point(corners[0].X, corners[1].Y); 14 | 15 | // maybe later: 16 | // var anchors = [corners[0], corners[1], middleTop, middleBottom, middleLeft, middleRight, upperRight, lowerLeft]; 17 | var anchors = [ 18 | { anchor: middleTop, onDrag: this.topMove.bind(this) }, 19 | { anchor: middleBottom, onDrag: this.bottomMove.bind(this) }, 20 | { anchor: middleLeft, onDrag: this.leftMove.bind(this) }, 21 | { anchor: middleRight, onDrag: this.rightMove.bind(this) } 22 | ]; 23 | 24 | return anchors; 25 | } 26 | 27 | getConnectionPoints() { 28 | var corners = this.getCorners(); 29 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 30 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 31 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 32 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 33 | //var upperRight = new Point(corners[1].X, corners[0].Y); 34 | //var lowerLeft = new Point(corners[0].X, corners[1].Y); 35 | 36 | // maybe later: 37 | // var anchors = [corners[0], corners[1], middleTop, middleBottom, middleLeft, middleRight, upperRight, lowerLeft]; 38 | var connectionPoints = [ 39 | { connectionPoint: middleTop }, 40 | { connectionPoint: middleBottom }, 41 | { connectionPoint: middleLeft }, 42 | { connectionPoint: middleRight } 43 | ]; 44 | 45 | return connectionPoints; 46 | } 47 | 48 | getULCorner() { 49 | var p = new Point(this.model.x, this.model.y); 50 | p = this.getAbsoluteLocation(p); 51 | 52 | return p; 53 | } 54 | 55 | getLRCorner() { 56 | var p = new Point(this.model.x + this.model.width, this.model.y + this.model.height); 57 | p = this.getAbsoluteLocation(p); 58 | 59 | return p; 60 | } 61 | 62 | topMove(anchors, anchor, dx, dy) { 63 | // Moving the top affects "y" and "height" 64 | var y = this.model.y + dy; 65 | var height = this.model.height - dy; 66 | this.model.y = y; 67 | this.model.height = height; 68 | this.moveAnchor(anchors[0], 0, dy); 69 | this.adjustAnchorY(anchors[2], dy / 2); 70 | this.adjustAnchorY(anchors[3], dy / 2); 71 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 0); 72 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 2); 73 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 3); 74 | } 75 | 76 | bottomMove(anchors, anchor, dx, dy) { 77 | // Moving the bottom affects only "height" 78 | var height = this.model.height + dy; 79 | this.model.height = height; 80 | this.moveAnchor(anchors[1], 0, dy); 81 | this.adjustAnchorY(anchors[2], dy / 2); 82 | this.adjustAnchorY(anchors[3], dy / 2); 83 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 1); 84 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 2); 85 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 3); 86 | } 87 | 88 | leftMove(anchors, anchor, dx, dy) { 89 | // Moving the left affects "x" and "width" 90 | var x = this.model.x + dx; 91 | var width = this.model.width - dx; 92 | this.model.x = x; 93 | this.model.width = width; 94 | this.moveAnchor(anchors[2], dx, 0); 95 | this.adjustAnchorX(anchors[0], dx / 2); 96 | this.adjustAnchorX(anchors[1], dx / 2); 97 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 2); 98 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 0); 99 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 1); 100 | } 101 | 102 | rightMove(anchors, anchor, dx, dy) { 103 | // Moving the right affects only "width" 104 | var width = this.model.width + dx; 105 | this.model.width = width; 106 | this.moveAnchor(anchors[3], dx, 0); 107 | this.adjustAnchorX(anchors[0], dx / 2); 108 | this.adjustAnchorX(anchors[1], dx / 2); 109 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 3); 110 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 0); 111 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 1); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /controllers/lineController.js: -------------------------------------------------------------------------------- 1 | class LineController extends ShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get canConnectToShapes() { 7 | return true; 8 | } 9 | 10 | onDrag(dx, dy) { 11 | super.onDrag(dx, dy); 12 | // When the entire line is being dragged, we disconnect any connections. 13 | diagramModel.disconnect(this.view.id, 0); 14 | diagramModel.disconnect(this.view.id, 1); 15 | } 16 | 17 | // Move the specified endpoint (by idx) to the point p. 18 | connect(idx, p) { 19 | switch (idx) { 20 | case 0: 21 | this.model.x1 = p.x; 22 | this.model.y1 = p.y; 23 | break; 24 | case 1: 25 | this.model.x2 = p.x; 26 | this.model.y2 = p.y; 27 | break; 28 | } 29 | } 30 | 31 | translateEndpoint(idx, dx, dy) { 32 | switch (idx) { 33 | case 0: 34 | var p = new Point(this.model.x1, this.model.y1); 35 | p = p.translate(dx, dy); 36 | this.model.x1 = p.x; 37 | this.model.y1 = p.y; 38 | break; 39 | case 1: 40 | var p = new Point(this.model.x2, this.model.y2); 41 | p = p.translate(dx, dy); 42 | this.model.x2 = p.x; 43 | this.model.y2 = p.y; 44 | break; 45 | } 46 | } 47 | 48 | getAnchors() { 49 | var corners = this.getCorners(); 50 | var anchors = [ 51 | { anchor: corners[0], onDrag: this.moveULCorner.bind(this) }, 52 | { anchor: corners[1], onDrag: this.moveLRCorner.bind(this) }]; 53 | 54 | return anchors; 55 | } 56 | 57 | getULCorner() { 58 | var p = new Point(this.model.x1, this.model.y1); 59 | p = this.getAbsoluteLocation(p); 60 | 61 | return p; 62 | } 63 | 64 | getLRCorner() { 65 | var p = new Point(this.model.x2, this.model.y2); 66 | p = this.getAbsoluteLocation(p); 67 | 68 | return p; 69 | } 70 | 71 | // Move the (x1, y1) coordinate. 72 | moveULCorner(anchors, anchor, dx, dy) { 73 | this.model.x1 = this.model.x1 + dx; 74 | this.model.y1 = this.model.y1 + dy; 75 | this.moveAnchor(anchor, dx, dy); 76 | diagramModel.disconnect(this.view.id, 0); 77 | } 78 | 79 | // Move the (x2, y2) coordinate. 80 | moveLRCorner(anchors, anchor, dx, dy) { 81 | this.model.x2 = this.model.x2 + dx; 82 | this.model.y2 = this.model.y2 + dy; 83 | this.moveAnchor(anchor, dx, dy); 84 | diagramModel.disconnect(this.view.id, 1); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /controllers/mouseController.js: -------------------------------------------------------------------------------- 1 | const LEFT_MOUSE_BUTTON = 0; 2 | const TOOLBOX_DRAG_MIN_MOVE = 3; 3 | 4 | class MouseController { 5 | constructor() { 6 | this.mouseDown = false; 7 | this.controllers = {}; 8 | this.activeControllers = null; 9 | this.currentHoverControllers = []; 10 | this.leavingId = -1; 11 | this.draggingToolboxShape = false; 12 | this.selectedControllers = null; 13 | this.selectedShapeId = null; 14 | this.hoverShapeId = null; 15 | this.actualModel = null; 16 | 17 | this.eventShapeSelected = new Event(); 18 | 19 | // We really can't use movementX and movementY of the event because 20 | // when the user moves the mouse quickly, the move events switch from 21 | // the shape to the surface (or another shape) and this causes deviances 22 | // in the movementX and movementY so that the shape is no longer positioned 23 | // at the same location as when clicked down. 24 | this.x = 0; 25 | this.y = 0; 26 | this.dx = 0; 27 | this.dy = 0; 28 | } 29 | 30 | // Attach as many controllers as you want to the view. 31 | attach(view, controller) { 32 | var id = view.id; 33 | 34 | if (this.controllers[id] == undefined) { 35 | this.controllers[id] = []; 36 | } 37 | 38 | this.controllers[id].push(controller); 39 | } 40 | 41 | // Compare functions detach with destroyAll. 42 | // We should probably implement a "destroy" method as well. 43 | 44 | // Detach all controllers associated with this view. 45 | detach(view) { 46 | var id = view.id; 47 | delete this.controllers[id]; 48 | } 49 | 50 | detachAll() { 51 | this.controllers = {}; 52 | } 53 | 54 | destroy(view) { 55 | var id = view.id; 56 | this.controllers[id].map(controller=>controller.destroy()); 57 | delete this.controllers[id]; 58 | } 59 | 60 | destroyShapeById(id) { 61 | this.controllers[id].map(controller => controller.destroy()); 62 | delete this.controllers[id]; 63 | } 64 | 65 | // Detaches all controllers and unwires events associated with the controller. 66 | destroyAll() { 67 | Object.entries(this.controllers).map(([key, val]) => val.map(v => v.destroy())); 68 | this.controllers = {}; 69 | } 70 | 71 | destroyAllButSurface() { 72 | Object.entries(this.controllers).map(([key, val]) => { 73 | val.map(v => { 74 | // Don't remove surface, toolbox, objects group, or toolbox shapes. 75 | if (!v.isSurfaceController && !v.isToolboxShapeController) { 76 | v.destroy(); 77 | // Hopefully deleting the dictionary entry while iterating won't be 78 | // a disaster since we called Object.entries! 79 | delete this.controllers[key]; 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | get isClick() { 86 | var endDownX = this.x; 87 | var endDownY = this.y; 88 | 89 | var isClick = Math.abs(this.startDownX - endDownX) < TOOLBOX_DRAG_MIN_MOVE && 90 | Math.abs(this.startDownY - endDownY) < TOOLBOX_DRAG_MIN_MOVE; 91 | 92 | return isClick; 93 | } 94 | 95 | onKeyDown(evt) { 96 | var isOverShape = this.hoverShapeId != null; 97 | var handled = false; 98 | 99 | if (isOverShape) { 100 | switch (evt.keyCode) { 101 | case Constants.KEY_RIGHT: 102 | this.currentHoverControllers.map(c => c.onDrag(1, 0)); 103 | handled = true; 104 | break; 105 | case Constants.KEY_UP: 106 | this.currentHoverControllers.map(c => c.onDrag(0, -1)); 107 | handled = true; 108 | break; 109 | case Constants.KEY_LEFT: 110 | this.currentHoverControllers.map(c => c.onDrag(-1, 0)); 111 | handled = true; 112 | break; 113 | case Constants.KEY_DOWN: 114 | this.currentHoverControllers.map(c => c.onDrag(0, 1)); 115 | handled = true; 116 | break; 117 | case Constants.KEY_DELETE: 118 | // Mouse is "leaving" this control, this removes any anchors. 119 | this.currentHoverControllers.map(c => c.onMouseLeave()); 120 | // Remove shape from diagram model, and all connections of this shape. 121 | diagramModel.removeShape(this.hoverShapeId); 122 | // Remove shape from mouse controller and detach events. 123 | this.destroyShapeById(this.hoverShapeId); 124 | // Remove from "objects" collection. 125 | var el = Helpers.getElement(this.hoverShapeId); 126 | el.parentNode.removeChild(el); 127 | // Cleanup. 128 | this.currentHoverControllers = []; 129 | this.hoverShapeId = null; 130 | handled = true; 131 | break; 132 | } 133 | } 134 | 135 | return isOverShape && handled; 136 | } 137 | 138 | // Get the controller associated with the event and remember where the user clicked. 139 | onMouseDown(evt) { 140 | if (evt.button == LEFT_MOUSE_BUTTON) { 141 | evt.preventDefault(); 142 | var id = evt.currentTarget.getAttribute("id"); 143 | this.selectedShapeId = id; 144 | this.activeControllers = this.controllers[id]; 145 | this.selectedControllers = this.controllers[id]; 146 | this.mouseDown = true; 147 | this.startDownX = evt.clientX; 148 | this.startDownY = evt.clientY; 149 | this.x = evt.clientX; 150 | this.y = evt.clientY; 151 | this.activeControllers.map(c => c.onMouseDown()); 152 | 153 | // A bit annoying -- if this is a toolbox shape user clicked on, we need to use the 154 | // toolbox model until the user drops the control on the surface and the real shape 155 | // is created. 156 | if (this.activeControllers.any(ctrl => ctrl.isToolboxShapeController)) { 157 | // Here we assume the toolbox controller we want is the first controller. 158 | this.eventShapeSelected.fire(this, { model: this.activeControllers[0].model, shapeId: this.activeControllers[0].shapeName }); 159 | } else { 160 | // Here we also handle the user clicking on the surface instead of a shape. 161 | if (this.activeControllers[0].isSurfaceController) { 162 | this.eventShapeSelected.fire(this, { model: this.activeControllers[0].model, shapeId: this.activeControllers[0].shapeName }); 163 | } else { 164 | this.eventShapeSelected.fire(this, { model: this.actualModel, shapeId: this.actualModel.shapeId }) 165 | } 166 | } 167 | } 168 | } 169 | 170 | // If the user is dragging, call the controller's onDrag function. 171 | onMouseMove(evt) { 172 | evt.preventDefault(); 173 | if (this.mouseDown && this.activeControllers != null) { 174 | this.dx = evt.clientX - this.x; 175 | this.dy = evt.clientY - this.y; 176 | this.x = evt.clientX; 177 | this.y = evt.clientY; 178 | this.activeControllers.map(c => c.onDrag(this.dx, this.dy)); 179 | } 180 | } 181 | 182 | onMouseUp(evt) { 183 | evt.preventDefault(); 184 | if (evt.button == LEFT_MOUSE_BUTTON && this.activeControllers != null) { 185 | this.selectedShapeId = null; 186 | this.x = evt.clientX; 187 | this.y = evt.clientY; 188 | var isClick = this.isClick; 189 | 190 | this.activeControllers.map(c => c.onMouseUp(isClick)); 191 | this.clearSelectedObject(); 192 | 193 | // Do this after the mouseDown flag is reset, otherwise anchors won't appear. 194 | if (this.draggingToolboxShape) { 195 | // shapeBeingDraggedAndDropped is set by the ToolboxShapeController. 196 | // We preserve this shape in case the user releases the mouse button 197 | // while the mouse is over a different shape (like the surface) as 198 | // as result of a very fast drag & drop where the shape hasn't caught 199 | // up with the mouse, or the mouse is outside of shape's boundaries. 200 | this.finishDragAndDrop(this.shapeBeingDraggedAndDropped, evt.currentTarget); 201 | } 202 | } 203 | } 204 | 205 | onMouseEnter(evt) { 206 | evt.preventDefault(); 207 | var id = evt.currentTarget.getAttribute("id"); 208 | this.hoverShapeId = id; 209 | this.updateActualModelBeingEntered(id); 210 | 211 | if (this.mouseDown) { 212 | // Doing a drag operation, so ignore shapes we enter and leave so 213 | // that even if the mouse moves over another shape, we keep track 214 | // of the shape we're dragging. 215 | } else { 216 | // Hover management. We're usually always leaving some shape, including the surface. 217 | if (this.leavingId != -1) { 218 | console.log("Leaving " + this.leavingId); 219 | 220 | // If we're entering an anchor, don't leave anything as we want to preserve the anchors. 221 | if (!this.controllers[id][0].isAnchorController) { 222 | this.currentHoverControllers.map(c => c.onMouseLeave()); 223 | console.log("Entering " + id + " => " + this.controllers[id].map(ctrl=>ctrl.constructor.name).join(", ")); 224 | // Tell the new shape that we're entering. 225 | this.currentHoverControllers = this.controllers[id]; 226 | this.currentHoverControllers.map(c => c.onMouseEnter()); 227 | } else { 228 | console.log("Leaving shape to enter anchor."); 229 | } 230 | } 231 | } 232 | } 233 | 234 | onMouseLeave(evt) { 235 | evt.preventDefault(); 236 | this.leavingId = evt.currentTarget.getAttribute("id"); 237 | this.hoverShapeId = null; 238 | } 239 | 240 | // Update the actual model being entered. It must be an actual shape model. 241 | updateActualModelBeingEntered(id) { 242 | var shapeController = this.controllers[id].find(ctrl => ctrl.model.isShape); 243 | 244 | if (shapeController !== undefined) { 245 | this.actualModel = shapeController.model; 246 | } 247 | } 248 | 249 | // Returns the controllers associated with the SVG element. 250 | getControllers(evt) { 251 | var id = evt.currentTarget.getAttribute("id"); 252 | var controllers = this.controllers[id]; 253 | 254 | return controllers; 255 | } 256 | 257 | getControllersById(id) { 258 | var controllers = this.controllers[id]; 259 | 260 | return controllers; 261 | } 262 | 263 | getControllersByElement(el) { 264 | var id = el.getAttribute("id"); 265 | 266 | return this.getControllersById(id); 267 | } 268 | 269 | clearSelectedObject() { 270 | this.mouseDown = false; 271 | this.activeControllers = null; 272 | } 273 | 274 | // Move the shape out of the toolbox group and into the objects group. 275 | // This requires dealing with surface translation. 276 | // Show the anchors, because the mouse is currently over the shape since it 277 | // is being drageed & dropped. 278 | finishDragAndDrop(elDropped, elCurrent) { 279 | // Remove from toolbox group, translate, add to objects group. 280 | Helpers.getElement(Constants.SVG_TOOLBOX_ID).removeChild(elDropped); 281 | var id = elDropped.getAttribute("id"); 282 | this.controllers[id].map(c => c.model.translate(-surfaceModel.tx + toolboxGroupController.model.tx, -surfaceModel.ty + toolboxGroupController.model.ty)); 283 | Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(elDropped); 284 | 285 | // Only show anchors if mouse is actually on the dropped shape. 286 | if (id == elCurrent.getAttribute("id")) { 287 | this.currentHoverControllers = this.controllers[id]; 288 | this.currentHoverControllers.map(c => c.onMouseEnter()); 289 | } 290 | 291 | this.draggingToolboxShape = false; 292 | } 293 | } 294 | 295 | -------------------------------------------------------------------------------- /controllers/objectsController.js: -------------------------------------------------------------------------------- 1 | class ObjectsController extends Controller { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get isSurfaceController() { 7 | return true; 8 | } 9 | 10 | get hasConnectionPoints() { 11 | return false; 12 | } 13 | 14 | // We do not want to attach mouse events to the view of the "objects" SVG element! 15 | wireUpEvents() { } 16 | } -------------------------------------------------------------------------------- /controllers/rectangleController.js: -------------------------------------------------------------------------------- 1 | class RectangleController extends ShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | getAnchors() { 7 | var corners = this.getCorners(); 8 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 9 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 10 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 11 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 12 | //var upperRight = new Point(corners[1].X, corners[0].Y); 13 | //var lowerLeft = new Point(corners[0].X, corners[1].Y); 14 | 15 | // maybe later: 16 | // var anchors = [corners[0], corners[1], middleTop, middleBottom, middleLeft, middleRight, upperRight, lowerLeft]; 17 | var anchors = [ 18 | { anchor: middleTop, onDrag: this.topMove.bind(this) }, 19 | { anchor: middleBottom, onDrag: this.bottomMove.bind(this) }, 20 | { anchor: middleLeft, onDrag: this.leftMove.bind(this) }, 21 | { anchor: middleRight, onDrag: this.rightMove.bind(this) } 22 | ]; 23 | 24 | return anchors; 25 | } 26 | 27 | getConnectionPoints() { 28 | var corners = this.getCorners(); 29 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 30 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 31 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 32 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 33 | //var upperRight = new Point(corners[1].X, corners[0].Y); 34 | //var lowerLeft = new Point(corners[0].X, corners[1].Y); 35 | 36 | // maybe later: 37 | // var anchors = [corners[0], corners[1], middleTop, middleBottom, middleLeft, middleRight, upperRight, lowerLeft]; 38 | var connectionPoints = [ 39 | { connectionPoint: middleTop }, 40 | { connectionPoint: middleBottom }, 41 | { connectionPoint: middleLeft }, 42 | { connectionPoint: middleRight } 43 | ]; 44 | 45 | return connectionPoints; 46 | } 47 | 48 | getULCorner() { 49 | var p = new Point(this.model.x, this.model.y); 50 | p = this.getAbsoluteLocation(p); 51 | 52 | return p; 53 | } 54 | 55 | getLRCorner() { 56 | var p = new Point(this.model.x + this.model.width, this.model.y + this.model.height); 57 | p = this.getAbsoluteLocation(p); 58 | 59 | return p; 60 | } 61 | 62 | topMove(anchors, anchor, dx, dy) { 63 | // Moving the top affects "y" and "height" 64 | var y = this.model.y + dy; 65 | var height = this.model.height - dy; 66 | this.model.y = y; 67 | this.model.height = height; 68 | this.moveAnchor(anchors[0], 0, dy); 69 | this.adjustAnchorY(anchors[2], dy / 2); 70 | this.adjustAnchorY(anchors[3], dy / 2); 71 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 0); 72 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 2); 73 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 3); 74 | } 75 | 76 | bottomMove(anchors, anchor, dx, dy) { 77 | // Moving the bottom affects only "height" 78 | var height = this.model.height + dy; 79 | this.model.height = height; 80 | this.moveAnchor(anchors[1], 0, dy); 81 | this.adjustAnchorY(anchors[2], dy / 2); 82 | this.adjustAnchorY(anchors[3], dy / 2); 83 | this.adjustConnectorsAttachedToConnectionPoint(0, dy, 1); 84 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 2); 85 | this.adjustConnectorsAttachedToConnectionPoint(0, dy / 2, 3); 86 | } 87 | 88 | leftMove(anchors, anchor, dx, dy) { 89 | // Moving the left affects "x" and "width" 90 | var x = this.model.x + dx; 91 | var width = this.model.width - dx; 92 | this.model.x = x; 93 | this.model.width = width; 94 | this.moveAnchor(anchors[2], dx, 0); 95 | this.adjustAnchorX(anchors[0], dx / 2); 96 | this.adjustAnchorX(anchors[1], dx / 2); 97 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 2); 98 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 0); 99 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 1); 100 | } 101 | 102 | rightMove(anchors, anchor, dx, dy) { 103 | // Moving the right affects only "width" 104 | var width = this.model.width + dx; 105 | this.model.width = width; 106 | this.moveAnchor(anchors[3], dx, 0); 107 | this.adjustAnchorX(anchors[0], dx / 2); 108 | this.adjustAnchorX(anchors[1], dx / 2); 109 | this.adjustConnectorsAttachedToConnectionPoint(dx, 0, 3); 110 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 0); 111 | this.adjustConnectorsAttachedToConnectionPoint(dx / 2, 0, 1); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /controllers/shapeController.js: -------------------------------------------------------------------------------- 1 | // The shape controller handles showing the anchors and other decorations. 2 | class ShapeController extends Controller { 3 | constructor(mouseController, shapeView, shapeModel) { 4 | super(mouseController, shapeView, shapeModel); 5 | } 6 | 7 | // Not all shapes have anchors. 8 | getAnchors() { 9 | return []; 10 | } 11 | 12 | // Not all shapes have connection points. 13 | getConnectionPoints() { 14 | return []; 15 | } 16 | 17 | getCorners() { 18 | return [this.getULCorner(), this.getLRCorner()]; 19 | } 20 | 21 | onDrag(dx, dy) { 22 | super.onDrag(dx, dy); 23 | } 24 | 25 | // Overrridden by the line controller. 26 | get canConnectToShapes() { 27 | return false; 28 | } 29 | 30 | connect(idx, p) { 31 | throw "Shape appears to be capable of connecting to other shapes but doesn't implement connect(idx, p)."; 32 | } 33 | 34 | onMouseEnter() { 35 | if (!this.mouseController.mouseDown && this.shouldShowAnchors) { 36 | anchorGroupController.showAnchors(this); 37 | } 38 | } 39 | 40 | // If we're showing the anchors, moving the mouse on top of an anchor will cause the current shape to leave, which 41 | // will erase the anchors! We handle this situation in the mouse controller. 42 | onMouseLeave() { 43 | if (this.shouldShowAnchors) { 44 | anchorGroupController.removeAnchors(); 45 | } 46 | } 47 | 48 | moveAnchor(anchor, dx, dy) { 49 | anchor.translate(dx, dy); 50 | } 51 | 52 | adjustAnchorX(anchor, dx) { 53 | anchor.translate(dx, 0); 54 | } 55 | 56 | adjustAnchorY(anchor, dy) { 57 | anchor.translate(0, dy); 58 | } 59 | } -------------------------------------------------------------------------------- /controllers/surfaceController.js: -------------------------------------------------------------------------------- 1 | class SurfaceController extends Controller { 2 | constructor(mouseController, surfaceView, surfaceModel) { 3 | super(mouseController, surfaceView, surfaceModel); 4 | } 5 | 6 | get shapeName() { return "surface"; } 7 | 8 | get isSurfaceController() { 9 | return true; 10 | } 11 | 12 | get hasConnectionPoints() { 13 | return false; 14 | } 15 | 16 | // overrides Controller.onDrag 17 | onDrag(dx, dy) { 18 | this.model.updateTranslation(dx, dy); 19 | var dx = this.model.tx % this.model.gridCellW; 20 | var dy = this.model.ty % this.model.gridCellH; 21 | this.model.setTranslate(dx, dy); 22 | } 23 | 24 | onMouseLeave() { 25 | this.mouseController.clearSelectedObject(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /controllers/textController.js: -------------------------------------------------------------------------------- 1 | class TextController extends ShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shouldShowAnchors() { 7 | return false; 8 | } 9 | 10 | getConnectionPoints() { 11 | var corners = this.getCorners(); 12 | var middleTop = new Point((corners[0].x + corners[1].x) / 2, corners[0].y); 13 | var middleBottom = new Point((corners[0].x + corners[1].x) / 2, corners[1].y); 14 | var middleLeft = new Point(corners[0].x, (corners[0].y + corners[1].y) / 2); 15 | var middleRight = new Point(corners[1].x, (corners[0].y + corners[1].y) / 2); 16 | 17 | var connectionPoints = [ 18 | { connectionPoint: middleTop }, 19 | { connectionPoint: middleBottom }, 20 | { connectionPoint: middleLeft }, 21 | { connectionPoint: middleRight } 22 | ]; 23 | 24 | return connectionPoints; 25 | } 26 | 27 | // Update the UI with the text associated with the shape. 28 | onMouseDown(evt) { 29 | super.onMouseDown(evt); 30 | var text = this.model.text; 31 | document.getElementById("text").value = text; 32 | } 33 | 34 | getULCorner() { 35 | var rect = this.view.svgElement.getBoundingClientRect(); 36 | var p = new Point(rect.left, rect.top); 37 | p = Helpers.translateToSvgCoordinate(p); 38 | 39 | return p; 40 | } 41 | 42 | getLRCorner() { 43 | var rect = this.view.svgElement.getBoundingClientRect(); 44 | var p = new Point(rect.right, rect.bottom); 45 | p = Helpers.translateToSvgCoordinate(p); 46 | 47 | return p; 48 | } 49 | } -------------------------------------------------------------------------------- /controllers/toolboxCircleController.js: -------------------------------------------------------------------------------- 1 | class ToolboxCircleController extends ToolboxShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shapeName() { return "circle"; } 7 | 8 | createElementAt(x, y) { 9 | var group = Helpers.createElement("g", {}, false); 10 | var el = Helpers.createElement('circle', { cx: x, cy: y, r:30, fill: "#FFFFFF", stroke: "black", "stroke-width": 1 }); 11 | group.appendChild(el); 12 | var model = new CircleModel(); 13 | model._cx = x; 14 | model._cy = y; 15 | model._r = 30; 16 | var view = new ShapeView(group, model); 17 | var controller = new CircleController(this.mouseController, view, model); 18 | 19 | return { el: group, model: model, view: view, controller: controller }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /controllers/toolboxDiamondController.js: -------------------------------------------------------------------------------- 1 | class ToolboxDiamondController extends ToolboxShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shapeName() { return "diamond"; } 7 | 8 | // For drag and drop 9 | createElementAt(x, y) { 10 | var points = [{ cmd: "M", x: x - 15, y: y - 30 }, { cmd: "L", x: x - 45, y: y }, { cmd: "L", x: x - 15, y: y + 30 }, { cmd: "L", x: x + 15, y: y }]; 11 | var path = points.reduce((acc, val) => acc = acc + val.cmd + " " + val.x + " " + val.y, ""); 12 | path = path + " Z"; 13 | var group = Helpers.createElement("g", {}, false); 14 | var el = Helpers.createElement('path', { d: path, stroke: "black", "stroke-width": 1, fill: "#FFFFFF" }); 15 | group.appendChild(el); 16 | 17 | var model = new DiamondModel(); 18 | model._d = path; 19 | var view = new ShapeView(group, model); 20 | var controller = new DiamondController(this.mouseController, view, model); 21 | 22 | return { el: group, model: model, view: view, controller: controller }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /controllers/toolboxGroupController.js: -------------------------------------------------------------------------------- 1 | class ToolboxGroupController extends Controller { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | wireUpEvents() { } 7 | } -------------------------------------------------------------------------------- /controllers/toolboxImageController.js: -------------------------------------------------------------------------------- 1 | class ToolboxImageController extends ToolboxShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shapeName() { return "image"; } 7 | 8 | createElementAt(x, y) { 9 | var group = Helpers.createElement("g", {}, false); 10 | var el = Helpers.createElement('image', { x: x - 30, y: y - 30, width: 60, height: 60, href: Constants.DEFAULT_IMAGE_HREF }); 11 | group.appendChild(el); 12 | var model = new ImageModel(); 13 | model._x = x - 30; 14 | model._y = y - 30; 15 | model._width = 60; 16 | model._height = 60; 17 | model._href = Constants.DEFAULT_IMAGE_HREF; 18 | var view = new ShapeView(group, model); 19 | var controller = new ImageController(this.mouseController, view, model); 20 | 21 | return { el: group, model: model, view: view, controller: controller }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /controllers/toolboxLineController.js: -------------------------------------------------------------------------------- 1 | class ToolboxLineController extends ToolboxShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shapeName() { return "line"; } 7 | 8 | // For drag and drop 9 | createElementAt(x, y) { 10 | var x1 = x - 30; 11 | var y1 = y - 30; 12 | var x2 = x + 30; 13 | var y2 = y + 30; 14 | var group = Helpers.createElement("g", {}, false); 15 | var el = Helpers.createElement('g', {}); 16 | el.appendChild(Helpers.createElement('line', { x1: x1, y1: y1, x2: x2, y2: y2, "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" })); 17 | el.appendChild(Helpers.createElement('line', { x1: x1, y1: y1, x2: x2, y2: y2, fill: "#FFFFFF", stroke: "black", "stroke-width": 1 })); 18 | group.appendChild(el); 19 | 20 | var model = new LineModel(); 21 | model._x1 = x1; 22 | model._y1 = y1; 23 | model._x2 = x2; 24 | model._y2 = y2; 25 | var view = new LineView(group, model); 26 | var controller = new LineController(this.mouseController, view, model); 27 | 28 | return { el: group, model: model, view: view, controller: controller }; 29 | } 30 | } 31 | 32 | class ToolboxLineWithStartController extends ToolboxShapeController { 33 | constructor(mouseController, view, model) { 34 | super(mouseController, view, model); 35 | } 36 | 37 | // For drag and drop 38 | createElementAt(x, y) { 39 | var x1 = x - 30; 40 | var y1 = y - 30; 41 | var x2 = x + 30; 42 | var y2 = y + 30; 43 | var group = Helpers.createElement("g", {}, false); 44 | var el = Helpers.createElement('g', {}); 45 | el.appendChild(Helpers.createElement('line', { x1: x1, y1: y1, x2: x2, y2: y2, "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" })); 46 | el.appendChild(Helpers.createElement('line', { x1: x1, y1: y1, x2: x2, y2: y2, fill: "#FFFFFF", stroke: "black", "stroke-width": 1, "marker-start": "url(#trianglestart)" })); 47 | group.appendChild(el); 48 | 49 | var model = new LineModelWithStart(); 50 | model._x1 = x1; 51 | model._y1 = y1; 52 | model._x2 = x2; 53 | model._y2 = y2; 54 | var view = new LineView(group, model); 55 | var controller = new LineController(this.mouseController, view, model); 56 | 57 | return { el: group, model: model, view: view, controller: controller }; 58 | } 59 | } 60 | 61 | class ToolboxLineWithStartEndController extends ToolboxShapeController { 62 | constructor(mouseController, view, model) { 63 | super(mouseController, view, model); 64 | } 65 | 66 | // For drag and drop 67 | createElementAt(x, y) { 68 | var x1 = x - 30; 69 | var y1 = y - 30; 70 | var x2 = x + 30; 71 | var y2 = y + 30; 72 | var group = Helpers.createElement("g", {}, false); 73 | var el = Helpers.createElement('g', {}); 74 | el.appendChild(Helpers.createElement('line', { x1: x1, y1: y1, x2: x2, y2: y2, "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" })); 75 | el.appendChild(Helpers.createElement('line', { x1: x1, y1: y1, x2: x2, y2: y2, fill: "#FFFFFF", stroke: "black", "stroke-width": 1, "marker-start":"url(#trianglestart)", "marker-end": "url(#triangleend)" })); 76 | group.appendChild(el); 77 | 78 | var model = new LineModelWithStartEnd(); 79 | model._x1 = x1; 80 | model._y1 = y1; 81 | model._x2 = x2; 82 | model._y2 = y2; 83 | var view = new LineView(group, model); 84 | var controller = new LineController(this.mouseController, view, model); 85 | 86 | return { el: group, model: model, view: view, controller: controller }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /controllers/toolboxRectangleController.js: -------------------------------------------------------------------------------- 1 | class ToolboxRectangleController extends ToolboxShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shapeName() { return "rectangle"; } 7 | 8 | createElementAt(x, y) { 9 | var group = Helpers.createElement("g", {}, false); 10 | var el = Helpers.createElement('rect', { x: x - 30, y: y - 30, width: 60, height: 60, fill: "#FFFFFF", stroke: "black", "stroke-width": 1 }); 11 | group.appendChild(el); 12 | var model = new RectangleModel(); 13 | model._x = x - 30; 14 | model._y = y - 30; 15 | model._width = 60; 16 | model._height = 60; 17 | var view = new ShapeView(group, model); 18 | var controller = new RectangleController(this.mouseController, view, model); 19 | 20 | return { el: group, model: model, view: view, controller: controller }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /controllers/toolboxShapeController.js: -------------------------------------------------------------------------------- 1 | class ToolboxShapeController extends Controller { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get isToolboxShapeController() { 7 | return true; 8 | } 9 | 10 | // Check if click. If so, create element on the surface. 11 | onMouseUp(isClick) { 12 | if (isClick) { 13 | console.log("toolbox shape click"); 14 | var emvc = this.createElementAt(270, 130); 15 | diagramModel.addModel(emvc.model, emvc.view.id); 16 | // Account for surface translation (scrolling) so that shape is always placed in a fixed position. 17 | emvc.model.translate(-surfaceModel.tx, -surfaceModel.ty); 18 | this.addToObjectsGroup(emvc); 19 | this.attachToMouseController(emvc); 20 | } 21 | } 22 | 23 | // Dragging a toolbox shape has a custom implementation. 24 | onDrag(dx, dy) { 25 | // The user must move the mouse a wee bit. 26 | if (!this.mouseController.isClick) { 27 | console.log("toolbox shape onDrag"); 28 | // Account for the translation of the toolbox group and SVG location on the client screen. 29 | var p = new Point(this.mouseController.x - toolboxGroupController.model.tx, this.mouseController.y - toolboxGroupController.model.ty); 30 | p = Helpers.translateToScreenCoordinate(p); 31 | var emvc = this.createElementAt(p.x, p.y); 32 | diagramModel.addModel(emvc.model, emvc.view.id); 33 | // Add the shape to the toolbox group for now so it is topmost, rather than adding 34 | // it to the objects group. 35 | this.addToToolboxGroup(emvc); 36 | var controllers = this.attachToMouseController(emvc); 37 | // Hoist these controllers onto the mouse active controllers so it switches over to moving this shape. 38 | this.mouseController.activeControllers = controllers; 39 | // Indiicate to the mouse controller that we're dragging a toolbox shape so that when it is dropped 40 | // on the service, special things can happen - the shape is moved into the objects group and the 41 | // anchors are shown. 42 | this.mouseController.draggingToolboxShape = true; 43 | this.mouseController.shapeBeingDraggedAndDropped = emvc.el; 44 | } 45 | } 46 | 47 | // Simply so that this method can be overridden. 48 | addToObjectsGroup(emvc) { 49 | Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(emvc.el); 50 | } 51 | 52 | addToToolboxGroup(emvc) { 53 | Helpers.getElement(Constants.SVG_TOOLBOX_ID).appendChild(emvc.el); 54 | } 55 | 56 | attachToMouseController(emvc) { 57 | this.mouseController.attach(emvc.view, emvc.controller); 58 | // Most shapes also need an anchor controller. An exception is the Text shape, at least for now. 59 | this.mouseController.attach(emvc.view, anchorGroupController); 60 | 61 | return [emvc.controller, anchorGroupController]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /controllers/toolboxSurfaceController.js: -------------------------------------------------------------------------------- 1 | class ToolboxSurfaceController extends Controller { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get isSurfaceController() { 7 | return true; 8 | } 9 | 10 | get hasConnectionPoints() { 11 | return false; 12 | } 13 | 14 | onDrag(dx, dy) { 15 | toolboxGroupController.onDrag(dx, dy); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /controllers/toolboxTextController.js: -------------------------------------------------------------------------------- 1 | class ToolboxTextController extends ToolboxShapeController { 2 | constructor(mouseController, view, model) { 3 | super(mouseController, view, model); 4 | } 5 | 6 | get shapeName() { return "text"; } 7 | 8 | createElementAt(x, y) { 9 | var group = Helpers.createElement("g", {}, false); 10 | var el = Helpers.createElement('text', { x: x, y: y, "font-size": 12, "font-family": "Verdana" }); 11 | el.innerHTML = Constants.DEFAULT_TEXT; 12 | group.appendChild(el); 13 | var model = new TextModel(); 14 | model._x = x; 15 | model._y = y; 16 | model._text = Constants.DEFAULT_TEXT; 17 | var view = new TextView(group, model); 18 | var controller = new TextController(this.mouseController, view, model); 19 | 20 | return { el: group, model: model, view: view, controller: controller }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /event.js: -------------------------------------------------------------------------------- 1 | class Event { 2 | constructor() { 3 | this.listeners = []; 4 | this.keyedListeners = {}; 5 | } 6 | 7 | // Attaches a listener for the lifetime of the object containing the event. 8 | // These listeners cannot be detached. 9 | attach(listener) { 10 | this.listeners.push(listener); 11 | } 12 | 13 | // Attaches a listener with a specific key, which can be detached 14 | attachKeyed(key, listener) { 15 | this.keyedListeners[key] = listener; 16 | } 17 | 18 | // Detaches the listener with the specified key. 19 | detachKeyed(key) { 20 | delete this.keyedListeners[key]; 21 | } 22 | 23 | // args (if any) is expected to be a dictionary of key-value pairs. 24 | fire(sender, args) { 25 | this.listeners.forEach(listener => listener(sender, args)); 26 | Object.values(this.keyedListeners).forEach(listener => listener(sender, args)); 27 | } 28 | } -------------------------------------------------------------------------------- /fileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 1.3.5 4 | * 2018-01-22 15:49:54 5 | * 6 | * By Eli Grey, https://eligrey.com 7 | * License: MIT 8 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 9 | */ 10 | 11 | /*global self */ 12 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 13 | 14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */ 15 | 16 | // export default var saveAs = saveAs || (function(view) { 17 | var saveAs = saveAs || (function (view) { 18 | "use strict"; 19 | // IE <10 is explicitly unsupported 20 | if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { 21 | return; 22 | } 23 | var 24 | doc = view.document 25 | // only get URL when necessary in case Blob.js hasn't overridden it yet 26 | , get_URL = function() { 27 | return view.URL || view.webkitURL || view; 28 | } 29 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 30 | , can_use_save_link = "download" in save_link 31 | , click = function(node) { 32 | var event = new MouseEvent("click"); 33 | node.dispatchEvent(event); 34 | } 35 | , is_safari = /constructor/i.test(view.HTMLElement) || view.safari 36 | , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent) 37 | , throw_outside = function(ex) { 38 | (view.setImmediate || view.setTimeout)(function() { 39 | throw ex; 40 | }, 0); 41 | } 42 | , force_saveable_type = "application/octet-stream" 43 | // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to 44 | , arbitrary_revoke_timeout = 1000 * 40 // in ms 45 | , revoke = function(file) { 46 | var revoker = function() { 47 | if (typeof file === "string") { // file is an object URL 48 | get_URL().revokeObjectURL(file); 49 | } else { // file is a File 50 | file.remove(); 51 | } 52 | }; 53 | setTimeout(revoker, arbitrary_revoke_timeout); 54 | } 55 | , dispatch = function(filesaver, event_types, event) { 56 | event_types = [].concat(event_types); 57 | var i = event_types.length; 58 | while (i--) { 59 | var listener = filesaver["on" + event_types[i]]; 60 | if (typeof listener === "function") { 61 | try { 62 | listener.call(filesaver, event || filesaver); 63 | } catch (ex) { 64 | throw_outside(ex); 65 | } 66 | } 67 | } 68 | } 69 | , auto_bom = function(blob) { 70 | // prepend BOM for UTF-8 XML and text/* types (including HTML) 71 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 72 | if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 73 | return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); 74 | } 75 | return blob; 76 | } 77 | , FileSaver = function(blob, name, no_auto_bom) { 78 | if (!no_auto_bom) { 79 | blob = auto_bom(blob); 80 | } 81 | // First try a.download, then web filesystem, then object URLs 82 | var 83 | filesaver = this 84 | , type = blob.type 85 | , force = type === force_saveable_type 86 | , object_url 87 | , dispatch_all = function() { 88 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 89 | } 90 | // on any filesys errors revert to saving with object URLs 91 | , fs_error = function() { 92 | if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { 93 | // Safari doesn't allow downloading of blob urls 94 | var reader = new FileReader(); 95 | reader.onloadend = function() { 96 | var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); 97 | var popup = view.open(url, '_blank'); 98 | if(!popup) view.location.href = url; 99 | url=undefined; // release reference before dispatching 100 | filesaver.readyState = filesaver.DONE; 101 | dispatch_all(); 102 | }; 103 | reader.readAsDataURL(blob); 104 | filesaver.readyState = filesaver.INIT; 105 | return; 106 | } 107 | // don't create more object URLs than needed 108 | if (!object_url) { 109 | object_url = get_URL().createObjectURL(blob); 110 | } 111 | if (force) { 112 | view.location.href = object_url; 113 | } else { 114 | var opened = view.open(object_url, "_blank"); 115 | if (!opened) { 116 | // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html 117 | view.location.href = object_url; 118 | } 119 | } 120 | filesaver.readyState = filesaver.DONE; 121 | dispatch_all(); 122 | revoke(object_url); 123 | } 124 | ; 125 | filesaver.readyState = filesaver.INIT; 126 | 127 | if (can_use_save_link) { 128 | object_url = get_URL().createObjectURL(blob); 129 | setTimeout(function() { 130 | save_link.href = object_url; 131 | save_link.download = name; 132 | click(save_link); 133 | dispatch_all(); 134 | revoke(object_url); 135 | filesaver.readyState = filesaver.DONE; 136 | }); 137 | return; 138 | } 139 | 140 | fs_error(); 141 | } 142 | , FS_proto = FileSaver.prototype 143 | , saveAs = function(blob, name, no_auto_bom) { 144 | return new FileSaver(blob, name || blob.name || "download", no_auto_bom); 145 | } 146 | ; 147 | // IE 10+ (native saveAs) 148 | if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { 149 | return function(blob, name, no_auto_bom) { 150 | name = name || blob.name || "download"; 151 | 152 | if (!no_auto_bom) { 153 | blob = auto_bom(blob); 154 | } 155 | return navigator.msSaveOrOpenBlob(blob, name); 156 | }; 157 | } 158 | 159 | FS_proto.abort = function(){}; 160 | FS_proto.readyState = FS_proto.INIT = 0; 161 | FS_proto.WRITING = 1; 162 | FS_proto.DONE = 2; 163 | 164 | FS_proto.error = 165 | FS_proto.onwritestart = 166 | FS_proto.onprogress = 167 | FS_proto.onwrite = 168 | FS_proto.onabort = 169 | FS_proto.onerror = 170 | FS_proto.onwriteend = 171 | null; 172 | 173 | return saveAs; 174 | }( 175 | typeof self !== "undefined" && self 176 | || typeof window !== "undefined" && window 177 | || this 178 | )); 179 | -------------------------------------------------------------------------------- /flowSharpWeb.all.js.min: -------------------------------------------------------------------------------- 1 | Array.prototype.any=function(n){for(var i,t=0;t(n^crypto.getRandomValues(new Uint8Array(1))[0]&15>>n/4).toString(16))}static parseTransform(n){var i={},r,t;for(r in a=n.match(/(\w+\((\-?\d+\.?\d*e?\-?\d*,?)+\))+/g))t=a[r].match(/[\w\.\-]+/g),i[t.shift()]=t;return i}static getElement(n){var t=document.getElementById(Constants.SVG_ELEMENT_ID);return t.getElementById(n)}static getElements(n){var t=document.getElementById(Constants.SVG_ELEMENT_ID);return t.getElementsByClassName(n)}static createElement(n,t){var i=document.createElementNS(Constants.SVG_NS,n);return i.setAttributeNS(null,"id",Helpers.uuidv4()),i.setAttributeNS(null,"class",Constants.SHAPE_CLASS_NAME),Object.entries(t).map([n,t]=>i.setAttributeNS(null,n,t)),i}static translateToSvgCoordinate(n){var t=document.getElementById(Constants.SVG_ELEMENT_ID),r=t.createSVGPoint(),i=r.matrixTransform(t.getScreenCTM().inverse());return n.translate(i.x,i.y)}static translateToScreenCoordinate(n){var t=document.getElementById(Constants.SVG_ELEMENT_ID),r=t.createSVGPoint(),i=r.matrixTransform(t.getScreenCTM());return n.translate(-i.x,-i.y)}static getNearbyShapes(n){var f=document.getElementById("svg"),t=f.createSVGRect(),r,u,i;for(t.x=n.x-Constants.NEARBY_DELTA/2,t.y=n.y-Constants.NEARBY_DELTA/2,t.height=Constants.NEARBY_DELTA,t.width=Constants.NEARBY_DELTA,r=f.getIntersectionList(t,null),u=[],i=0;ithis.createElement("rect")},Circle:{model:CircleModel,view:ShapeView,controller:CircleController,creator:()=>this.createElement("circle")},Diamond:{model:DiamondModel,view:ShapeView,controller:DiamondController,creator:()=>this.createElement("path")},Line:{model:LineModel,view:LineView,controller:LineController,creator:()=>this.createLineElement()},Text:{model:TextModel,view:TextView,controller:TextController,creator:()=>this.createTextElement()}};this.connections=[]}clear(){this.models=[];this.connections=[]}addModel(n,t){this.models.push({model:n,id:t})}connect(n,t,i,r){this.connections.push({shapeId:n,lineId:t,shapeCPIdx:i,lineAnchorIdx:r})}disconnect(n,t){this.connections=this.connections.filter(i=>!(i.lineId==n&&i.lineAnchorIdx==t))}removeShape(n){this.connections=this.connections.filter(t=>!(t.shapeId==n||t.lineId==n));this.models=this.models.filter(t=>t.id!=n)}createElement(n){return Helpers.createElement(n,{fill:"#FFFFFF",stroke:"black","stroke-width":1})}createTextElement(){return Helpers.createElement("text",{"font-size":12,"font-family":"Verdana"})}createLineElement(){var n=Helpers.createElement("g",{});return n.appendChild(Helpers.createElement("line",{"stroke-width":20,stroke:"black","stroke-opacity":"0","fill-opacity":"0"})),n.appendChild(Helpers.createElement("line",{fill:"#FFFFFF",stroke:"black","stroke-width":1})),n}serialize(){var n=[],t=surfaceModel.serialize();return t[Object.keys(t)[0]].id=Constants.SVG_SURFACE_ID,n.push(t),this.models.map(t=>{var i=t.model.serialize();i[Object.keys(i)[0]].id=t.id;n.push(i)}),JSON.stringify({model:n,connections:this.connections})}deserialize(n){var t=JSON.parse(n),r=t.model,i;this.connections=t.connections;i=[];surfaceModel.setTranslation(0,0);objectsModel.setTranslation(0,0);r.map(n=>{var t=Object.keys(n)[0],u=n[t],n,f,r,e;t=="Surface"?(surfaceModel.deserialize(u),objectsModel.setTranslation(surfaceModel.tx,surfaceModel.ty)):(n=new this.mvc[t].model,i.push(n),f=this.mvc[t].creator(),r=new this.mvc[t].view(f,n),n.deserialize(u,f),r.id=u.id,e=new this.mvc[t].controller(mouseController,r,n),this.models.push({model:n,id:u.id}),Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(f),this.mouseController.attach(r,e),e.shouldShowAnchors&&this.mouseController.attach(r,anchorGroupController))})}}class ObjectsModel extends Model{constructor(){super()}}class ShapeModel extends Model{constructor(){super()}}class SurfaceModel extends Model{constructor(){super();this.gridCellW=80;this.gridCellH=80;this.cellW=8;this.cellH=8}serialize(){var n=super.serialize();return n.gridCellW=this.gridCellW,n.gridCellH=this.gridCellH,n.cellW=this.cellW,n.cellH=this.cellH,{Surface:n}}deserialize(n){this.gridCellW=n.gridCellW;this.gridCellH=n.gridCellH;this.cellW=n.cellW;this.cellH=n.cellH;this.resizeGrid(this.gridCellW,this.gridCellH,this.cellW,this.cellH);this._tx=n.tx;this._ty=n.ty;var t=this.tx%this.gridCellW,i=this.ty%this.gridCellH;this.setTranslate(t,i)}resizeGrid(n,t,i,r){var f,e;this.gridCellW=n;this.gridCellH=t;this.cellW=i;this.cellH=r;var o=document.getElementById("largeGridRect"),a=document.getElementById("largeGridPath"),s=document.getElementById("largeGrid"),v=document.getElementById("smallGridPath"),h=document.getElementById("smallGrid"),c=document.getElementById("svg"),u=document.getElementById("surface"),l=document.getElementById("grid");o.setAttribute("width",n);o.setAttribute("height",t);a.setAttribute("d","M "+n+" 0 H 0 V "+t);s.setAttribute("width",n);s.setAttribute("height",t);v.setAttribute("d","M "+i+" 0 H 0 V "+r);h.setAttribute("width",i);h.setAttribute("height",r);l.setAttribute("x",-n);l.setAttribute("y",-t);f=+c.getAttribute("width");e=+c.getAttribute("height");u.setAttribute("width",f+n*2);u.setAttribute("height",e+t*2);u.setAttribute("x",-n);u.setAttribute("y",-t);u.setAttribute("width",f+n*2);u.setAttribute("height",e+t*2)}}class RectangleModel extends ShapeModel{constructor(){super();this._x=0;this._y=0;this._width=0;this._height=0}serialize(){var n=super.serialize();return n.x=this._x,n.y=this._y,n.width=this._width,n.height=this._height,{Rectangle:n}}deserialize(n,t){super.deserialize(n,t);this.x=n.x;this.y=n.y;this.width=n.width;this.height=n.height}get x(){return this._x}get y(){return this._y}get width(){return this._width}get height(){return this._height}set x(n){this._x=n;this.propertyChanged("x",n)}set y(n){this._y=n;this.propertyChanged("y",n)}set width(n){this._width=n;this.propertyChanged("width",n)}set height(n){this._height=n;this.propertyChanged("height",n)}}class CircleModel extends ShapeModel{constructor(){super();this._cx=0;this._cy=0;this._r=0}serialize(){var n=super.serialize();return n.cx=this._cx,n.cy=this._cy,n.r=this._r,{Circle:n}}deserialize(n,t){super.deserialize(n,t);this.cx=n.cx;this.cy=n.cy;this.r=n.r}get cx(){return this._cx}get cy(){return this._cy}get r(){return this._r}set cx(n){this._cx=n;this.propertyChanged("cx",n)}set cy(n){this._cy=n;this.propertyChanged("cy",n)}set r(n){this._r=n;this.propertyChanged("r",n)}}class PathModel extends ShapeModel{constructor(){super();this._d=null}serialize(){var n=super.serialize();return n.d=this._d,n}deserialize(n,t){super.deserialize(n,t);this.d=n.d}get d(){return this._d}set d(n){this._d=n;this.propertyChanged("d",n)}}class DiamondModel extends PathModel{constructor(){super()}serialize(){var n=super.serialize();return{Diamond:n}}}class LineModel extends ShapeModel{constructor(){super();this._x1=0;this._y1=0;this._x2=0;this._y2=0}serialize(){var n=super.serialize();return n.x1=this._x1,n.y1=this._y1,n.x2=this._x2,n.y2=this._y2,{Line:n}}deserialize(n,t){super.deserialize(n,t);this.x1=n.x1;this.y1=n.y1;this.x2=n.x2;this.y2=n.y2}get x1(){return this._x1}get y1(){return this._y1}get x2(){return this._x2}get y2(){return this._y2}set x1(n){this._x1=n;this.propertyChanged("x1",n)}set y1(n){this._y1=n;this.propertyChanged("y1",n)}set x2(n){this._x2=n;this.propertyChanged("x2",n)}set y2(n){this._y2=n;this.propertyChanged("y2",n)}}class TextModel extends ShapeModel{constructor(){super();this._x=0;this._y=0;this._text=""}serialize(){var n=super.serialize();return n.x=this._x,n.y=this._y,n.text=this._text,{Text:n}}deserialize(n,t){super.deserialize(n,t);this.x=n.x;this.y=n.y;this.text=n.text}get x(){return this._x}get y(){return this._y}get text(){return this._text}set x(n){this._x=n;this.propertyChanged("x",n)}set y(n){this._y=n;this.propertyChanged("y",n)}set text(n){this._text=n;this.propertyChanged("text",n)}}class View{constructor(n,t){this.svgElement=n;t.eventPropertyChanged=this.onPropertyChange.bind(this)}get id(){return this.svgElement.getAttribute("id")}set id(n){this.svgElement.setAttribute("id",n)}onPropertyChange(n,t){this.svgElement.firstElementChild==null?this.svgElement.setAttribute(n,t):this.svgElement.firstElementChild.setAttribute(n,t)}}class AnchorView extends View{constructor(n,t){super(n,t)}onPropertyChange(n,t){this.svgElement.setAttribute(n,t)}}class ObjectsView extends View{constructor(n,t){super(n,t)}}class ShapeView extends View{constructor(n,t){super(n,t)}}class LineView extends ShapeView{constructor(n,t){super(n,t)}onPropertyChange(n,t){this.svgElement.children[0].setAttribute(n,t);this.svgElement.children[1].setAttribute(n,t)}}class TextView extends View{constructor(n,t){super(n,t)}onPropertyChange(n,t){if(n=="text")this.svgElement.innerHTML=t;else super.onPropertyChange(n,t)}}class SurfaceView extends View{constructor(n,t){super(n,t)}onPropertyChange(n,t){this.svgElement.setAttribute(n,t)}}class Controller{constructor(n,t,i){this.mouseController=n;this.view=t;this.model=i;this.events=[];this.wireUpEvents()}get isSurfaceController(){return!1}get isAnchorController(){return!1}get isToolboxShapeController(){return!1}get shouldShowAnchors(){return!0}get hasConnectionPoints(){return!0}registerEvent(n,t,i){this.events.push({element:n,eventName:t,callbackRef:i})}destroy(){this.unhookEvents()}registerEventListener(n,t,i,r){var u;(r==null||r===undefined)&&(r=this);n.addEventListener(t,u=i.bind(r));this.registerEvent(n,t,u)}unhookEvents(){for(var t,n=0;nn.shapeId==this.view.id);i.map(i=>{var r=this.mouseController.getControllersById(i.lineId)[0];r.translateEndpoint(i.lineAnchorIdx,n,t)})}adjustConnectorsAttachedToConnectionPoint(n,t,i){var r=diagramModel.connections.filter(n=>n.shapeId==this.view.id&&n.shapeCPIdx==i);r.map(i=>{var r=this.mouseController.getControllersById(i.lineId)[0];r.translateEndpoint(i.lineAnchorIdx,n,t)})}}class AnchorController extends Controller{constructor(n,t,i,r,u,f){super(n,t,i);this.fncDragAnchor=u;this.anchorIdx=f;this.shapeConnectionPoints=[];this.shapeController=r}get isAnchorController(){return!0}get hasConnectionPoints(){return!1}get shouldShowAnchors(){return!1}onDrag(n,t){this.fncDragAnchor(n,t);this.showAnyConnectionPoints()}onMouseUp(n){super.onMouseUp(n);this.connectIfCloseToShapeConnectionPoint();this.removeConnectionPoints();this.shapeConnectionPoints=[]}showAnyConnectionPoints(){var n,i,r,t;this.shapeController.canConnectToShapes&&(n=this.getNewNearbyShapes(this.mouseController.x,this.mouseController.y),this.createConnectionPoints(n.newShapes),i=n.newShapes.concat(n.existingShapes).map(n=>n.id),r=this.shapeConnectionPoints.filter(n=>i.indexOf(n.id)<0),this.removeExpiredShapeConnectionPoints(r),t=n.existingShapes.map(n=>n.id),this.shapeConnectionPoints=this.shapeConnectionPoints.filter(n=>t.indexOf(n.id)>=0),this.shapeConnectionPoints=this.shapeConnectionPoints.concat(n.newShapes),console.log("scp: "+this.shapeConnectionPoints.length+", new: "+n.newShapes.length+", existing: "+t.length))}getNewNearbyShapes(n,t){var r=[],u=[],i=new Point(n,t),f;return i=Helpers.translateToScreenCoordinate(i),f=Helpers.getNearbyShapes(i),f.map(n=>{var t=this.mouseController.getControllersByElement(n);t&&t.map(n=>{var t,i;n.hasConnectionPoints&&(t=n.view.id,this.shapeConnectionPoints.any(n=>n.id==t)?u.push({id:t}):(i=n.getConnectionPoints(),r.push({id:t,controller:n,connectionPoints:i})))})}),{newShapes:r,existingShapes:u}}createConnectionPoints(n){var t=Helpers.getElement(Constants.SVG_CONNECTION_POINTS_ID);n.map(n=>{n.connectionPoints.map(i=>{var r=i.connectionPoint,u=Helpers.createElement("g",{connectingToShapeId:n.id});u.appendChild(Helpers.createElement("line",{x1:r.x-5,y1:r.y-5,x2:r.x+5,y2:r.y+5,fill:"#FFFFFF",stroke:"#000080","stroke-width":1}));u.appendChild(Helpers.createElement("line",{x1:r.x+5,y1:r.y-5,x2:r.x-5,y2:r.y+5,fill:"#FFFFFF",stroke:"#000080","stroke-width":1}));t.appendChild(u)})})}removeConnectionPoints(){var n=Helpers.getElement(Constants.SVG_CONNECTION_POINTS_ID);Helpers.removeChildren(n)}removeExpiredShapeConnectionPoints(n){n.map(n=>{var t=document.querySelectorAll('[connectingtoshapeid="'+n.id+'"]');[...t].map(n=>{n.parentNode.removeChild(n)})})}connectIfCloseToShapeConnectionPoint(){var n=new Point(this.mouseController.x,this.mouseController.y),t,i;n=Helpers.translateToScreenCoordinate(n);t=[];this.shapeConnectionPoints.filter(i=>{for(var u,r=0;r{var f=n.anchor,t=new RectangleModel,u;t._x=f.x-5;t._y=f.y-5;t._width=10;t._height=10;u=this.createElement("rect",{x:t.x,y:t.y,width:t.width,height:t.height,fill:"#FFFFFF",stroke:"#808080","stroke-width":.5});r.push(u);i.push(t);e.appendChild(u)}),t=0;ti.setAttribute(n,t)),i}removeAnchors(){var n=Helpers.getElement(Constants.SVG_ANCHORS_ID);n.innerHTML="";this.anchors.views.map(n=>this.mouseController.destroy(n));this.anchors=[];this.showingAnchors=!1}}const LEFT_MOUSE_BUTTON=0,TOOLBOX_DRAG_MIN_MOVE=3;class MouseController{constructor(){this.mouseDown=!1;this.controllers={};this.activeControllers=null;this.currentHoverControllers=[];this.leavingId=-1;this.draggingToolboxShape=!1;this.selectedControllers=null;this.selectedShapeId=null;this.hoverShapeId=null;this.x=0;this.y=0;this.dx=0;this.dy=0}attach(n,t){var i=n.id;this.controllers[i]==undefined&&(this.controllers[i]=[]);this.controllers[i].push(t)}detach(n){var t=n.id;delete this.controllers[t]}detachAll(){this.controllers={}}destroy(n){var t=n.id;this.controllers[t].map(n=>n.destroy());delete this.controllers[t]}destroyShapeById(n){this.controllers[n].map(n=>n.destroy());delete this.controllers[n]}destroyAll(){Object.entries(this.controllers).map([,n]=>n.map(n=>n.destroy()));this.controllers={}}destroyAllButSurface(){Object.entries(this.controllers).map([n,t]=>{t.map(t=>{t.isSurfaceController||t.isToolboxShapeController||(t.destroy(),delete this.controllers[n])})})}get isClick(){var n=this.x,t=this.y;return Math.abs(this.startDownX-n)n.onDrag(1,0));t=!0;break;case Constants.KEY_UP:this.currentHoverControllers.map(n=>n.onDrag(0,-1));t=!0;break;case Constants.KEY_LEFT:this.currentHoverControllers.map(n=>n.onDrag(-1,0));t=!0;break;case Constants.KEY_DOWN:this.currentHoverControllers.map(n=>n.onDrag(0,1));t=!0;break;case Constants.KEY_DELETE:this.currentHoverControllers.map(n=>n.onMouseLeave());diagramModel.removeShape(this.hoverShapeId);this.destroyShapeById(this.hoverShapeId);i=Helpers.getElement(this.hoverShapeId);i.parentNode.removeChild(i);this.currentHoverControllers=[];this.hoverShapeId=null;t=!0}return r&&t}onMouseDown(n){if(n.button==LEFT_MOUSE_BUTTON){n.preventDefault();var t=n.currentTarget.getAttribute("id");this.selectedShapeId=t;this.activeControllers=this.controllers[t];this.selectedControllers=this.controllers[t];this.mouseDown=!0;this.startDownX=n.clientX;this.startDownY=n.clientY;this.x=n.clientX;this.y=n.clientY;this.activeControllers.map(n=>n.onMouseDown())}}onMouseMove(n){n.preventDefault();this.mouseDown&&this.activeControllers!=null&&(this.dx=n.clientX-this.x,this.dy=n.clientY-this.y,this.x=n.clientX,this.y=n.clientY,this.activeControllers.map(n=>n.onDrag(this.dx,this.dy)))}onMouseUp(n){if(n.preventDefault(),n.button==LEFT_MOUSE_BUTTON&&this.activeControllers!=null){this.selectedShapeId=null;this.x=n.clientX;this.y=n.clientY;var t=this.isClick;this.activeControllers.map(n=>n.onMouseUp(t));this.clearSelectedObject();this.draggingToolboxShape&&this.finishDragAndDrop(this.shapeBeingDraggedAndDropped,n.currentTarget)}}onMouseEnter(n){n.preventDefault();var t=n.currentTarget.getAttribute("id");this.hoverShapeId=t;this.mouseDown||this.leavingId!=-1&&(console.log("Leaving "+this.leavingId),this.controllers[t][0].isAnchorController?console.log("Leaving shape to enter anchor."):(this.currentHoverControllers.map(n=>n.onMouseLeave()),console.log("Entering "+t+" => "+this.controllers[t].map(n=>n.constructor.name).join(", ")),this.currentHoverControllers=this.controllers[t],this.currentHoverControllers.map(n=>n.onMouseEnter())))}onMouseLeave(n){n.preventDefault();this.leavingId=n.currentTarget.getAttribute("id");this.hoverShapeId=null}getControllers(n){var t=n.currentTarget.getAttribute("id");return this.controllers[t]}getControllersById(n){return this.controllers[n]}getControllersByElement(n){var t=n.getAttribute("id");return this.getControllersById(t)}clearSelectedObject(){this.mouseDown=!1;this.activeControllers=null}finishDragAndDrop(n,t){Helpers.getElement(Constants.SVG_TOOLBOX_ID).removeChild(n);var i=n.getAttribute("id");this.controllers[i].map(n=>n.model.translate(-surfaceModel.tx+toolboxGroupController.model.tx,-surfaceModel.ty+toolboxGroupController.model.ty));Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(n);i==t.getAttribute("id")&&(this.currentHoverControllers=this.controllers[i],this.currentHoverControllers.map(n=>n.onMouseEnter()));this.draggingToolboxShape=!1}}class ShapeController extends Controller{constructor(n,t,i){super(n,t,i)}getAnchors(){return[]}getConnectionPoints(){return[]}getCorners(){return[this.getULCorner(),this.getLRCorner()]}onDrag(n,t){super.onDrag(n,t)}get canConnectToShapes(){return!1}connect(){throw"Shape appears to be capable of connecting to other shapes but doesn't implement connect(idx, p).";}onMouseEnter(){!this.mouseController.mouseDown&&this.shouldShowAnchors&&anchorGroupController.showAnchors(this)}onMouseLeave(){this.shouldShowAnchors&&anchorGroupController.removeAnchors()}moveAnchor(n,t,i){n.translate(t,i)}adjustAnchorX(n,t){n.translate(t,0)}adjustAnchorY(n,t){n.translate(0,t)}}class ToolboxShapeController extends Controller{constructor(n,t,i){super(n,t,i)}get isToolboxShapeController(){return!0}onMouseUp(n){if(n){console.log("toolbox shape click");var t=this.createElementAt(270,130);diagramModel.addModel(t.model,t.view.id);t.model.translate(-surfaceModel.tx,-surfaceModel.ty);this.addToObjectsGroup(t);this.attachToMouseController(t)}}onDrag(){var n,t;this.mouseController.isClick||(console.log("toolbox shape onDrag"),n=this.createElementAt(this.mouseController.x-toolboxGroupController.model.tx,this.mouseController.y-toolboxGroupController.model.ty),diagramModel.addModel(n.model,n.view.id),this.addToToolboxGroup(n),t=this.attachToMouseController(n),this.mouseController.activeControllers=t,this.mouseController.draggingToolboxShape=!0,this.mouseController.shapeBeingDraggedAndDropped=n.el)}addToObjectsGroup(n){Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(n.el)}addToToolboxGroup(n){Helpers.getElement(Constants.SVG_TOOLBOX_ID).appendChild(n.el)}attachToMouseController(n){return this.mouseController.attach(n.view,n.controller),this.mouseController.attach(n.view,anchorGroupController),[n.controller,anchorGroupController]}}class SurfaceController extends Controller{constructor(n,t,i){super(n,t,i)}get isSurfaceController(){return!0}get hasConnectionPoints(){return!1}onDrag(n,t){this.model.updateTranslation(n,t);var n=this.model.tx%this.model.gridCellW,t=this.model.ty%this.model.gridCellH;this.model.setTranslate(n,t)}onMouseLeave(){this.mouseController.clearSelectedObject()}}class ObjectsController extends Controller{constructor(n,t,i){super(n,t,i)}get isSurfaceController(){return!0}get hasConnectionPoints(){return!1}wireUpEvents(){}}class ToolboxGroupController extends Controller{constructor(n,t,i){super(n,t,i)}wireUpEvents(){}}class ToolboxSurfaceController extends Controller{constructor(n,t,i){super(n,t,i)}get isSurfaceController(){return!0}get hasConnectionPoints(){return!1}onDrag(n,t){toolboxGroupController.onDrag(n,t)}}class RectangleController extends ShapeController{constructor(n,t,i){super(n,t,i)}getAnchors(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{anchor:t,onDrag:this.topMove.bind(this)},{anchor:i,onDrag:this.bottomMove.bind(this)},{anchor:r,onDrag:this.leftMove.bind(this)},{anchor:u,onDrag:this.rightMove.bind(this)}]}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}getULCorner(){var n=new Point(this.model.x,this.model.y);return this.getAbsoluteLocation(n)}getLRCorner(){var n=new Point(this.model.x+this.model.width,this.model.y+this.model.height);return this.getAbsoluteLocation(n)}topMove(n,t,i,r){var u=this.model.y+r,f=this.model.height-r;this.model.y=u;this.model.height=f;this.moveAnchor(n[0],0,r);this.adjustAnchorY(n[2],r/2);this.adjustAnchorY(n[3],r/2);this.adjustConnectorsAttachedToConnectionPoint(0,r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,2);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,3)}bottomMove(n,t,i,r){var u=this.model.height+r;this.model.height=u;this.moveAnchor(n[1],0,r);this.adjustAnchorY(n[2],r/2);this.adjustAnchorY(n[3],r/2);this.adjustConnectorsAttachedToConnectionPoint(0,r,1);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,2);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,3)}leftMove(n,t,i){var r=this.model.x+i,u=this.model.width-i;this.model.x=r;this.model.width=u;this.moveAnchor(n[2],i,0);this.adjustAnchorX(n[0],i/2);this.adjustAnchorX(n[1],i/2);this.adjustConnectorsAttachedToConnectionPoint(i,0,2);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,0);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,1)}rightMove(n,t,i){var r=this.model.width+i;this.model.width=r;this.moveAnchor(n[3],i,0);this.adjustAnchorX(n[0],i/2);this.adjustAnchorX(n[1],i/2);this.adjustConnectorsAttachedToConnectionPoint(i,0,3);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,0);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,1)}}class CircleController extends ShapeController{constructor(n,t,i){super(n,t,i)}getAnchors(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{anchor:t,onDrag:this.topMove.bind(this)},{anchor:i,onDrag:this.bottomMove.bind(this)},{anchor:r,onDrag:this.leftMove.bind(this)},{anchor:u,onDrag:this.rightMove.bind(this)}]}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}getULCorner(){var n=new Point(this.model.cx-this.model.r,this.model.cy-this.model.r);return this.getAbsoluteLocation(n)}getLRCorner(){var n=new Point(this.model.cx+this.model.r,this.model.cy+this.model.r);return this.getAbsoluteLocation(n)}topMove(n,t,i,r){this.changeRadius(-r);this.moveAnchor(n[0],0,r);this.moveAnchor(n[1],0,-r);this.moveAnchor(n[2],r,0);this.moveAnchor(n[3],-r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r,0);this.adjustConnectorsAttachedToConnectionPoint(0,-r,1);this.adjustConnectorsAttachedToConnectionPoint(r,0,2);this.adjustConnectorsAttachedToConnectionPoint(-r,0,3)}bottomMove(n,t,i,r){this.changeRadius(r);this.moveAnchor(n[0],0,-r);this.moveAnchor(n[1],0,r);this.moveAnchor(n[2],-r,0);this.moveAnchor(n[3],r,0);this.adjustConnectorsAttachedToConnectionPoint(0,-r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r,1);this.adjustConnectorsAttachedToConnectionPoint(-r,0,2);this.adjustConnectorsAttachedToConnectionPoint(r,0,3)}leftMove(n,t,i){this.changeRadius(-i);this.moveAnchor(n[0],0,i);this.moveAnchor(n[1],0,-i);this.moveAnchor(n[2],i,0);this.moveAnchor(n[3],-i,0);this.adjustConnectorsAttachedToConnectionPoint(0,i,0);this.adjustConnectorsAttachedToConnectionPoint(0,-i,1);this.adjustConnectorsAttachedToConnectionPoint(i,0,2);this.adjustConnectorsAttachedToConnectionPoint(-i,0,3)}rightMove(n,t,i){this.changeRadius(i);this.moveAnchor(n[0],0,-i);this.moveAnchor(n[1],0,i);this.moveAnchor(n[2],-i,0);this.moveAnchor(n[3],i,0);this.adjustConnectorsAttachedToConnectionPoint(0,-i,0);this.adjustConnectorsAttachedToConnectionPoint(0,i,1);this.adjustConnectorsAttachedToConnectionPoint(-i,0,2);this.adjustConnectorsAttachedToConnectionPoint(i,0,3)}changeRadius(n){this.model.r=this.model.r+n}}class DiamondController extends ShapeController{constructor(n,t,i){super(n,t,i)}getAnchors(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{anchor:t,onDrag:this.topMove.bind(this)},{anchor:i,onDrag:this.bottomMove.bind(this)},{anchor:r,onDrag:this.leftMove.bind(this)},{anchor:u,onDrag:this.rightMove.bind(this)}]}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}getULCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.left,n.top);return Helpers.translateToSvgCoordinate(t)}getLRCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.right,n.bottom);return Helpers.translateToSvgCoordinate(t)}topMove(n,t,i,r){var u=this.getULCorner(),f=this.getLRCorner();this.changeHeight(u,f,-r);this.moveAnchor(n[0],0,r);this.moveAnchor(n[1],0,-r);this.adjustConnectorsAttachedToConnectionPoint(0,r,0);this.adjustConnectorsAttachedToConnectionPoint(0,-r,1)}bottomMove(n,t,i,r){var u=this.getULCorner(),f=this.getLRCorner();this.changeHeight(u,f,r);this.moveAnchor(n[0],0,-r);this.moveAnchor(n[1],0,r);this.adjustConnectorsAttachedToConnectionPoint(0,-r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r,1)}leftMove(n,t,i){var r=this.getULCorner(),u=this.getLRCorner();this.changeWidth(r,u,-i);this.moveAnchor(n[2],i,0);this.moveAnchor(n[3],-i,0);this.adjustConnectorsAttachedToConnectionPoint(i,0,2);this.adjustConnectorsAttachedToConnectionPoint(-i,0,3)}rightMove(n,t,i){var r=this.getULCorner(),u=this.getLRCorner();this.changeWidth(r,u,i);this.moveAnchor(n[2],-i,0);this.moveAnchor(n[3],i,0);this.adjustConnectorsAttachedToConnectionPoint(-i,0,2);this.adjustConnectorsAttachedToConnectionPoint(i,0,3)}changeWidth(n,t,i){n.x-=i;t.x+=i;this.updatePath(n,t)}changeHeight(n,t,i){n.y-=i;t.y+=i;this.updatePath(n,t)}updatePath(n,t){var n=this.getRelativeLocation(n),t=this.getRelativeLocation(t),r=(n.x+t.x)/2,u=(n.y+t.y)/2,i="M "+r+" "+n.y;i=i+" L "+n.x+" "+u;i=i+" L "+r+" "+t.y;i=i+" L "+t.x+" "+u;i=i+" Z";this.model.d=i}}class LineController extends ShapeController{constructor(n,t,i){super(n,t,i)}get canConnectToShapes(){return!0}onDrag(n,t){super.onDrag(n,t);diagramModel.disconnect(this.view.id,0);diagramModel.disconnect(this.view.id,1)}connect(n,t){switch(n){case 0:this.model.x1=t.x;this.model.y1=t.y;break;case 1:this.model.x2=t.x;this.model.y2=t.y}}translateEndpoint(n,t,i){var r;switch(n){case 0:r=new Point(this.model.x1,this.model.y1);r=r.translate(t,i);this.model.x1=r.x;this.model.y1=r.y;break;case 1:r=new Point(this.model.x2,this.model.y2);r=r.translate(t,i);this.model.x2=r.x;this.model.y2=r.y}}getAnchors(){var n=this.getCorners();return[{anchor:n[0],onDrag:this.moveULCorner.bind(this)},{anchor:n[1],onDrag:this.moveLRCorner.bind(this)}]}getULCorner(){var n=new Point(this.model.x1,this.model.y1);return this.getAbsoluteLocation(n)}getLRCorner(){var n=new Point(this.model.x2,this.model.y2);return this.getAbsoluteLocation(n)}moveULCorner(n,t,i,r){this.model.x1=this.model.x1+i;this.model.y1=this.model.y1+r;this.moveAnchor(t,i,r);diagramModel.disconnect(this.view.id,0)}moveLRCorner(n,t,i,r){this.model.x2=this.model.x2+i;this.model.y2=this.model.y2+r;this.moveAnchor(t,i,r);diagramModel.disconnect(this.view.id,1)}}class TextController extends ShapeController{constructor(n,t,i){super(n,t,i)}get shouldShowAnchors(){return!1}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}onMouseDown(n){super.onMouseDown(n);var t=this.model.text;document.getElementById("text").value=t}getULCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.left,n.top);return Helpers.translateToSvgCoordinate(t)}getLRCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.right,n.bottom);return Helpers.translateToSvgCoordinate(t)}}class ToolboxRectangleController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var r=Helpers.createElement("g",{}),e=Helpers.createElement("rect",{x:n-30,y:t-30,width:60,height:60,fill:"#FFFFFF",stroke:"black","stroke-width":1}),i,u,f;return r.appendChild(e),i=new RectangleModel,i._x=n-30,i._y=t-30,i._width=60,i._height=60,u=new ShapeView(r,i),f=new RectangleController(this.mouseController,u,i),{el:r,model:i,view:u,controller:f}}}class ToolboxCircleController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var u=Helpers.createElement("circle",{cx:n,cy:t,r:30,fill:"#FFFFFF",stroke:"black","stroke-width":1}),i=new CircleModel,r,f;return i._cx=n,i._cy=t,i._r=30,r=new ShapeView(u,i),f=new CircleController(this.mouseController,r,i),{el:u,model:i,view:r,controller:f}}}class ToolboxDiamondController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var o=[{cmd:"M",x:n-15,y:t-30},{cmd:"L",x:n-45,y:t},{cmd:"L",x:n-15,y:t+30},{cmd:"L",x:n+15,y:t}],r=o.reduce((n,t)=>n=n+t.cmd+" "+t.x+" "+t.y,""),u,i,f,e;return r=r+" Z",u=Helpers.createElement("path",{d:r,stroke:"black","stroke-width":1,fill:"#FFFFFF"}),i=new DiamondModel,i._d=r,f=new ShapeView(u,i),e=new DiamondController(this.mouseController,f,i),{el:u,model:i,view:f,controller:e}}}class ToolboxLineController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var u=n-30,f=t-30,e=n+30,o=t+30,r=Helpers.createElement("g",{}),i,s,h;return r.appendChild(Helpers.createElement("line",{x1:u,y1:f,x2:e,y2:o,"stroke-width":20,stroke:"black","stroke-opacity":"0","fill-opacity":"0"})),r.appendChild(Helpers.createElement("line",{x1:u,y1:f,x2:e,y2:o,fill:"#FFFFFF",stroke:"black","stroke-width":1})),i=new LineModel,i._x1=u,i._y1=f,i._x2=e,i._y2=o,s=new LineView(r,i),h=new LineController(this.mouseController,s,i),{el:r,model:i,view:s,controller:h}}}class ToolboxTextController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var r=Helpers.createElement("text",{x:n,y:t,"font-size":12,"font-family":"Verdana"}),i,u,f;return r.innerHTML=Constants.DEFAULT_TEXT,i=new TextModel,i._x=n,i._y=t,i._text=Constants.DEFAULT_TEXT,u=new TextView(r,i),f=new TextController(this.mouseController,u,i),{el:r,model:i,view:u,controller:f}}} -------------------------------------------------------------------------------- /flowSharpWeb.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 42 | 43 | 44 | 45 | 46 | FlowSharpWeb 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 |   Text: 118 | 119 |
120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 144 | 148 | 149 | 150 | 151 | 153 | 155 | 156 | 157 | 158 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | A 181 | 182 | 183 | 184 |
185 |
186 |
187 |

 

188 |
189 |
190 |
191 | 192 | 193 | 408 | 455 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | class Helpers { 2 | // From SO: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript 3 | static uuidv4() { 4 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, 5 | c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)) 6 | } 7 | 8 | // https://stackoverflow.com/questions/17824145/parse-svg-transform-attribute-with-javascript 9 | static parseTransform(transform) { 10 | var transforms = {}; 11 | for (var i in a = transform.match(/(\w+\((\-?\d+\.?\d*e?\-?\d*,?)+\))+/g)) { 12 | var c = a[i].match(/[\w\.\-]+/g); 13 | transforms[c.shift()] = c; 14 | } 15 | 16 | return transforms; 17 | } 18 | 19 | static getElement(id) { 20 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID); 21 | var el = svg.getElementById(id); 22 | 23 | return el; 24 | } 25 | 26 | static getElements(className) { 27 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID); 28 | var els = svg.getElementsByClassName(className); 29 | 30 | return els; 31 | } 32 | 33 | // Create the specified element with the attributes provided in a key-value dictionary. 34 | static createElement(elementName, attributes, createClass = true) { 35 | var el = document.createElementNS(Constants.SVG_NS, elementName); 36 | 37 | // Create a unique ID for the element so we can acquire the correct shape controller 38 | // when the user drags the shape. 39 | el.setAttributeNS(null, "id", Helpers.uuidv4()); 40 | 41 | if (createClass) { 42 | // Create a class common to all shapes so that, on file load, we can get them all and re-attach them 43 | // to the mouse controller. 44 | el.setAttributeNS(null, "class", Constants.SHAPE_CLASS_NAME); 45 | } 46 | 47 | // Add the attributes to the element. 48 | Object.entries(attributes).map(([key, val]) => { 49 | if (key == "href") { 50 | el.setAttributeNS("http://www.w3.org/1999/xlink", key, val); 51 | 52 | } else { 53 | el.setAttributeNS(null, key, val); 54 | } 55 | }); 56 | 57 | //Object.entries(attributes).map(([key, val]) => { 58 | // console.log("ATTR: " + key); 59 | // el.setAttributeNS(null, key, val); 60 | //}); 61 | 62 | return el; 63 | } 64 | 65 | // https://stackoverflow.com/questions/22183727/how-do-you-convert-screen-coordinates-to-document-space-in-a-scaled-svg 66 | static translateToSvgCoordinate(p) { 67 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID); 68 | var pt = svg.createSVGPoint(); 69 | var offset = pt.matrixTransform(svg.getScreenCTM().inverse()); 70 | p = p.translate(offset.x, offset.y); 71 | 72 | return p; 73 | } 74 | 75 | static translateToScreenCoordinate(p) { 76 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID); 77 | var pt = svg.createSVGPoint(); 78 | var offset = pt.matrixTransform(svg.getScreenCTM()); 79 | p = p.translate(-offset.x, -offset.y); 80 | 81 | return p; 82 | } 83 | 84 | static getNearbyShapes(p) { 85 | // https://stackoverflow.com/questions/2174640/hit-testing-svg-shapes 86 | // var el = document.elementFromPoint(evt.clientX, evt.clientY); 87 | // console.log(el); 88 | 89 | var svg = document.getElementById("svg"); 90 | var hitRect = svg.createSVGRect(); 91 | hitRect.x = p.x - Constants.NEARBY_DELTA / 2; 92 | hitRect.y = p.y - Constants.NEARBY_DELTA / 2; 93 | hitRect.height = Constants.NEARBY_DELTA; 94 | hitRect.width = Constants.NEARBY_DELTA; 95 | var nodeList = svg.getIntersectionList(hitRect, null); 96 | 97 | var nearShapes = []; 98 | 99 | for (var i = 0; i < nodeList.length; i++) { 100 | // get only nodes that are shapes. 101 | if (nodeList[i].getAttribute("class") == Constants.SHAPE_CLASS_NAME) { 102 | nearShapes.push(nodeList[i]); 103 | } 104 | } 105 | 106 | return nearShapes; 107 | } 108 | 109 | // https://stackoverflow.com/questions/3955229/remove-all-child-elements-of-a-dom-node-in-javascript 110 | static removeChildren(node) { 111 | while (node.firstChild) { 112 | node.removeChild(node.firstChild); 113 | } 114 | } 115 | 116 | static isNear(p1, p2, delta) { 117 | return Math.abs(p1.x - p2.x) <= delta && Math.abs(p1.y - p2.y) <= delta; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /models/AnchorModel.js: -------------------------------------------------------------------------------- 1 | class AnchorModel extends RectangleModel { 2 | constructor() { 3 | super(null); 4 | } 5 | 6 | // Anchors are not user-selectable shapes. 7 | get isShape() { return false; } 8 | } -------------------------------------------------------------------------------- /models/circleModel.js: -------------------------------------------------------------------------------- 1 | class CircleModel extends ShapeModel { 2 | constructor() { 3 | super(Constants.SHAPE_CIRCLE); 4 | this._cx = 0; 5 | this._cy = 0; 6 | this._r = 0; 7 | } 8 | 9 | get isShape() { return true; } 10 | 11 | serialize() { 12 | var model = super.serialize(); 13 | model.cx = this._cx; 14 | model.cy = this._cy; 15 | model.r = this._r; 16 | 17 | return { Circle: model }; 18 | } 19 | 20 | deserialize(model, el) { 21 | super.deserialize(model, el); 22 | this.cx = model.cx; 23 | this.cy = model.cy; 24 | this.r = model.r; 25 | } 26 | 27 | getProperties() { 28 | return [ 29 | { propertyName: 'cx', label: 'CX', column: 0, row: 0, getter: () => this.cx + this.tx }, 30 | { propertyName: 'cy', label: 'CY', column: 1, row: 0, getter: () => this.cy + this.ty }, 31 | { propertyName: 'r', label: 'Radius', column: 0, row: 1, getter: () => this.r }, 32 | { propertyName: 'tx', alias: 'cx', getter: () => this.cx + this.tx }, 33 | { propertyName: 'ty', alias: 'cy', getter: () => this.cy + this.ty }, 34 | ]; 35 | } 36 | 37 | get cx() { return this._cx; } 38 | get cy() { return this._cy; } 39 | get r() { return this._r; } 40 | 41 | set cx(value) { 42 | this._cx = value; 43 | this.propertyChanged("cx", value); 44 | } 45 | 46 | set cy(value) { 47 | this._cy = value; 48 | this.propertyChanged("cy", value); 49 | } 50 | 51 | set r(value) { 52 | this._r = value; 53 | this.propertyChanged("r", value); 54 | } 55 | } -------------------------------------------------------------------------------- /models/diagramModel.js: -------------------------------------------------------------------------------- 1 | class DiagramModel { 2 | constructor(mouseController) { 3 | this.mouseController = mouseController; 4 | this.models = []; 5 | this.mvc = { 6 | Rectangle: { model: RectangleModel, view: ShapeView, controller: RectangleController, creator : () => this.createElement("rect") }, 7 | Circle: { model: CircleModel, view: ShapeView, controller: CircleController, creator: () => this.createElement("circle") }, 8 | Diamond: { model: DiamondModel, view: ShapeView, controller: DiamondController, creator: () => this.createElement("path") }, 9 | Line: { model: LineModel, view: LineView, controller: LineController, creator: () => this.createLineElement() }, 10 | LineWithStart: { model: LineModelWithStart, view: LineView, controller: LineController, creator: () => this.createLineWithStartElement() }, 11 | LineWithStartEnd: { model: LineModelWithStartEnd, view: LineView, controller: LineController, creator: () => this.createLineWithStartEndElement() }, 12 | Text: { model: TextModel, view: TextView, controller: TextController, creator: () => this.createTextElement() }, 13 | Image: { model: ImageModel, view: ShapeView, controller: ImageController, creator: () => this.createImageElement() }, 14 | }; 15 | 16 | // For the moment we'll use array indices into the shape's connection points. 17 | // This is problematic when the feature is added so that the user can add/remove connection points. 18 | // In that case, each shape should create its default connection points with associated id's. 19 | // As for the line, it should be OK to always use the endpoint index. 20 | // Connection structure: 21 | // shapeId, lineId, shapeConnectionPointIndex, lineEndpointIndex 22 | this.connections = []; 23 | } 24 | 25 | clear() { 26 | this.models = []; 27 | this.connections = []; 28 | } 29 | 30 | addModel(model, id) { 31 | this.models.push({ model: model, id: id }); 32 | } 33 | 34 | connect(shapeId, lineId, shapeCPIdx, lineAnchorIdx) { 35 | this.connections.push({ shapeId: shapeId, lineId: lineId, shapeCPIdx: shapeCPIdx, lineAnchorIdx: lineAnchorIdx }); 36 | } 37 | 38 | // Disconnect any connections associated with the line and anchor index. 39 | disconnect(lineId, lineAnchorIdx) { 40 | this.connections = this.connections.filter(c => !(c.lineId == lineId && c.lineAnchorIdx == lineAnchorIdx)); 41 | } 42 | 43 | // remove connections of this shape as both the "connected to" shape (shapeId) and the "connecting" shape (lineId). 44 | removeShape(shapeId) { 45 | this.connections = this.connections.filter(c => !(c.shapeId == shapeId || c.lineId == shapeId)); 46 | this.models = this.models.filter(m => m.id != shapeId); 47 | } 48 | 49 | createElement(elName) { 50 | var group = Helpers.createElement("g", {}); 51 | var el = Helpers.createElement(elName, { fill: "#FFFFFF", stroke: "black", "stroke-width": 1 }); 52 | group.appendChild(el); 53 | 54 | return group; 55 | } 56 | 57 | createTextElement() { 58 | var group = Helpers.createElement("g", {}); 59 | var el = Helpers.createElement('text', { "font-size": 12, "font-family": "Verdana" }); 60 | el.innerHTML = Constants.DEFAULT_TEXT; 61 | group.appendChild(el); 62 | 63 | return group; 64 | } 65 | 66 | createImageElement() { 67 | var group = Helpers.createElement("g", {}); 68 | var el = Helpers.createElement('image', {}); 69 | group.appendChild(el); 70 | 71 | return group; 72 | } 73 | 74 | createLineElement(elName) { 75 | var group = Helpers.createElement("g", {}); 76 | var el = Helpers.createElement('g', {}); 77 | el.appendChild(Helpers.createElement('line', {"stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" })); 78 | el.appendChild(Helpers.createElement('line', {fill: "#FFFFFF", stroke: "black", "stroke-width": 1 })); 79 | group.appendChild(el); 80 | 81 | return group; 82 | } 83 | 84 | createLineWithStartElement(elName) { 85 | var group = Helpers.createElement("g", {}); 86 | var el = Helpers.createElement('g', {}); 87 | el.appendChild(Helpers.createElement('line', { "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" })); 88 | el.appendChild(Helpers.createElement('line', { fill: "#FFFFFF", stroke: "black", "stroke-width": 1, "marker-start": "url(#trianglestart)" })); 89 | group.appendChild(el); 90 | 91 | return group; 92 | } 93 | 94 | createLineWithStartEndElement(elName) { 95 | var group = Helpers.createElement("g", {}); 96 | var el = Helpers.createElement('g', {}); 97 | el.appendChild(Helpers.createElement('line', { "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" })); 98 | el.appendChild(Helpers.createElement('line', { fill: "#FFFFFF", stroke: "black", "stroke-width": 1, "marker-start": "url(#trianglestart)", "marker-end": "url(#triangleend)" })); 99 | group.appendChild(el); 100 | 101 | return group; 102 | } 103 | 104 | // Returns JSON of serialized models. 105 | serialize() { 106 | var uberModel = []; 107 | var model = surfaceModel.serialize(); 108 | model[Object.keys(model)[0]].id = Constants.SVG_SURFACE_ID; 109 | uberModel.push(model); 110 | 111 | this.models.map(m => { 112 | var model = m.model.serialize(); 113 | model[Object.keys(model)[0]].id = m.id; 114 | uberModel.push(model); 115 | }); 116 | 117 | return JSON.stringify({ model: uberModel, connections: this.connections }); 118 | } 119 | 120 | // Creates an MVC for each model of the provided JSON. 121 | deserialize(jsonString) { 122 | var modelData = JSON.parse(jsonString); 123 | var models = modelData.model; 124 | this.connections = modelData.connections; 125 | var objectModels = []; 126 | surfaceModel.setTranslation(0, 0); 127 | objectsModel.setTranslation(0, 0); 128 | 129 | models.map(model => { 130 | var key = Object.keys(model)[0]; 131 | var val = model[key]; 132 | 133 | if (key == "Surface") { 134 | // Special handler for surface, we keep the existing MVC objects. 135 | // We set both the surface and objects translation, but the surface translation 136 | // is mod'd by the gridCellW/H. 137 | surfaceModel.deserialize(val); 138 | objectsModel.setTranslation(surfaceModel.tx, surfaceModel.ty); 139 | } else { 140 | var model = new this.mvc[key].model(); 141 | objectModels.push(model); 142 | var el = this.mvc[key].creator(); 143 | // Create the view first so it hooks into the model's property change event. 144 | var view = new this.mvc[key].view(el, model); 145 | model.deserialize(val, el); 146 | view.id = val.id; 147 | var controller = new this.mvc[key].controller(mouseController, view, model); 148 | 149 | // Update our diagram's model collection. 150 | this.models.push({ model: model, id: val.id }); 151 | 152 | Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(el); 153 | this.mouseController.attach(view, controller); 154 | 155 | // Most shapes also need an anchor controller. An exception is the Text shape, at least for now. 156 | if (controller.shouldShowAnchors) { 157 | this.mouseController.attach(view, anchorGroupController); 158 | } 159 | } 160 | }); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /models/diamondModel.js: -------------------------------------------------------------------------------- 1 | class DiamondModel extends PathModel { 2 | constructor() { 3 | super(Constants.SHAPE_DIAMOND); 4 | } 5 | 6 | get isShape() { return true; } 7 | 8 | serialize() { 9 | var model = super.serialize(); 10 | 11 | return { Diamond: model }; 12 | } 13 | } -------------------------------------------------------------------------------- /models/imageModel.js: -------------------------------------------------------------------------------- 1 | class ImageModel extends ShapeModel { 2 | constructor(shapeName = Constants.SHAPE_IMAGE) { 3 | super(shapeName); 4 | this._x = 0; 5 | this._y = 0; 6 | this._width = 0; 7 | this._height = 0; 8 | this._href = ""; 9 | } 10 | 11 | get isShape() { return true; } 12 | 13 | serialize() { 14 | var model = super.serialize(); 15 | model.x = this._x; 16 | model.y = this._y; 17 | model.width = this._width; 18 | model.height = this._height; 19 | model.href = this._href; 20 | 21 | return { Image: model }; 22 | } 23 | 24 | deserialize(model, el) { 25 | super.deserialize(model, el); 26 | this.x = model.x; 27 | this.y = model.y; 28 | this.width = model.width; 29 | this.height = model.height; 30 | this.href = model.href; 31 | } 32 | 33 | getProperties() { 34 | return [ 35 | { propertyName: 'x', label: 'X', column: 0, row: 0, getter: () => this.x + this.tx }, 36 | { propertyName: 'y', label: 'Y', column: 1, row: 0, getter: () => this.y + this.ty }, 37 | { propertyName: 'width', label: 'Width', column: 0, row: 1, getter: () => this.width }, 38 | { propertyName: 'height', label: 'Height', column: 1, row: 1, getter: () => this.height }, 39 | { propertyName: 'href', label: 'HREF', column: 0, row: 2, getter: () => this.href }, 40 | { propertyName: 'tx', alias: 'x', getter: () => this.x + this.tx }, 41 | { propertyName: 'ty', alias: 'y', getter: () => this.y + this.ty }, 42 | ]; 43 | } 44 | 45 | get x() { return this._x; } 46 | get y() { return this._y; } 47 | get width() { return this._width; } 48 | get height() { return this._height; } 49 | get href() { return this._href;} 50 | 51 | set x(value) { 52 | this._x = value; 53 | this.propertyChanged("x", value); 54 | } 55 | 56 | set y(value) { 57 | this._y = value; 58 | this.propertyChanged("y", value); 59 | } 60 | 61 | set width(value) { 62 | this._width = value; 63 | this.propertyChanged("width", value); 64 | } 65 | 66 | set height(value) { 67 | this._height = value; 68 | this.propertyChanged("height", value); 69 | } 70 | 71 | set href(value) { 72 | this._href = value; 73 | this.propertyChanged("href", value); 74 | } 75 | } -------------------------------------------------------------------------------- /models/lineModel.js: -------------------------------------------------------------------------------- 1 | class LineModel extends ShapeModel { 2 | constructor() { 3 | super(Constants.SHAPE_LINE); 4 | this._x1 = 0; 5 | this._y1 = 0; 6 | this._x2 = 0; 7 | this._y2 = 0; 8 | } 9 | 10 | get isShape() { return true; } 11 | 12 | serialize() { 13 | var model = super.serialize(); 14 | model.x1 = this._x1; 15 | model.y1 = this._y1; 16 | model.x2 = this._x2; 17 | model.y2 = this._y2; 18 | 19 | return { Line: model }; 20 | } 21 | 22 | deserialize(model, el) { 23 | super.deserialize(model, el); 24 | this.x1 = model.x1; 25 | this.y1 = model.y1; 26 | this.x2 = model.x2; 27 | this.y2 = model.y2; 28 | } 29 | 30 | getProperties() { 31 | return [ 32 | { propertyName: 'x1', label: 'X1', column: 0, row: 0, getter: () => this.x1 + this.tx }, 33 | { propertyName: 'y1', label: 'Y1', column: 1, row: 0, getter: () => this.y1 + this.ty }, 34 | { propertyName: 'x2', label: 'X2', column: 0, row: 1, getter: () => this.x2 + this.tx }, 35 | { propertyName: 'y2', label: 'Y2', column: 1, row: 1, getter: () => this.y2 + this.ty }, 36 | { propertyName: 'tx', alias: 'x1', getter: () => this.x1 + this.tx }, 37 | { propertyName: 'ty', alias: 'y1', getter: () => this.y1 + this.ty }, 38 | { propertyName: 'tx', alias: 'x2', getter: () => this.x2 + this.tx }, 39 | { propertyName: 'ty', alias: 'y2', getter: () => this.y2 + this.ty }, 40 | ]; 41 | } 42 | 43 | get x1() { return this._x1; } 44 | get y1() { return this._y1; } 45 | get x2() { return this._x2; } 46 | get y2() { return this._y2; } 47 | 48 | set x1(value) { 49 | this._x1 = value; 50 | this.propertyChanged("x1", value); 51 | } 52 | 53 | set y1(value) { 54 | this._y1 = value; 55 | this.propertyChanged("y1", value); 56 | } 57 | 58 | set x2(value) { 59 | this._x2 = value; 60 | this.propertyChanged("x2", value); 61 | } 62 | 63 | set y2(value) { 64 | this._y2 = value; 65 | this.propertyChanged("y2", value); 66 | } 67 | } 68 | 69 | // Overrides so we can specify the key for the model. 70 | 71 | class LineModelWithStart extends LineModel { 72 | serialize() { 73 | var model = this.baseSerialize(); 74 | model.x1 = this._x1; 75 | model.y1 = this._y1; 76 | model.x2 = this._x2; 77 | model.y2 = this._y2; 78 | 79 | return { LineWithStart: model }; 80 | } 81 | } 82 | 83 | class LineModelWithStartEnd extends LineModel { 84 | serialize() { 85 | var model = this.baseSerialize(); 86 | model.x1 = this._x1; 87 | model.y1 = this._y1; 88 | model.x2 = this._x2; 89 | model.y2 = this._y2; 90 | 91 | return { LineWithStartEnd: model }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /models/model.js: -------------------------------------------------------------------------------- 1 | // See static initializer at bottom of class definition! 2 | // Model.idCount = 0; 3 | 4 | class Model { 5 | constructor(shapeName) { 6 | this.eventPropertyChanged = new Event(); 7 | 8 | // Certain shapes (like anchors, surface, etc.) are temporary so we don't want to increment the model ID. 9 | if (shapeName != null) { 10 | this._shapeName = shapeName; 11 | this._shapeId = shapeName + '.' + Model.idCount; 12 | Model.idCount += 1; 13 | } 14 | 15 | this._tx = 0; 16 | this._ty = 0; 17 | } 18 | 19 | // By default, we assume the model is not actually a shape. Only circle, diamond, line, rectangle, text, and other "shapes" are shapes. 20 | get isShape() { return false;} 21 | 22 | get tx() { return this._tx; } 23 | get ty() { return this._ty; } 24 | get shapeName() { return this._shapeName; } 25 | get shapeId() { return this._shapeId; } 26 | 27 | propertyChanged(propertyName, value) { 28 | // console.log(propertyName + " = " + value); 29 | this.eventPropertyChanged.fire(this, {propertyName : propertyName, value : value}) 30 | } 31 | 32 | getProperties() { 33 | return []; 34 | } 35 | 36 | serialize() { 37 | return { tx: this._tx, ty: this._ty, shapeName: this._shapeName, shapeId: this._shapeId }; 38 | } 39 | 40 | // Used to skip the ShapeModel's serializer in derived Line classes with start/end arrows. 41 | // Sort of annoying to have to do this. 42 | baseSerialize() { 43 | return { tx: this._tx, ty: this._ty, shapeName: this._shapeName, shapeId: this._shapeId }; 44 | } 45 | 46 | deserialize(model, el) { 47 | this._tx = model.tx; 48 | this._ty = model.ty; 49 | this._shapeName = model.shapeName; 50 | this.setTranslate(this._tx, this._ty); 51 | } 52 | 53 | translate(x, y) { 54 | this._tx += x; 55 | this._ty += y; 56 | this.propertyChanged("tx", this._tx); 57 | this.propertyChanged("ty", this._ty); 58 | this.setTranslate(this._tx, this._ty); 59 | } 60 | 61 | // Update our internal translation and set the translation immediately. 62 | setTranslation(x, y) { 63 | this._tx = x; 64 | this._ty = y; 65 | this.propertyChanged("tx", this._tx); 66 | this.propertyChanged("ty", this._ty); 67 | this.setTranslate(x, y); 68 | } 69 | 70 | // Deferred translation -- this only updates _tx and _ty 71 | // Used when we want to internally maintain the true _tx and _ty 72 | // but set the translation to a modulus, as in when translating 73 | // the grid. 74 | updateTranslation(dx, dy) { 75 | this._tx += dx; 76 | this._ty += dy; 77 | this.propertyChanged("tx", this._tx); 78 | this.propertyChanged("ty", this._ty); 79 | } 80 | 81 | // Sets the "translate" portion of the "transform" property. 82 | // All models have a translation. Notice we do not use _tx, _ty here 83 | // nor do we set _tx, _ty to (x, y) because (x, y) might be mod'ed by 84 | // the grid (w, h). We want to use exactly the parameters passed in 85 | // without modifying our model. 86 | // See SurfaceController.onDrag and note how the translation is updated 87 | // but setTranslate is called with the mod'ed (x, y) coordinates. 88 | setTranslate(x, y) { 89 | this.translation = "translate(" + x + "," + y + ")"; 90 | this.transform = this.translation; 91 | } 92 | 93 | // TODO: Later to be extended to build the transform so that it includes rotation and other things we can do. 94 | set transform(value) { 95 | this._transform = value; 96 | this.propertyChanged("transform", value); 97 | } 98 | 99 | set tx(value) { 100 | this._tx = value; 101 | this.propertyChanged("tx", value); 102 | this.translation = "translate(" + this._tx + "," + this._ty + ")"; 103 | this.transform = this.translation; 104 | } 105 | 106 | set ty(value) { 107 | this._ty = value; 108 | this.propertyChanged("ty", value); 109 | this.translation = "translate(" + this._tx + "," + this._ty + ")"; 110 | this.transform = this.translation; 111 | } 112 | } 113 | 114 | Model.idCount = 0; 115 | -------------------------------------------------------------------------------- /models/objectsModel.js: -------------------------------------------------------------------------------- 1 | class ObjectsModel extends Model { 2 | constructor() { 3 | super(); 4 | } 5 | 6 | get actualElement() { 7 | return this.svgElement; 8 | } 9 | } -------------------------------------------------------------------------------- /models/pathModel.js: -------------------------------------------------------------------------------- 1 | class PathModel extends ShapeModel { 2 | constructor(shapeName) { 3 | super(shapeName); 4 | this._d = null; 5 | } 6 | 7 | serialize() { 8 | var model = super.serialize(); 9 | model.d = this._d; 10 | 11 | return model; 12 | } 13 | 14 | deserialize(model, el) { 15 | super.deserialize(model, el); 16 | this.d = model.d; 17 | } 18 | 19 | get d() { return this._d; } 20 | 21 | set d(value) { 22 | this._d = value; 23 | this.propertyChanged("d", value); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /models/rectangleModel.js: -------------------------------------------------------------------------------- 1 | class RectangleModel extends ShapeModel { 2 | constructor(shapeName = Constants.SHAPE_RECTANGLE) { 3 | super(shapeName); 4 | this._x = 0; 5 | this._y = 0; 6 | this._width = 0; 7 | this._height = 0; 8 | } 9 | 10 | get isShape() { return true; } 11 | 12 | serialize() { 13 | var model = super.serialize(); 14 | model.x = this._x; 15 | model.y = this._y; 16 | model.width = this._width; 17 | model.height = this._height; 18 | 19 | return { Rectangle: model }; 20 | } 21 | 22 | deserialize(model, el) { 23 | super.deserialize(model, el); 24 | this.x = model.x; 25 | this.y = model.y; 26 | this.width = model.width; 27 | this.height = model.height; 28 | } 29 | 30 | getProperties() { 31 | return [ 32 | { propertyName: 'x', label: 'X', column: 0, row: 0, getter: () => this.x + this.tx}, 33 | { propertyName: 'y', label: 'Y', column: 1, row: 0, getter: () => this.y + this.ty}, 34 | { propertyName: 'width', label: 'Width', column: 0, row: 1, getter: () => this.width }, 35 | { propertyName: 'height', label: 'Height', column: 1, row: 1, getter: () => this.height }, 36 | { propertyName: 'tx', alias: 'x', getter: () => this.x + this.tx }, 37 | { propertyName: 'ty', alias: 'y', getter: () => this.y + this.ty }, 38 | ]; 39 | } 40 | 41 | get x() { return this._x; } 42 | get y() { return this._y; } 43 | get width() { return this._width; } 44 | get height() { return this._height; } 45 | 46 | set x(value) { 47 | this._x = value; 48 | this.propertyChanged("x", value); 49 | } 50 | 51 | set y(value) { 52 | this._y = value; 53 | this.propertyChanged("y", value); 54 | } 55 | 56 | set width(value) { 57 | this._width = value; 58 | this.propertyChanged("width", value); 59 | } 60 | 61 | set height(value) { 62 | this._height = value; 63 | this.propertyChanged("height", value); 64 | } 65 | } -------------------------------------------------------------------------------- /models/shapeModel.js: -------------------------------------------------------------------------------- 1 | class ShapeModel extends Model { 2 | constructor(shapeName) { 3 | super(shapeName); 4 | } 5 | } -------------------------------------------------------------------------------- /models/surfaceModel.js: -------------------------------------------------------------------------------- 1 | class SurfaceModel extends Model { 2 | constructor() { 3 | super(Constants.SHAPE_SURFACE); 4 | this.gridCellW = 80; 5 | this.gridCellH = 80; 6 | this.cellW = 8; 7 | this.cellH = 8; 8 | } 9 | 10 | get actualElement() { 11 | return this.svgElement; 12 | } 13 | 14 | serialize() { 15 | var model = super.serialize(); 16 | model.gridCellW = this.gridCellW; 17 | model.gridCellH = this.gridCellH; 18 | model.cellW = this.cellW; 19 | model.cellH = this.cellH; 20 | 21 | return { Surface: model }; 22 | } 23 | 24 | deserialize(model, el) { 25 | // DO NOT CALL BASE METHOD. Surface translations are mod'd by the gridCellW/H 26 | this.gridCellW = model.gridCellW; 27 | this.gridCellH = model.gridCellH; 28 | this.cellW = model.cellW; 29 | this.cellH = model.cellH; 30 | this.resizeGrid(this.gridCellW, this.gridCellH, this.cellW, this.cellH); 31 | 32 | // 33 | this._tx = model.tx; 34 | this._ty = model.ty; 35 | 36 | var dx = this.tx % this.gridCellW; 37 | var dy = this.ty % this.gridCellH; 38 | 39 | this.setTranslate(dx, dy); 40 | } 41 | 42 | // Programmatically change the grid spacing for the larger grid cells and smaller grid cells. 43 | // None of this is relevant to the SurfaceView so we just set the attributes directly. 44 | resizeGrid(lw, lh, sw, sh) { 45 | this.gridCellW = lw; 46 | this.gridCellH = lh; 47 | this.cellW = sw; 48 | this.cellH = sh; 49 | var elLargeGridRect = document.getElementById("largeGridRect"); 50 | var elLargeGridPath = document.getElementById("largeGridPath"); 51 | var elLargeGrid = document.getElementById("largeGrid"); 52 | 53 | var elSmallGridPath = document.getElementById("smallGridPath"); 54 | var elSmallGrid = document.getElementById("smallGrid"); 55 | 56 | var elSvg = document.getElementById("svg"); 57 | var elSurface = document.getElementById("surface"); 58 | var elGrid = document.getElementById("grid"); 59 | 60 | elLargeGridRect.setAttribute("width", lw); 61 | elLargeGridRect.setAttribute("height", lh); 62 | 63 | elLargeGridPath.setAttribute("d", "M " + lw + " 0 H 0 V " + lh); 64 | elLargeGrid.setAttribute("width", lw); 65 | elLargeGrid.setAttribute("height", lh); 66 | 67 | elSmallGridPath.setAttribute("d", "M " + sw + " 0 H 0 V " + sh); 68 | elSmallGrid.setAttribute("width", sw); 69 | elSmallGrid.setAttribute("height", sh); 70 | 71 | elGrid.setAttribute("x", -lw); 72 | elGrid.setAttribute("y", -lh); 73 | 74 | var svgW = +elSvg.getAttribute("width"); 75 | var svgH = +elSvg.getAttribute("height"); 76 | 77 | elSurface.setAttribute("width", svgW + lw * 2); 78 | elSurface.setAttribute("height", svgH + lh * 2); 79 | 80 | elSurface.setAttribute("x", -lw); 81 | elSurface.setAttribute("y", -lh); 82 | 83 | elSurface.setAttribute("width", svgW + lw * 2); 84 | elSurface.setAttribute("height", svgH + lh * 2); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /models/textModel.js: -------------------------------------------------------------------------------- 1 | class TextModel extends ShapeModel { 2 | constructor() { 3 | super(Constants.SHAPE_TEXT); 4 | this._x = 0; 5 | this._y = 0; 6 | this._text = ""; 7 | } 8 | 9 | get isShape() { return true; } 10 | 11 | serialize() { 12 | var model = super.serialize(); 13 | model.x = this._x; 14 | model.y = this._y; 15 | model.text = this._text; 16 | 17 | return { Text: model }; 18 | } 19 | 20 | deserialize(model, el) { 21 | super.deserialize(model, el); 22 | this.x = model.x; 23 | this.y = model.y; 24 | this.text = model.text; 25 | } 26 | 27 | getProperties() { 28 | return [ 29 | { propertyName: 'x', label: 'X', column: 0, row: 0, getter: () => this.x + this.tx }, 30 | { propertyName: 'y', label: 'Y', column: 1, row: 0, getter: () => this.y + this.ty }, 31 | { propertyName: 'text', label: 'Text', column: 0, row: 1, getter: () => this.text }, 32 | { propertyName: 'tx', alias: 'x', getter: () => this.x + this.tx }, 33 | { propertyName: 'ty', alias: 'y', getter: () => this.y + this.ty }, 34 | ]; 35 | } 36 | 37 | get x() { return this._x; } 38 | get y() { return this._y; } 39 | get text() { return this._text; } 40 | 41 | set x(value) { 42 | this._x = value; 43 | this.propertyChanged("x", value); 44 | } 45 | 46 | set y(value) { 47 | this._y = value; 48 | this.propertyChanged("y", value); 49 | } 50 | 51 | set text(value) { 52 | this._text = value; 53 | this.propertyChanged("text", value); 54 | } 55 | } -------------------------------------------------------------------------------- /point.js: -------------------------------------------------------------------------------- 1 | class Point { 2 | constructor(x, y) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | 7 | translate(x, y) { 8 | var p = new Point(this.x + x, this.y + y); 9 | 10 | return p; 11 | } 12 | } -------------------------------------------------------------------------------- /prototypes.js: -------------------------------------------------------------------------------- 1 | // "Extension methods" 2 | 3 | Array.prototype.any = function (predicate) { 4 | for (var i = 0; i < this.length; i++) { 5 | if (predicate) { 6 | var any = predicate(this[i]); 7 | if (any) { 8 | return true; 9 | } 10 | } 11 | } 12 | 13 | return false; 14 | } 15 | 16 | /* 17 | 18 | enumProto.any = function (predicate) { 19 | var any = false; 20 | this.forEach(function (elem, index) { 21 | if (predicate) { 22 | any = predicate(elem, index); 23 | return !any; 24 | } 25 | any = true; 26 | return false; 27 | }); 28 | return any; 29 | }; 30 | 31 | */ -------------------------------------------------------------------------------- /svggrid8.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | FlowSharpWeb 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 |   Text: 70 | 71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 96 | 100 | 101 | 102 | 103 | 105 | 107 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | A 133 | 134 | 135 |
136 | 137 | 138 | 334 | 381 | -------------------------------------------------------------------------------- /views/anchorView.js: -------------------------------------------------------------------------------- 1 | class AnchorView extends View { 2 | constructor(svgElement, model) { 3 | super(svgElement, model); 4 | } 5 | 6 | // For anchors, we always move the group, not the child elements. 7 | onPropertyChange(sender, args) { 8 | this.svgElement.setAttribute(args.propertyName, args.value); 9 | } 10 | } -------------------------------------------------------------------------------- /views/lineView.js: -------------------------------------------------------------------------------- 1 | class LineView extends ShapeView { 2 | constructor(svgElement, model) { 3 | super(svgElement, model); 4 | } 5 | 6 | onPropertyChange(sender, args) { 7 | // A line consists of a transparent portion [0] with a larger stroke width than the visible line [1] 8 | // firstElementChild drills into the outer group. 9 | this.svgElement.firstElementChild.children[0].setAttribute(args.propertyName, args.value); 10 | this.svgElement.firstElementChild.children[1].setAttribute(args.propertyName, args.value); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /views/objectsView.js: -------------------------------------------------------------------------------- 1 | class ObjectsView extends View { 2 | constructor(svgObjects, shapesModel) { 3 | super(svgObjects, shapesModel); 4 | } 5 | 6 | // For objects, we always move the group, not the child elements. 7 | onPropertyChange(sender, args) { 8 | this.svgElement.setAttribute(args.propertyName, args.value); 9 | } 10 | } -------------------------------------------------------------------------------- /views/propertyGridView.js: -------------------------------------------------------------------------------- 1 | class PropertyGridView { 2 | constructor(mouseController) { 3 | this.mouseController = mouseController; 4 | this.currentModel = undefined; 5 | this.pnpcMap = {}; // Property-Name : Property-Control map 6 | this.aliases = []; // Array of key-value pairs, because some shapes, like lines, have multiple getters for a single property, like "tx" 7 | mouseController.eventShapeSelected.attach(this.onShapeSelected.bind(this)); 8 | } 9 | 10 | // Show the shape ID on the property grid. 11 | onShapeSelected(sender, args) { 12 | document.getElementById(Constants.SHAPE_ID).innerHTML = args.shapeId; 13 | this.unregisterExistingPropertyChangedEvent(args.model); 14 | this.registerPropertyChangedEvent(args.model); 15 | this.renderProperties(args.model); 16 | } 17 | 18 | propertyChanged(sender, args) { 19 | let model = sender; 20 | let propertyName = args.propertyName; 21 | let value = args.value; 22 | 23 | if (model.shapeName !== undefined && value != null) { 24 | // console.log(model.shapeName + " : " + propertyName + ' = ' + value); 25 | 26 | // TODO: x, y, width, height and translate all affect the x, y, w, h PG controls. 27 | // Also, we map to a function that deals with the setting of the value, particularly 28 | // to perform computations and custom UI control value settings, like colors, comboboxes with line ends, width num up/down, etc. 29 | 30 | let gridControlId = this.pnpcMap[propertyName]; 31 | 32 | // If we don't have a grid control, then we probably have a property that is an alias to an existing property. 33 | // An example is where the translation (tx,ty) is actually an alias for (cx,cy) - circle or (x,y) - rectangle or (x1/x2, y1/y2) - line. 34 | if (gridControlId === undefined) { 35 | // We allow for multiple aliasing, so for a line, updates tx adjusts x1 and x2 together. 36 | this.aliases.filter(a => a.pname == propertyName).forEach(a => { 37 | let alias = a.palias; 38 | gridControlId = this.pnpcMap[alias]; 39 | 40 | if (gridControlId !== undefined) { 41 | let getter = model.getProperties().filter(p => p.propertyName == a.pname && p.alias == alias)[0].getter; 42 | let computedValue = getter(); 43 | // console.log("pname = " + a.pname + ", alias = " + a.palias + ", ID = " + gridControlId + ", Value = " + computedValue); 44 | document.getElementById(gridControlId).value = computedValue; 45 | } 46 | }); 47 | } else { 48 | // We don't necessarily want to use the value of the property that got changed, particular with regards to (x, y) and (tx, ty). 49 | // Instead, we always want to use the value returned by the getter method. 50 | let computedValue = model.getProperties().filter(p => p.propertyName === propertyName)[0].getter(); 51 | document.getElementById(gridControlId).value = computedValue; 52 | } 53 | } 54 | } 55 | 56 | unregisterExistingPropertyChangedEvent(model) { 57 | if (this.currentModel !== undefined && this.currentModel != model) { 58 | this.currentModel.eventPropertyChanged.detachKeyed(Constants.PROPERTY_GRID_LISTENER_KEY, this.propertyChanged.bind(this)); 59 | } 60 | } 61 | 62 | registerPropertyChangedEvent(model) { 63 | this.currentModel = model; 64 | this.currentModel.eventPropertyChanged.attachKeyed(Constants.PROPERTY_GRID_LISTENER_KEY, this.propertyChanged.bind(this)); 65 | } 66 | 67 | renderProperties(model) { 68 | this.pnpcMap = {}; 69 | let twoColumnPropertyGridTemplate = 70 | '' + 71 | ' ' + 72 | ' ' + 73 | ' ' + 74 | ' ' + 75 | ' '; 76 | 77 | let rowNum = -1; 78 | let pg = document.getElementById(Constants.PROPERTY_GRID_ID); 79 | pg.innerHTML = ''; 80 | 81 | model.getProperties().filter(p => p.alias === undefined).forEach(p => { 82 | let col = p.column; 83 | let row = p.row; 84 | let rowHtml = ''; 85 | // let newRow = row > rowNum; 86 | 87 | // Allow for empty rows as visual spacing 88 | while (row > rowNum) { 89 | rowNum += 1; 90 | rowHtml = twoColumnPropertyGridTemplate; 91 | rowHtml = rowHtml.replace('row0', 'row' + rowNum); 92 | rowHtml = rowHtml.replace('rc00', 'rc' + rowNum + '-0'); 93 | rowHtml = rowHtml.replace('prop00', 'prop' + rowNum + '-0'); 94 | rowHtml = rowHtml.replace('rc01', 'rc' + rowNum + '-1'); 95 | rowHtml = rowHtml.replace('prop01', 'prop' + rowNum + '-1'); 96 | pg.innerHTML += rowHtml; 97 | } 98 | 99 | let labelid = 'rc' + rowNum + '-' + col; 100 | let propid = 'prop' + row + '-' + col; 101 | this.pnpcMap[p.propertyName] = propid; 102 | document.getElementById(labelid).innerHTML = p.label + ':'; 103 | }); 104 | 105 | // Initialize all property grid input box values. 106 | model.getProperties().filter(p => p.alias === undefined).forEach(p => { 107 | let gridControlId = this.pnpcMap[p.propertyName]; 108 | 109 | if (gridControlId !== undefined) { 110 | document.getElementById(gridControlId).value = p.getter(); 111 | } 112 | }); 113 | 114 | // Setup aliases 115 | this.aliases = []; 116 | model.getProperties().filter(p => p.alias !== undefined).forEach(p => this.aliases.push({ pname: p.propertyName, palias : p.alias })); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /views/shapeView.js: -------------------------------------------------------------------------------- 1 | class ShapeView extends View { 2 | constructor(svgElement, model) { 3 | super(svgElement, model); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /views/surfaceView.js: -------------------------------------------------------------------------------- 1 | class SurfaceView extends View { 2 | constructor(svgSurface, surfaceModel) { 3 | super(svgSurface, surfaceModel); 4 | } 5 | 6 | // For surface, we always move the group, not the child elements. 7 | onPropertyChange(sender, args) { 8 | this.svgElement.setAttribute(args.propertyName, args.value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /views/textView.js: -------------------------------------------------------------------------------- 1 | class TextView extends View{ 2 | constructor(svgElement, model) { 3 | super(svgElement, model); 4 | } 5 | 6 | // Custom handling for property "text" 7 | onPropertyChange(sender, args) { 8 | if (args.propertyName == "text") { 9 | this.actualElement.innerHTML = args.value; 10 | } else { 11 | super.onPropertyChange(sender, args); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /views/toolboxSurfaceView.js: -------------------------------------------------------------------------------- 1 | class ToolboxSurfaceView extends View { 2 | constructor(svgSurface, surfaceModel) { 3 | super(svgSurface, surfaceModel); 4 | } 5 | 6 | // For surface, we always move the group, not the child elements. 7 | onPropertyChange(sender, args) { 8 | this.svgElement.setAttribute(args.propertyName, args.value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /views/toolboxView.js: -------------------------------------------------------------------------------- 1 | class ToolboxView extends View { 2 | constructor(svgSurface, surfaceModel) { 3 | super(svgSurface, surfaceModel); 4 | } 5 | 6 | // For surface, we always move the group, not the child elements. 7 | onPropertyChange(property, value) { 8 | this.svgElement.setAttribute(property, value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /views/view.js: -------------------------------------------------------------------------------- 1 | class View { 2 | constructor(svgElement, model) { 3 | this.svgElement = svgElement; 4 | model.eventPropertyChanged.attach(this.onPropertyChange.bind(this)); 5 | } 6 | 7 | get id() { 8 | return this.svgElement.getAttribute("id"); 9 | } 10 | 11 | set id(val) { 12 | this.svgElement.setAttribute("id", val); 13 | } 14 | 15 | // Returns the ID of the first child, the "real" shape, of the group surrounding the shape. 16 | get actualId() { 17 | return this.actualElement.getAttribute("id"); 18 | } 19 | 20 | // Anchors don't have a wrapping group so there are no child elements. 21 | get actualElement() { 22 | return this.svgElement.firstElementChild == null ? this.svgElement : this.svgElement.firstElementChild; 23 | } 24 | 25 | onPropertyChange(sender, args) { 26 | // Every shape is grouped, so we want to update the property of the first child in the group. 27 | // This behavior is overridden by specific views -- surface and objects, for example. 28 | // firstElementChild ignores text and comment nodes. 29 | // this.svgElement.firstElementChild.setAttribute(property, value); 30 | 31 | this.actualElement.setAttribute(args.propertyName, args.value); 32 | } 33 | } 34 | --------------------------------------------------------------------------------