├── .gitignore ├── LICENSE ├── MMM-ModulePosition.3.4.css ├── MMM-ModulePosition.js ├── README.md ├── archive ├── MMM-ModulePosition.css ├── smoothpositioning.3.2.js ├── smoothpositioning.3.3.js └── smoothpositioning.3.4.js ├── config.js ├── configscripts.js ├── css └── example.custom.css.1738597574256 ├── images ├── after.png ├── before.gif ├── screenshot_edit.png ├── screenshot_edit2.png ├── screenshot_edit3.png ├── screenshot_read.png └── screenshot_save.png ├── logger.js ├── modPos.njsproj ├── modPos.sln ├── node_helper.js ├── package.json └── smoothpositioning.3.5.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TheBodger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MMM-ModulePosition.3.4.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor:auto; 3 | } 4 | 5 | body { 6 | margin: 50px; 7 | background-size: 50px 50px; 8 | /*background-image: radial-gradient(circle, white 1px, black 1px);*/ 9 | background-image: linear-gradient(to right, grey 1px, transparent 1px), linear-gradient(to bottom, grey 1px, transparent 1px); 10 | } 11 | 12 | .resizable { 13 | background: white; 14 | box-sizing: border-box; 15 | } 16 | 17 | .resizable .resizers { 18 | width: 100%; 19 | height: 100%; 20 | border: 3px solid #4286f4; 21 | box-sizing: border-box; 22 | } 23 | 24 | .resizable .resizer { 25 | width: 10px; 26 | height: 10px; 27 | border-radius: 50%; /*magic to turn square into circle*/ 28 | background: white; 29 | border: 3px solid #4286f4; 30 | position:absolute; 31 | } 32 | 33 | .resizable .resizer.top-left { 34 | left: -5px; 35 | top: -5px; 36 | cursor: nwse-resize; /*resizer cursor*/ 37 | } 38 | 39 | .resizable .resizer.top-right { 40 | right: -5px; 41 | top: -5px; 42 | cursor: nesw-resize; 43 | } 44 | 45 | .resizable .resizer.bottom-left { 46 | left: -5px; 47 | bottom: -5px; 48 | cursor: nesw-resize; 49 | } 50 | 51 | .resizable .resizer.bottom-right { 52 | right: -5px; 53 | bottom: -5px; 54 | cursor: nwse-resize; 55 | } 56 | 57 | .drag { 58 | background-color: rgba(255,255,255,.25); 59 | color: white; 60 | font-size: 12px; 61 | font-family: sans-serif; 62 | border-radius: 8px; 63 | padding: 20px; 64 | touch-action: none; 65 | /*width: 120px;*/ 66 | /* This makes things *much* easier */ 67 | box-sizing: border-box; 68 | border-color: rgba(255,255,255,.5); 69 | border-width: 1px; 70 | border-style: solid; 71 | } 72 | 73 | .drag:hover { 74 | border-color: white; 75 | border-width: 1px; 76 | border-style: solid; 77 | } 78 | .currentmodulemeta{ 79 | position:absolute; 80 | background-color:rgba(0,0,0,0); 81 | height:12pt; 82 | width:100%; 83 | top:1px; 84 | left:0px; 85 | z-index:1000; 86 | } 87 | /* 88 | div { 89 | border-color: white; 90 | border-width: 1px; 91 | border-style:solid; 92 | } 93 | */ 94 | 95 | .glass { 96 | /* background styles */ 97 | position: relative; 98 | display: inline-block; 99 | padding: 15px 25px; 100 | background-color: lightgray; /*for compatibility with older browsers*/ 101 | background-image: linear-gradient(lightgray,white); 102 | /* text styles */ 103 | text-decoration: none; 104 | color: #123; 105 | font-size: 12px; 106 | font-family: sans-serif; 107 | font-weight: 100; 108 | border-radius: 3px; 109 | box-shadow: 0px 1px 4px -2px #333; 110 | text-shadow: 0px -1px #333; 111 | } 112 | 113 | .glass:after { 114 | content: ''; 115 | position: absolute; 116 | top: 2px; 117 | left: 2px; 118 | width: calc(100% - 4px); 119 | height: 50%; 120 | background: linear-gradient(rgba(255,255,255,0.8), rgba(255,255,255,0.2)); 121 | } 122 | 123 | .glass:hover { 124 | background: linear-gradient(grey,white); 125 | } 126 | 127 | /* the meta grid*/ 128 | 129 | .metagridname { 130 | grid-area: name; 131 | } 132 | 133 | .metagridx { 134 | grid-area: x; 135 | } 136 | 137 | .metagridw { 138 | grid-area: w; 139 | } 140 | 141 | .metagridy { 142 | grid-area: y; 143 | } 144 | 145 | .metagridh { 146 | grid-area: h; 147 | } 148 | 149 | .metagrid { 150 | display: grid; 151 | grid-template-areas: 'name name' 'x w' 'y h'; 152 | grid-gap: 1px; 153 | background-color: rgba(255,255,255,0.25); 154 | padding: 2px; 155 | left: 110px; 156 | top: 0; 157 | width: 180px; 158 | position: absolute; 159 | } 160 | 161 | .metagrid > div { 162 | color: white; 163 | font-size: 14px; 164 | padding: 4px; 165 | background-color: rgba(255,255,255,0.35); 166 | } 167 | 168 | .switch { 169 | position: relative; 170 | display: inline-block; 171 | width: 40px; 172 | height: 20px; 173 | } 174 | /* hide the actual switch */ 175 | .switch input { 176 | opacity: 0; 177 | width: 0; 178 | height: 0; 179 | } 180 | 181 | .slider { 182 | position: absolute; 183 | cursor: pointer; 184 | top: 0; 185 | left: 0; 186 | right: 0; 187 | bottom: 0; 188 | background-color: #ccc; 189 | -webkit-transition: .4s; 190 | transition: .4s; 191 | } 192 | 193 | .slider:before { 194 | position: absolute; 195 | content: ""; 196 | height: 16px; 197 | width: 16px; 198 | left: 2px; 199 | bottom: 2px; 200 | background-color: white; 201 | -webkit-transition: .4s; 202 | transition: .4s; 203 | } 204 | 205 | input:checked + .slider { 206 | background-color: #2196F3; 207 | } 208 | 209 | input:focus + .slider { 210 | box-shadow: 0 0 1px #2196F3; 211 | } 212 | 213 | input:checked + .slider:before { 214 | -webkit-transform: translateX(18px); 215 | -ms-transform: translateX(18px); 216 | transform: translateX(18px); 217 | } 218 | 219 | /* Rounded sliders */ 220 | .slider.round { 221 | border-radius: 20px; 222 | } 223 | 224 | .slider.round:before { 225 | border-radius: 50%; 226 | } 227 | 228 | -------------------------------------------------------------------------------- /MMM-ModulePosition.js: -------------------------------------------------------------------------------- 1 | /* global Module, MMM-ModulePosition */ 2 | 3 | /* Magic Mirror 4 | * Module: MMM-ModulePosition 5 | * 6 | * By Neil Scott 7 | * MIT Licensed. 8 | */ 9 | 10 | var startTime = new Date(); //use for getting elapsed times during debugging 11 | var interactmodules = {}; 12 | var theCanvas; 13 | var currentelement; 14 | 15 | Module.register("MMM-ModulePosition", { 16 | 17 | // Default module config. 18 | 19 | defaults: { 20 | text: "... loading", 21 | easeAmount : 0.30, // percentage of delta to step 22 | FPS: 15 ,// frames per second 23 | minimum_size: 50, //minimum size in px that the resizer will go down to 24 | canvasid: "body", //the overall parent for all movement constraints, any named DOM element, or if null a canvas is created from the visible window 25 | grid: 10, //the size of a grid to snap modules to when dragging and resizing which is enabled within the running MM2 26 | showAlerts: true, //show alerts on screen (File save, number of modules repositioned etc) 27 | }, 28 | 29 | start: function () { 30 | 31 | Log.log(this.name + ' is started!'); 32 | 33 | var self = this; 34 | 35 | this.sendNotificationToNodeHelper("CONFIG", { moduleinstance: this.identifier, config: this.config }); 36 | 37 | this.sendNotificationToNodeHelper("STATUS", this.identifier); 38 | 39 | this.moduletracking = {}; 40 | 41 | }, 42 | 43 | showElapsed: function () { 44 | endTime = new Date(); 45 | var timeDiff = endTime - startTime; //in ms 46 | // strip the ms 47 | timeDiff /= 1000; 48 | 49 | // get seconds 50 | var seconds = Math.round(timeDiff); 51 | return (" " + seconds + " seconds"); 52 | }, 53 | 54 | getScripts: function () { 55 | return [ 56 | 'moment.js', 57 | 'smoothpositioning.3.5.js' 58 | ] 59 | }, 60 | 61 | getStyles: function () { 62 | return [ 63 | 'MMM-ModulePosition.3.4.css' 64 | ] 65 | }, 66 | 67 | notificationReceived: function (notification, payload, sender) { 68 | 69 | var self = this; 70 | 71 | if (sender) { 72 | Log.log(self.identifier + " " + this.name + " received a module notification: " + notification + " from sender: " + sender.name); 73 | } else { 74 | Log.log(self.identifier + " " + this.name + " received a system notification: " + notification); 75 | } 76 | 77 | if (notification == 'DOM_OBJECTS_CREATED') { 78 | 79 | // start amending the current DOM to add the drag and resize Class 80 | 81 | this.setupconfig(); 82 | 83 | } 84 | 85 | }, 86 | 87 | socketNotificationReceived: function (notification, payload) { 88 | var self = this; 89 | Log.log(self.identifier + " " + this.identifier + "hello, received a socket notification @ " + this.showElapsed() + " " + notification + " - Payload: " + payload); 90 | 91 | if (notification == 'ALERT') { 92 | self.showNotification(notification,payload); 93 | } 94 | 95 | }, 96 | 97 | showNotification: function (msg, content) 98 | { 99 | alert(msg + ":" + content); 100 | }, 101 | 102 | setupconfig: function () { 103 | 104 | //get all the modules 105 | //set up instances based on the identifier 106 | //make explicit where the entry is within the config array of modules so 107 | //if multpiple entries of the same type are found we can track this and where we write back into the 108 | //copy of the config 109 | 110 | //need a tracking list called moduletracking of modules based on the module identifier 111 | //use position to determine if we ignore it when setting up the classes on the divs 112 | //name is the MMM- or defaults type of module 113 | 114 | //{identifier:{index:index in modules array,duplicate:false,ignore:false,name:'',modpos:{modpos}}} 115 | 116 | var self = this; 117 | 118 | MM.getModules().forEach(function (module, index) { 119 | self.moduletracking[module.identifier] = {}; 120 | self.moduletracking[module.identifier]['index'] = index; 121 | self.moduletracking[module.identifier]['ignore'] = (module.name == self.name || module.data.position == null || module.data.position == 'fullscreen_above' || module.data.position == 'fullscreen_below'); 122 | self.moduletracking[module.identifier]['duplicate'] = self.isduplicatemodule(module.name); 123 | self.moduletracking[module.identifier]['name'] = module.name; // add after check for duplicate 124 | self.moduletracking[module.identifier]['modpos'] = { x: 0, y: 0, w: 0, h: 0 }; 125 | }); 126 | 127 | //share the global variables from the config 128 | //must be done before setting up the modules 129 | 130 | smoothpositioninginit(this.config); 131 | 132 | //now we search the completed dom module looking for all the divs we need to amend 133 | 134 | for (var module in self.moduletracking) { 135 | 136 | if (!self.moduletracking[module].ignore) { 137 | 138 | var modulediv = document.getElementById(module); 139 | 140 | makedraggable(modulediv); 141 | makeresizable(modulediv); 142 | 143 | //and we need to add a couple of events so we can track the mouse over the modules 144 | 145 | modulediv.onmouseover = function () { self.showover(event) }; 146 | modulediv.onmouseout = function () { self.showout(event) }; 147 | 148 | //if we are cropping add the class to one with cropping on 149 | 150 | } 151 | } 152 | 153 | }, 154 | 155 | showover: function (event) { 156 | 157 | setgrid(event.currentTarget.id, getmeta(event.currentTarget).current); 158 | }, 159 | 160 | showout: function () { 161 | 162 | setgrid('No Selected Module', { x: 0, y: 0, w: 0, h: 0 }); 163 | 164 | }, 165 | 166 | isduplicatemodule: function (modulename) { 167 | var isit = false; 168 | for (identifier in this.moduletracking) { 169 | isit = (this.moduletracking[identifier].name == modulename); 170 | if (isit) { 171 | this.moduletracking[identifier].duplicate = true; //set the one in moduletracking that is now a duplicate. 172 | return isit; 173 | } 174 | } 175 | 176 | return isit; 177 | 178 | }, 179 | 180 | getDom: function () { 181 | 182 | var self = this; 183 | 184 | this.wrapper = document.createElement("div"); 185 | this.wrapper.className = "currentmodulemeta"; 186 | this.wrapper.id = "currentmodulemeta"; 187 | this.wrapper.style.position = 'absolute' 188 | this.wrapper.style.left = '100px'; 189 | this.wrapper.style.top = '100px'; 190 | this.wrapper.style.width = '300px'; 191 | 192 | //add the save button 193 | 194 | this.savebutton = document.createElement("button"); 195 | this.savebutton.className = 'save-button glass'; 196 | this.savebutton.id = 'save-button'; 197 | this.savebutton.innerHTML = "Save Positions"; 198 | this.savebutton.style.position = 'absolute' 199 | this.savebutton.style.width = '148px'; 200 | 201 | //add the grid toggle 202 | 203 | this.gridtoggle = document.createElement("label"); 204 | this.gridtoggle.className = "switch"; 205 | this.gridtoggle.innerHTML = ''; 206 | this.gridtoggle.innerHTML += ' GRID'; 207 | //this.gridtoggle.innerHTML += 'GRID'; 208 | this.gridtoggle.style.left = '10px'; 209 | this.gridtoggle.style.top = '60px'; 210 | 211 | //add the current item meta display 212 | 213 | this.modulemeta = document.createElement('div'); 214 | this.modulemeta.className = "metagrid"; 215 | this.modulemeta.style.position = "absolute"; 216 | this.modulemeta.style.left = '152px'; 217 | 218 | this.modulemetaname = document.createElement('div'); 219 | this.modulemetaname.className = "metagridname"; 220 | this.modulemetaname.id = "metagridname"; 221 | this.modulemetaname.innerHTML = "Module Name"; 222 | 223 | this.modulemetax = document.createElement('div'); 224 | this.modulemetax.className = "metagridx"; 225 | this.modulemetax.id = "metagridx"; 226 | this.modulemetax.innerHTML = "X:"; 227 | 228 | this.modulemetay = document.createElement('div'); 229 | this.modulemetay.className = "metagridy"; 230 | this.modulemetay.id = "metagridy"; 231 | this.modulemetay.innerHTML = "Y:"; 232 | 233 | this.modulemetaw = document.createElement('div'); 234 | this.modulemetaw.className = "metagridw"; 235 | this.modulemetaw.id = "metagridw"; 236 | this.modulemetaw.innerHTML = "W:"; 237 | 238 | this.modulemetah = document.createElement('div'); 239 | this.modulemetah.className = "metagridh"; 240 | this.modulemetah.id = "metagridh"; 241 | this.modulemetah.innerHTML = "H:"; 242 | 243 | this.modulemeta.appendChild(this.modulemetaname); 244 | this.modulemeta.appendChild(this.modulemetax); 245 | this.modulemeta.appendChild(this.modulemetaw); 246 | this.modulemeta.appendChild(this.modulemetay); 247 | this.modulemeta.appendChild(this.modulemetah); 248 | 249 | this.wrapper.appendChild(this.savebutton); 250 | this.wrapper.appendChild(this.gridtoggle); 251 | this.wrapper.appendChild(this.modulemeta); 252 | 253 | if (this.savebutton.addEventListener) { 254 | this.savebutton.addEventListener('click', function () { 255 | self.saveFunction(); 256 | }); 257 | } else if (this.savebutton.attachEvent) { 258 | this.savebutton.attachEvent('onclick', function () { 259 | self.saveFunction(); 260 | }); 261 | } 262 | 263 | return this.wrapper; 264 | }, 265 | 266 | saveFunction: function() { 267 | 268 | //get all the modules current positions 269 | 270 | for (var module in this.moduletracking){ 271 | if (!this.moduletracking[module].ignore) { 272 | var element = document.getElementById(module); 273 | var modposcurrent = getmeta(element).current; 274 | var modposoriginal = getmeta(element).original; 275 | var modposcssoffset = getcss(element); 276 | 277 | //calculate the modpos that will work when we apply absolute positioning to the element in the custom.css 278 | //all calcs are at the top left and not middle 279 | //delta is the difference between the initial location and the final location 280 | //the offset is the difference between pre and post absolute positioning 281 | 282 | //var deltax = modposoriginal.x - modposcurrent.x ; 283 | //var deltay = modposoriginal.y - modposcurrent.y ; 284 | 285 | this.moduletracking[module].modpos.x = (modposcurrent.x - (modposcurrent.w / 2)) + modposcssoffset.offsetX; 286 | this.moduletracking[module].modpos.y = (modposcurrent.y - (modposcurrent.h / 2)) + modposcssoffset.offsetY; 287 | 288 | this.moduletracking[module].modpos.w = modposcurrent.w; 289 | this.moduletracking[module].modpos.h = modposcurrent.h; 290 | 291 | this.moduletracking[module]['state'] = getstate(element); 292 | }; 293 | } 294 | 295 | //send them to the nodehelper to write out 296 | 297 | Log.log(self.identifier + " " + "SENDING WRITE_THIS TO NODEHELPER"); 298 | 299 | this.sendNotificationToNodeHelper("WRITE_THIS", { moduleinstance: this.identifier, payload: this.moduletracking }); 300 | 301 | //we want to save a revised config with the new modpos values 302 | //as opposed to using CSS ?? 303 | //it is more flexible (if modpos exists, use them to setup the position of the class/module name) 304 | //though CSS might be a good way to do it initially 305 | //so 306 | //can we read the custom.css to merge the new details as a module css entry 307 | //or overwrite an existing one 308 | 309 | //here we will need to write the file out useing the nodehelper 310 | // custom.css.timestamp 311 | // config.js.timestamp 312 | 313 | }, 314 | 315 | sendNotificationToNodeHelper: function (notification, payload) { 316 | this.sendSocketNotification(notification, payload); 317 | }, 318 | 319 | 320 | }); 321 | 322 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # MMM-ModulePosition 3 | 4 | This magic mirror module will enable the user to dynamically select, drag and resize any module defined in their magic mirror configuration. When the desired layout is reached, the settings can be saved. These are saved as a custom CSS. 5 | 6 | ### Examples: 7 | 8 | Showing before and after changing the layout, with text honoring the new size and the grid turned on to enable snapping, and the final layout with the module removed from the config file. Final Screenshot shows an alert when the custom css is saved and the number of repositioned /resized modules included 9 | ![Example of MMM-ModulePosition resizing modules](images/screenshot_edit.png?raw=true "Example screenshot") 10 | ![Example of MMM-ModulePosition resizing modules](images/screenshot_edit2.png?raw=true "Example screenshot") 11 | ![Example of MMM-ModulePosition resizing modules](images/screenshot_edit3.png?raw=true "Example screenshot") 12 | ![Example of MMM-ModulePosition resizing modules](images/screenshot_save.png?raw=true "Alert message on Save") 13 | The Video below was speeded up 5 times. To ensure the best results, all the modules where given the position of top_bar. 14 | ![Video of MMM-ModulePosition resizing modules](images/before.gif) 15 | This shows the result after the custom.css was applied and moduleposition removed. 16 | ![Example of MMM-ModulePosition resizing modules](images/after.png?raw=true "Modules repositioned") 17 | 18 | ### Dependencies 19 | 20 | This module requires MMM-FeedUtilities to be installed. 21 | 22 | Before installing this module; 23 | use https://github.com/TheBodger/MMM-FeedUtilities to setup the MMM-Feed... dependencies and install all modules. 24 | 25 | ## Installation 26 | To install the module, use your terminal to: 27 | 1. Navigate to your MagicMirror's modules folder. If you are using the default installation directory, use the command:
`cd ~/MagicMirror/modules` 28 | 2. Clone the module:
`git clone https://github.com/TheBodger/MMM-ModulePosition ` 29 | 30 | ## Update 31 | to update this module, use your terminal to: 32 | 1. `cd ~/MagicMirror/modules/MMM-ModulePosition` 33 | 2. `git pull` 34 | 35 | ## Using the module 36 | 37 | ### MagicMirror² Configuration 38 | 39 | To use this module, add the following minimum configuration block to the END of the modules array in the `config/config.js` file: 40 | ```js 41 | { 42 | module: "MMM-ModulePosition", 43 | position: "fullscreen_below", 44 | }, 45 | ``` 46 | ### Not quite WYSIWYG 47 | 48 | If planning to reposition and / or resize most or all of the modules, moving them across the screen far from their original positions, the best way of ensuring the best results is to give them the same initial module position in the config.js. top_bar works best. 49 | 50 | When repositioning modules, their contents may left justify. This is only in the positioner and when the custom.css is applied and the postioning module removed from the config, the correct justification will be shown on you magic mirror. 51 | 52 | ### Known "Features" and Docker watch outs 53 | 54 | Docker implementations may not provide all the permissions required to write the custom CSS to the css folder. If the file isnt being written or is empty, check that the css folder in the MMM-ModulePosition folder has write permissions for the Docker instance. i.e. Synology NAS requires that the folder has read/write access applied for Everyone 55 | 56 | The MMM-WallPaper module (https://github.com/kolbyjack/MMM-Wallpaper) - awesome module! covers the grid. Grid snapping will still work but the grid will not be visible. The grid will be visible when the MMM-WallPaper module is removed from the config file. 57 | 58 | ### MMM-Carousel compatability 59 | 60 | if using MMM-carousel, add/update the ignoreModules line to include module position otherwise the module wont operate correctly 61 | ```js 62 | { 63 | module: 'MMM-Carousel', 64 | config: { 65 | ignoreModules:['MMM-ModulePosition'], 66 | //rest of config 67 | } 68 | }, 69 | ``` 70 | If you are using the carousel arrow keys that appear on the screen, these wil be disabled when running this module. there is a simple workaround. 71 | 72 | If the carousel transitioninterval is set to 0, temporarily change it to 10000 ~(10 seconds) which will ensure each slide is shown whilst this module is running. Any changes made on any of the slides during an edit session will be captured in the save file and can be used in custom.css as described below. 73 | 74 | This also works if you already have the slides changing automatically. 75 | 76 | ### Saving and using custom.css 77 | 78 | This module uses the names allocated by the MM process, which will change depending on their absolute order within the config files. Make sure that this module is the last in the configuration file to ensure all modules have the correct name when the new layout is saved. 79 | 80 | Drag and /or resize the modules displayed on the MM display. Some module contents will resize to fit the new module size, others will ignore the size set due to how that particular module is coded. 81 | 82 | Once the layout is saved, using the SAVE button, it can be found in the css sub folder of the MMM-ModulePosition folder (it should be here: modules/MMM-ModulePosition/css/) 83 | 84 | Each save is given the name of custom.css.timestamp, where timestamp is a numeric representation of the time when the file is saved and will always be unique. This is to allow multiple saves within one positioning session without overwriting each save. 85 | 86 | To use the saved custom css file, simply copy all the contents and paste into the bottom of the custom.css file found in the magic mirror css folder, normally found as a sub folder to the MagicMirror folder. Remove this module from the config file and restart MM2. 87 | 88 | If any new modules are added to the MM config, to maintain the validity of the new custom CSS, ensure they are added at the end of the modules list. If a module is removed, then the custom CSS may not behave as expected and a new custom CSS will need to be created. 89 | 90 | 91 | ### Configuration Options 92 | 93 | | Option | Details 94 | |------------------------ |-------------- 95 | | `text` | *Optional* -

**Possible values:** Any string.
**Default value:** "... loading" 96 | | `easeAmount` | *Optional* - the percentage of the total delta to move an object during each frame

**Possible values:** a numeric value where 1 = 100%
**Default value:** 0.3 97 | | `FPS` | *Optional* - frames per second of the animation of objects during resizing and dragging

**Possible values:** a whole numeric value between 5 and 60
**Default value:** 15 98 | | `minimum_size` | *Optional* - minimum size in pixels that the resizer will allow an objects width and height to be.

**Possible values:** a whole number of pixels
**Default value:** 50 99 | | `canvasid` | *Optional* - the name of the dom element within the MM display to use as a relative container for any movements. If not set then the window is used.

**Possible values:** any dom element defined within the current MM display
**Default value:** `"body"` 100 | | `grid` |*Optional* - the size of a grid in pixels to snap modules to when dragging and resizing

**Possible values:** a whole number of pixels.
**Default value:** 10 101 | | `showAlerts` |*Optional* - display javscript alerts on the screen that are created for events such as custom css file save

**Possible values:** true,false
**Default value:** true 102 | 103 | ### Additional Notes 104 | 105 | This is a WIP; changes are being made all the time to improve the compatibility across the modules. 106 | 107 | Leave settings as the default for best results, minimum size is probably the only setting that may need amending depending on the size of the MM2 display 108 | 109 | This has been tested with a number of different MM layouts and layout options. It may however not cater for all combinations and may have problems with modules that adjust the modules displayed in the MM display or that swap between sets of visible modules. Try it out to see if it works ok with your favorite layout. Raise an issue in Github if it doesnt work as expected. 110 | -------------------------------------------------------------------------------- /archive/MMM-ModulePosition.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor:auto; 3 | } 4 | .resizable { 5 | background: white; 6 | box-sizing: border-box; 7 | } 8 | 9 | .resizable .resizers { 10 | width: 100%; 11 | height: 100%; 12 | border: 3px solid #4286f4; 13 | box-sizing: border-box; 14 | } 15 | 16 | .resizable .resizer { 17 | width: 10px; 18 | height: 10px; 19 | border-radius: 50%; /*magic to turn square into circle*/ 20 | background: white; 21 | border: 3px solid #4286f4; 22 | position:absolute; 23 | } 24 | 25 | .resizable .resizer.top-left { 26 | left: -5px; 27 | top: -5px; 28 | cursor: nwse-resize; /*resizer cursor*/ 29 | } 30 | 31 | .resizable .resizer.top-right { 32 | right: -5px; 33 | top: -5px; 34 | cursor: nesw-resize; 35 | } 36 | 37 | .resizable .resizer.bottom-left { 38 | left: -5px; 39 | bottom: -5px; 40 | cursor: nesw-resize; 41 | } 42 | 43 | .resizable .resizer.bottom-right { 44 | right: -5px; 45 | bottom: -5px; 46 | cursor: nwse-resize; 47 | } 48 | 49 | .drag { 50 | background-color: rgba(255,255,255,.25); 51 | color: white; 52 | font-size: 12px; 53 | font-family: sans-serif; 54 | border-radius: 8px; 55 | padding: 20px; 56 | touch-action: none; 57 | /*width: 120px;*/ 58 | /* This makes things *much* easier */ 59 | box-sizing: border-box; 60 | border-color: rgba(255,255,255,.5); 61 | border-width: 4px; 62 | border-style: groove; 63 | } 64 | 65 | .drag:hover { 66 | border-color: white; 67 | border-width: 4px; 68 | border-style: groove; 69 | } 70 | .currentmodulemeta{ 71 | position:absolute; 72 | background-color:rgba(0,0,0,0); 73 | height:12pt; 74 | width:100%; 75 | top:1px; 76 | left:0px; 77 | z-index:1000; 78 | } 79 | /* 80 | div { 81 | border-color: white; 82 | border-width: 1px; 83 | border-style:solid; 84 | } 85 | */ 86 | 87 | .glass { 88 | /* background styles */ 89 | position: relative; 90 | display: inline-block; 91 | padding: 15px 25px; 92 | background-color: yellow; /*for compatibility with older browsers*/ 93 | background-image: linear-gradient(yellow,white); 94 | /* text styles */ 95 | text-decoration: none; 96 | color: #123; 97 | font-size: 12px; 98 | font-family: sans-serif; 99 | font-weight: 100; 100 | border-radius: 3px; 101 | box-shadow: 0px 1px 4px -2px #333; 102 | text-shadow: 0px -1px #333; 103 | } 104 | 105 | .glass:after { 106 | content: ''; 107 | position: absolute; 108 | top: 2px; 109 | left: 2px; 110 | width: calc(100% - 4px); 111 | height: 50%; 112 | background: linear-gradient(rgba(255,255,255,0.8), rgba(255,255,255,0.2)); 113 | } 114 | 115 | .glass:hover { 116 | background: linear-gradient(orange,white); 117 | } 118 | 119 | /* the meta grid*/ 120 | 121 | .metagridname { 122 | grid-area: name; 123 | } 124 | 125 | .metagridx { 126 | grid-area: x; 127 | } 128 | 129 | .metagridw { 130 | grid-area: w; 131 | } 132 | 133 | .metagridy { 134 | grid-area: y; 135 | } 136 | 137 | .metagridh { 138 | grid-area: h; 139 | } 140 | 141 | .metagrid { 142 | display: grid; 143 | grid-template-areas: 'name name' 'x w' 'y h'; 144 | grid-gap: 1px; 145 | background-color: rgba(255,255,255,0.25); 146 | padding: 2px; 147 | left: 110px; 148 | top: 0; 149 | width: 180px; 150 | position: absolute; 151 | } 152 | 153 | .metagrid > div { 154 | color: white; 155 | font-size: 14px; 156 | padding: 4px; 157 | background-color: rgba(255,255,255,0.35); 158 | } 159 | -------------------------------------------------------------------------------- /archive/smoothpositioning.3.2.js: -------------------------------------------------------------------------------- 1 | 2 | //create 3.3. 3 | //calculate the offset of the current element/module from its parent(or we need to find a parent positioned element) 4 | //use this offset to amend the final location so that we adjust back from being relative to body to being relative to its real parent 5 | 6 | alert("BANG"); 7 | 8 | // JavaScript source code 9 | 10 | //each element carries 3 sets of information: 11 | //1) where am i (x,y,w,h) (current/also obtainable from the element itself) 12 | //2) where am i going (x,y,w,h) (target) 13 | //3) how do i get there (xstep,ystep,wstep,hstep) the value towards target controlled by the easing % 14 | 15 | // 0 - all locations held in the element relate to its centre, so need to be adjusted for showing 16 | // 1 - initialise all elements with current,target and steps 17 | // 2 - add the resizing stuff to the element, and make it absolute 18 | // 3 - listen for mousedown on an element and reset the details for the element(or parent) 19 | // 4 - listen for mousedown on an elements resizer (how do we know ? ask the parent) ditto 20 | // 5 - start timer, tell the world we are dragging, do some javascript stuff and set window level event listeners for move and up 21 | // 6 - and may be add a mouse out for the canvas to capture when some one goes awol 22 | // 7 - on each mouse move, update the target, recalc the steps from the current using easing % 23 | // 8 - the timer simply draws where we are based on current + step and adjusts current 24 | // 9 - on mouseup we stop dragging and let the timer take the element to its target, one step at a time 25 | 26 | //default local variables 27 | 28 | var easeAmount = 0.30 // percentage of delta to step 29 | var FPS = 15 // frames per second 30 | var interval = 1000 / FPS //how long each frame lasts for 31 | var minimum_size = 50; 32 | 33 | //---------------- 34 | 35 | var currentelement; 36 | var timers = {}; 37 | var dragging = false; 38 | var resizing = false; 39 | 40 | function smoothpositioninginit(smoothpositioningconfig) { 41 | 42 | //add verification that the config has been set 43 | //TODO - support null canvasid = viewable window 44 | 45 | if (smoothpositioningconfig.canvasid.toLowerCase() == 'body') { 46 | theCanvas = document.body; 47 | } 48 | else { 49 | theCanvas = document.getElementById(smoothpositioningconfig.canvasid); 50 | } 51 | 52 | //set the local variables 53 | 54 | easeAmount = smoothpositioningconfig.easeAmount; 55 | FPS = smoothpositioningconfig.FPS; 56 | interval = 1000 / FPS; 57 | minimum_size = smoothpositioningconfig.minimum_size ; 58 | 59 | } 60 | 61 | function setmeta(element, current, target, step) { 62 | element.dataset.meta = JSON.stringify({ current: current, target: target, step: step }); 63 | } 64 | 65 | function setstate(element, amended, active) { 66 | element.dataset.state = JSON.stringify({amended:amended, active:active}); 67 | } 68 | 69 | function setmousemeta(element, mousemeta) { 70 | element.dataset.mousemeta = JSON.stringify({ mousemeta: mousemeta }); 71 | } 72 | 73 | function getmeta(element) { 74 | 75 | return JSON.parse(element.dataset.meta); 76 | //({ current: current, target: target, step: step }); 77 | 78 | } 79 | 80 | function getstate(element) { 81 | return JSON.parse(element.dataset.state); 82 | } 83 | 84 | function getmousemeta(element) { 85 | 86 | return JSON.parse(element.dataset.mousemeta); 87 | 88 | } 89 | 90 | function getcurrentmeta(element) { 91 | 92 | //adjust the x,y to the centre of the element 93 | //x,y this is its apparent absolute position, manually taking into account margins etc 94 | //we know this is relative to the body 95 | 96 | var temp = { x: 0, y: 0, w: 0, h: 0 }; 97 | 98 | var trueoffset = getmouseposition({ clientX: element.offsetLeft, clientY: element.offsetTop }); 99 | 100 | //get the style information 101 | var tempstyle = theCanvas.currentStyle || window.getComputedStyle(theCanvas); 102 | 103 | temp.x = -parseFloat(tempstyle.marginLeft.replace('px', '')) + (element.getBoundingClientRect().left + (element.getBoundingClientRect().width / 2)); 104 | temp.y = -parseFloat(tempstyle.marginTop.replace('px', '')) + (element.getBoundingClientRect().top + (element.getBoundingClientRect().height / 2)); 105 | 106 | temp.w = element.getBoundingClientRect().width; 107 | temp.h = element.getBoundingClientRect().height; 108 | 109 | return temp; 110 | 111 | } 112 | 113 | function makedraggable(element) { 114 | 115 | element.classList.add("drag"); 116 | element.addEventListener("mousedown", mouseDownListener, false); 117 | 118 | setmeta(element, getcurrentmeta(element), getcurrentmeta(element), { x: 0, y: 0, w: 0, h: 0 }); 119 | 120 | //add a couple of tracking eleements 121 | 122 | setstate(element, false, false ); 123 | 124 | //add an observer to catch a change to the position (made by the main.js as part of hiding/showing modules, animating transitions) 125 | //so we can overide and keep them visible at all times 126 | 127 | // Select the node that will be observed for mutations 128 | const targetNode = element; 129 | 130 | // Options for the observer (which mutations to observe) 131 | const config = { attributes: true, attributeFilter: ["style"], attributeOldValue: true,}; 132 | 133 | // Callback function to execute when mutations are observed 134 | // only actually fire once the target element is active 135 | const callback = function (mutationsList, observer) { 136 | // Use traditional 'for loops' for IE 11 137 | for (let mutation of mutationsList) { 138 | if (mutation.target.dataset != null) { 139 | var state = getstate(mutation.target); 140 | if (state.active) { 141 | var oldvalue = getstyleasjson(mutation.oldValue); 142 | if (oldvalue != null) { 143 | console.log('The ' + mutation.attributeName + ' attribute of element ' + mutation.target.id + ' was modified. Old value was ' + oldvalue.position); 144 | console.log('The new position is ' + mutation.target.style.position); 145 | if (mutation.target.style.postion != 'absolute') { mutation.target.style.position = 'absolute' }; 146 | } 147 | } 148 | } 149 | } 150 | }; 151 | 152 | function getstyleasjson(stylestring) { 153 | if (stylestring == null) { return null }; 154 | var temp = ''; 155 | var obj = stylestring.split(";"); 156 | obj.forEach(function (pair) { 157 | if (pair != "") { 158 | var jpair = pair.split(":"); 159 | temp = temp + '"' + jpair[0].trim() + '":"' + jpair[1].trim() + '",'; 160 | } 161 | }) 162 | temp = "{" + temp.substr(0, temp.length - 1) + "}" 163 | return JSON.parse(temp); 164 | } 165 | 166 | // Create an observer instance linked to the callback function 167 | const observer = new MutationObserver(callback); 168 | 169 | // Start observing the target node for configured mutations 170 | observer.observe(targetNode, config); 171 | 172 | // Later, you can stop observing 173 | //observer.disconnect(); 174 | 175 | } 176 | 177 | function makeresizable(element) { 178 | 179 | element.classList.add("resizable"); 180 | 181 | var divtl = document.createElement('div'); 182 | divtl.classList.add("resizer"); 183 | divtl.classList.add("top-left"); 184 | element.appendChild(divtl); 185 | 186 | var divtr = document.createElement('div'); 187 | divtr.classList.add("resizer"); 188 | divtr.classList.add("top-right"); 189 | element.appendChild(divtr); 190 | 191 | var divbl = document.createElement('div'); 192 | divbl.classList.add("resizer"); 193 | divbl.classList.add("bottom-left"); 194 | element.appendChild(divbl); 195 | 196 | var divbr = document.createElement('div'); 197 | divbr.classList.add("resizer"); 198 | divbr.classList.add("bottom-right"); 199 | element.appendChild(divbr); 200 | 201 | divtl.addEventListener("mousedown", mouseDownListener, false); 202 | divtr.addEventListener("mousedown", mouseDownListener, false); 203 | divbl.addEventListener("mousedown", mouseDownListener, false); 204 | divbr.addEventListener("mousedown", mouseDownListener, false); 205 | 206 | setmeta(element, getcurrentmeta(element), getcurrentmeta(element), { x: 0, y: 0, w: 0, h: 0 }); 207 | 208 | } 209 | 210 | //get the actual element not the resizer 211 | function getelement(element,getparent=false) { 212 | 213 | if (element.classList == null) { //must be over the body or elsewhere if this fires 214 | return currentelement; 215 | } 216 | 217 | if (element.classList.contains('drag')) { 218 | return element; 219 | } 220 | 221 | if (element.classList.contains('resizer')) { // we manage all movement based on the resizer circles, only redrawing is at parent level 222 | if (getparent) { 223 | return element.parentElement; 224 | } 225 | else { 226 | return element; 227 | } 228 | } 229 | 230 | //must be over the body or elsewhere 231 | 232 | return currentelement; 233 | 234 | } 235 | 236 | function getmouseposition(mouseevent) { 237 | 238 | //additional support for a canvas that hasn't yet been populated (like body) 239 | //it takes a default value of the window 240 | 241 | var defaultheight = window.innerHeight; 242 | var defaultwidth = window.innerWidth; 243 | 244 | //getting mouse position correctly 245 | var bRect = theCanvas.getBoundingClientRect(); 246 | mouseX = (mouseevent.clientX - bRect.left) * (((theCanvas.clientWidth == 0) ? defaultwidth : theCanvas.clientWidth) / ((bRect.width == 0) ? defaultwidth : bRect.width) ); 247 | mouseY = (mouseevent.clientY - bRect.top) * (((theCanvas.clientHeight == 0) ? defaultheight : theCanvas.clientHeight) / ((bRect.height == 0) ? defaultheight : bRect.height)); 248 | 249 | return { mouseX: mouseX, mouseY: mouseY }; 250 | 251 | } 252 | 253 | //mousedown supports both resize and draggable 254 | //theCanvas is whatever element is used to constrain the action 255 | 256 | function mouseDownListener(event) { 257 | 258 | //stop a resizer mousedown from bubbling up to the parent and vice versa 259 | 260 | event.stopPropagation(); 261 | 262 | var mouse = getmouseposition(event); 263 | 264 | var mouseX = mouse.mouseX; 265 | var mouseY = mouse.mouseY; 266 | 267 | //determine if we are dragging or resizing 268 | 269 | dragging = true; //we found something to drag //this should always be true as the mousedown events are only linked to draggable and re-sizable elements 270 | 271 | //but, if there is an outstanding timer, (about to be orphaned) , don't let the action start 272 | 273 | if (Object.keys(timers).length > 0) { 274 | dragging = false; 275 | } 276 | 277 | if (dragging) { 278 | 279 | event.currentTarget.removeEventListener("mousedown", mouseDownListener, false); 280 | 281 | //determine who we are dealing with 282 | 283 | var element = getelement(event.currentTarget); 284 | var parentelement = getelement(event.currentTarget, true); 285 | 286 | //pop the div to the top level so absolute actual works 287 | //and make it absolute here so we have correct initial positioning 288 | //before we do this we set the location so it doesn't jump around the screen 289 | //and we get the latest values for w/h/x/y because they have changed since last we were here for this element 290 | 291 | setmeta(parentelement,getcurrentmeta(parentelement), getcurrentmeta(parentelement), {x:0,y:0,w:0,h:0}) 292 | var currentmeta = getmeta(parentelement); 293 | 294 | //move the element 295 | parentelement.style.top = Math.round(currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 296 | parentelement.style.left = Math.round(currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 297 | 298 | parentelement.style.width = Math.round(currentmeta.current.w).toString() + 'px'; 299 | parentelement.style.height = Math.round(currentmeta.current.h).toString() + 'px'; 300 | 301 | parentelement.style.position = 'absolute'; 302 | 303 | setstate(parentelement, getstate(parentelement).amended, true); //set active to true 304 | 305 | document.body.append(parentelement); 306 | 307 | //tell the mutation observer for this element to start observing. 308 | 309 | //parentelement. 310 | 311 | //check if we are actually resizing 312 | 313 | if (element != parentelement) { 314 | resizing = true; 315 | } 316 | 317 | if (!resizing) { 318 | document.body.style.cursor = "move"; 319 | }; 320 | 321 | //store the current element 322 | currentelement = element; 323 | 324 | window.addEventListener("mousemove", mouseMoveListener, false); 325 | window.addEventListener("mouseup", mouseUpListener, false); 326 | 327 | //store the current mouse position 328 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: 0, deltaY: 0 }); 329 | 330 | //adjust the element target to be same as location (it should be anyway) 331 | currentmeta = getmeta((resizing) ? parentelement : element); 332 | setmeta((resizing) ? parentelement :element, currentmeta.current, currentmeta.current, currentmeta.step); 333 | 334 | timer = setInterval(onTimerTick, 1000 / interval); 335 | timers[timer]=timer; 336 | 337 | //code below prevents the mouse down from having an effect on the main browser window: 338 | if (event.preventDefault) { 339 | event.preventDefault(); 340 | } //standard 341 | else if (event.returnValue) { 342 | event.returnValue = false; 343 | } //older IE 344 | return false; 345 | } 346 | } 347 | 348 | function mouseMoveListener(event) { 349 | 350 | //work out the delta of mouse 351 | //new mouse becomes target 352 | //after clamping to the canvas 353 | 354 | //because we are moving we stick with the current element and dont try to determine who we are moving over 355 | //otherwise the mouseover finds another valid element 356 | 357 | var element = currentelement; //was getelement(event.target); 358 | 359 | //as the element has been moved, we assume it has been amended 360 | //resizing works differently to dragging so we have to split the parts of the code - really ! 361 | 362 | if (resizing) { 363 | var currentmeta = getmeta(element.parentElement); 364 | setstate(element, true, getstate(element.parentElement).active); //set amended true 365 | } 366 | else { 367 | var currentmeta = getmeta(element); 368 | setstate(element, true, getstate(element).active); //set amended true 369 | } 370 | 371 | var checkmeta = { target: currentmeta.target }; 372 | 373 | //get new mouse position 374 | var mouse = getmouseposition(event); 375 | var mouseX = mouse.mouseX; 376 | var mouseY = mouse.mouseY; 377 | 378 | //determine if new target is in bounds 379 | //test is based on the existing target being adjusted by the delta NOT the current location of the element as that is being ticked 380 | //we need to take into account that resizing adjusts x or y and h or w as deltaX an deltaY change by creating a temporary new target before testing it 381 | //delta(x,y) applied to oldtarget (x,y) must be between min(x,y) and max(x,y) 382 | //otherwise clamp the delta to a value to adhere to the above rule 383 | 384 | //mouse delta, try this first 385 | var deltaX = mouseX - getmousemeta(element).mousemeta.x; 386 | var deltaY = mouseY - getmousemeta(element).mousemeta.y; 387 | 388 | if (resizing) { 389 | 390 | //calculate the new element size and centre 391 | checkmeta.target = getresizedelement(element, deltaX, deltaY, true); 392 | 393 | } 394 | 395 | //calculate the bounds based on the new target size 396 | var minX = (checkmeta.target.w / 2); 397 | var maxX = (((theCanvas.clientWidth == 0) ? window.innerWidth : theCanvas.clientWidth) - (checkmeta.target.w / 2)); 398 | var minY = (checkmeta.target.h / 2); 399 | var maxY = (((theCanvas.clientHeight == 0) ? window.innerHeight : theCanvas.clientHeight) - (checkmeta.target.h / 2)); 400 | 401 | //check the centre fits within the bounds 402 | 403 | if (resizing) { 404 | //checkmax returns a -value if out of bounds 405 | //checkmin returns a +value if out of bounds 406 | var checkmaxX = (maxX - checkmeta.target.x); 407 | var checkmaxY = (maxY - checkmeta.target.y); 408 | var checkminX = (minX - checkmeta.target.x); 409 | var checkminY = (minY - checkmeta.target.y); 410 | } 411 | else { 412 | 413 | var checkmaxX = Math.round(maxX - (checkmeta.target.x + deltaX)); 414 | var checkmaxY = Math.round(maxY - (checkmeta.target.y + deltaY)); 415 | var checkminX = Math.round(minX - (checkmeta.target.x + deltaX)); 416 | var checkminY = Math.round(minY - (checkmeta.target.y + deltaY)); 417 | } 418 | 419 | //adjust the new mouse position to take into account any out of bounds amounts 420 | //if any neg max values or pos min values 421 | mouseX = mouseX + ((checkminX > 0) ? checkminX : 0) + ((checkmaxX < 0) ? checkmaxX : 0); 422 | mouseY = mouseY + ((checkminY > 0) ? checkminY : 0) + ((checkmaxY < 0) ? checkmaxY : 0); 423 | 424 | //recalculate the mouse delta based on the revised mouse position 425 | var deltaX = mouseX - getmousemeta(element).mousemeta.x; 426 | var deltaY = mouseY - getmousemeta(element).mousemeta.y; 427 | 428 | //store the new mouse location 429 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: deltaX, deltaY: deltaY }); 430 | 431 | if (resizing) 432 | //store the new target 433 | { 434 | currentmeta.target = getresizedelement(element, deltaX, deltaY); 435 | setmeta(element.parentElement, currentmeta.current, currentmeta.target, currentmeta.step); 436 | } 437 | else { 438 | //store the new mouse location 439 | //the target is the current target + the calculated delta, 440 | //as the currentX represents some position between originalx and the target, we add the delta to the target 441 | currentmeta.target.x = Math.round(currentmeta.target.x + deltaX); 442 | currentmeta.target.y = Math.round(currentmeta.target.y + deltaY); 443 | 444 | //store the new target 445 | setmeta(element, currentmeta.current, currentmeta.target, currentmeta.step); 446 | } 447 | } 448 | 449 | function getresizedelement(element, deltaX, deltaY,roundvalues=false) { 450 | 451 | var currentmeta = getmeta(element.parentElement); 452 | var tempmeta = currentmeta.target; 453 | 454 | if (element.classList.contains('bottom-right')) { 455 | const width = currentmeta.target.w + deltaX; 456 | const height = currentmeta.target.h + deltaY; 457 | if (width > minimum_size) { 458 | tempmeta.w = width; 459 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 460 | 461 | } 462 | if (height > minimum_size) { 463 | tempmeta.h = height; 464 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 465 | } 466 | } 467 | 468 | else if (element.classList.contains('bottom-left')) { 469 | const height = currentmeta.target.h + deltaY; 470 | const width = currentmeta.target.w - deltaX; 471 | if (height > minimum_size) { 472 | tempmeta.h = height; 473 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 474 | } 475 | if (width > minimum_size) { 476 | tempmeta.w = width; 477 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 478 | } 479 | } 480 | 481 | else if (element.classList.contains('top-right')) { 482 | const width = currentmeta.target.w + deltaX; 483 | const height = currentmeta.target.h - deltaY; 484 | if (width > minimum_size) { 485 | tempmeta.w = width; 486 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 487 | } 488 | if (height > minimum_size) { 489 | tempmeta.h = height; 490 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 491 | 492 | } 493 | } 494 | 495 | else {//top-left 496 | const width = currentmeta.target.w - deltaX; 497 | const height = currentmeta.target.h - deltaY; 498 | if (width > minimum_size) { 499 | tempmeta.w = width; 500 | tempmeta.x = currentmeta.target.x + (deltaX/2); 501 | } 502 | if (height > minimum_size) { 503 | tempmeta.h = height; 504 | tempmeta.y = currentmeta.target.y + (deltaY/2); 505 | } 506 | } 507 | 508 | if (roundvalues) { 509 | tempmeta.w = Math.round(tempmeta.w); 510 | tempmeta.h = Math.round(tempmeta.h); 511 | tempmeta.x = Math.round(tempmeta.x); 512 | tempmeta.y = Math.round(tempmeta.y); 513 | } 514 | 515 | return tempmeta; 516 | 517 | } 518 | 519 | function onTimerTick() { 520 | 521 | //get the correct element to action 522 | 523 | var actionelement = (resizing) ? currentelement.parentElement : currentelement; 524 | 525 | var currentmeta = getmeta(actionelement); 526 | 527 | //calculate the step 528 | currentmeta.step.x = easeAmount * (currentmeta.target.x - currentmeta.current.x); 529 | currentmeta.step.y = easeAmount * (currentmeta.target.y - currentmeta.current.y); 530 | currentmeta.step.w = easeAmount * (currentmeta.target.w - currentmeta.current.w); 531 | currentmeta.step.h = easeAmount * (currentmeta.target.h - currentmeta.current.h); 532 | 533 | //adjust the current location 534 | currentmeta.current.x = currentmeta.current.x + currentmeta.step.x; 535 | currentmeta.current.y = currentmeta.current.y + currentmeta.step.y; 536 | currentmeta.current.w = currentmeta.current.w + currentmeta.step.w; 537 | currentmeta.current.h = currentmeta.current.h + currentmeta.step.h; 538 | 539 | //stop the timer when the target position is reached (close enough) 540 | if ( 541 | (!dragging) && 542 | (Math.abs(currentmeta.current.x - currentmeta.target.x) < 0.1) && 543 | (Math.abs(currentmeta.current.y - currentmeta.target.y) < 0.1) 544 | && 545 | (Math.abs(currentmeta.current.w - currentmeta.target.w) < 0.1) && 546 | (Math.abs(currentmeta.current.h - currentmeta.target.h) < 0.1) 547 | ) 548 | { 549 | currentmeta.current.x = currentmeta.target.x; 550 | currentmeta.current.y = currentmeta.target.y; 551 | currentmeta.current.w = currentmeta.target.w; 552 | currentmeta.current.h = currentmeta.target.h; 553 | 554 | //stop timer: 555 | 556 | delete timers[timer]; 557 | 558 | clearInterval(timer); 559 | } 560 | 561 | //save the new location 562 | setmeta(actionelement, currentmeta.current, currentmeta.target, currentmeta.step) 563 | 564 | //move the element 565 | actionelement.style.top = Math.round(currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 566 | actionelement.style.left = Math.round(currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 567 | 568 | actionelement.style.width = Math.round(currentmeta.current.w).toString() + 'px'; 569 | actionelement.style.height = Math.round(currentmeta.current.h).toString() + 'px'; 570 | 571 | } 572 | 573 | function mouseUpListener(event) { 574 | 575 | //because we were moving we stick with the current element and dont try to determine who we are moving over 576 | //otherwise the mouseover finds another valid dragme and attachs the mouse down to the wrong element 577 | var element = currentelement; //getelement(event.target); 578 | 579 | element.addEventListener("mousedown", mouseDownListener, false); 580 | window.removeEventListener("mouseup", mouseUpListener, false); 581 | if (dragging) { 582 | dragging = false; 583 | if (resizing) { 584 | resizing = false; 585 | currentelement = currentelement.parentElement; // as we loose the resizer indicator, we need to let tick tock know which element to actually ease out 586 | } 587 | document.body.style.cursor = "default" 588 | window.removeEventListener("mousemove", mouseMoveListener, false); 589 | } 590 | } 591 | 592 | -------------------------------------------------------------------------------- /archive/smoothpositioning.3.3.js: -------------------------------------------------------------------------------- 1 | // JavaScript source code 2 | 3 | //create 3.3. 4 | //calculate the offset of the current element/module from its parent(or we need to find a parent positioned element) 5 | //use this offset to amend the final location so that we adjust back from being relative to body to being relative to its real parent 6 | 7 | // JavaScript source code 8 | 9 | //each element carries 3 sets of information: 10 | //1) where am i (x,y,w,h) (current/also obtainable from the element itself) 11 | //2) where am i going (x,y,w,h) (target) 12 | //3) how do i get there (xstep,ystep,wstep,hstep) the value towards target controlled by the easing % 13 | 14 | // 0 - all locations held in the element relate to its centre, so need to be adjusted for showing 15 | // 1 - initialise all elements with current,target and steps 16 | // 2 - add the resizing stuff to the element, and make it absolute 17 | // 3 - listen for mousedown on an element and reset the details for the element(or parent) 18 | // 4 - listen for mousedown on an elements resizer (how do we know ? ask the parent) ditto 19 | // 5 - start timer, tell the world we are dragging, do some javascript stuff and set window level event listeners for move and up 20 | // 6 - and may be add a mouse out for the canvas to capture when some one goes awol 21 | // 7 - on each mouse move, update the target, recalc the steps from the current using easing % 22 | // 8 - the timer simply draws where we are based on current + step and adjusts current 23 | // 9 - on mouseup we stop dragging and let the timer take the element to its target, one step at a time 24 | 25 | //default local variables 26 | 27 | var easeAmount = 0.30 // percentage of delta to step 28 | var FPS = 15 // frames per second 29 | var interval = 1000 / FPS //how long each frame lasts for 30 | var minimum_size = 50; 31 | 32 | //---------------- 33 | 34 | var currentelement; 35 | var timers = {}; 36 | var dragging = false; 37 | var resizing = false; 38 | 39 | function smoothpositioninginit(smoothpositioningconfig) { 40 | 41 | //add verification that the config has been set 42 | //TODO - support null canvasid = viewable window 43 | 44 | if (smoothpositioningconfig.canvasid.toLowerCase() == 'body') { 45 | theCanvas = document.body; 46 | } 47 | else { 48 | theCanvas = document.getElementById(smoothpositioningconfig.canvasid); 49 | } 50 | 51 | //set the local variables 52 | 53 | easeAmount = smoothpositioningconfig.easeAmount; 54 | FPS = smoothpositioningconfig.FPS; 55 | interval = 1000 / FPS; 56 | minimum_size = smoothpositioningconfig.minimum_size; 57 | 58 | } 59 | 60 | function setmeta(element, original, current, target, step) { 61 | element.dataset.meta = JSON.stringify({ original: original, current: current, target: target, step: step }); 62 | } 63 | 64 | function setstate(element, amended, active, absolute) { 65 | element.dataset.state = JSON.stringify({ amended: amended, active: active, absolute: absolute }); 66 | } 67 | 68 | function setcss(element, offsetX, offsetY) { 69 | element.dataset.cssoffset = JSON.stringify({ offsetX: offsetX, offsetY: offsetY }); 70 | } 71 | 72 | function getcss(element) { 73 | return JSON.parse(element.dataset.cssoffset); 74 | } 75 | 76 | function setmousemeta(element, mousemeta) { 77 | element.dataset.mousemeta = JSON.stringify({ mousemeta: mousemeta }); 78 | } 79 | 80 | function getmeta(element) { 81 | return JSON.parse(element.dataset.meta); 82 | //({ original:original, current: current, target: target, step: step }); 83 | } 84 | 85 | function getstate(element) { 86 | return JSON.parse(element.dataset.state); 87 | } 88 | 89 | function getmousemeta(element) { 90 | return JSON.parse(element.dataset.mousemeta); 91 | } 92 | 93 | function getcurrentmeta(element) { 94 | 95 | //adjust the x,y to the centre of the element 96 | //x,y this is its apparent absolute position, manually taking into account margins etc 97 | //we know this is relative to the body 98 | 99 | var temp = { x: 0, y: 0, w: 0, h: 0 }; 100 | 101 | var trueoffset = getmouseposition({ clientX: element.offsetLeft, clientY: element.offsetTop }); 102 | 103 | //alert(JSON.stringify(element.offsetLeft)); 104 | 105 | //get the style information 106 | var tempstyle = theCanvas.currentStyle || window.getComputedStyle(theCanvas); 107 | 108 | temp.x = -parseFloat(tempstyle.marginLeft.replace('px', '')) + (element.getBoundingClientRect().left + element.getBoundingClientRect().width / 2); 109 | temp.y = -parseFloat(tempstyle.marginTop.replace('px', '')) + (element.getBoundingClientRect().top + element.getBoundingClientRect().height / 2); 110 | 111 | temp.w = element.getBoundingClientRect().width; 112 | temp.h = element.getBoundingClientRect().height; 113 | 114 | return temp; 115 | 116 | } 117 | 118 | function setgrid(name, meta) { 119 | var t; 120 | t = document.getElementById('metagridname') 121 | t.innerHTML = name; 122 | t = document.getElementById('metagridx') 123 | t.innerHTML = 'X: ' + meta.x.toFixed(2); 124 | t = document.getElementById('metagridy') 125 | t.innerHTML = 'Y: ' + meta.y.toFixed(2); 126 | t = document.getElementById('metagridw') 127 | t.innerHTML = 'W: ' + meta.w.toFixed(2); 128 | t = document.getElementById('metagridh') 129 | t.innerHTML = 'H: ' + meta.h.toFixed(2); 130 | } 131 | 132 | function makedraggable(element) { 133 | 134 | element.classList.add("drag"); 135 | element.addEventListener("mousedown", mouseDownListener, false); 136 | 137 | //get the original location based on whatever the CSS is at the time of loading the element 138 | var origmeta = getcurrentmeta(element); 139 | 140 | var origLeft = (origmeta.x - (origmeta.w / 2)); 141 | var origTop = (origmeta.y - (origmeta.h / 2)); 142 | 143 | setmeta(element, origmeta, origmeta, origmeta, { x: 0, y: 0, w: 0, h: 0 }); 144 | 145 | console.log("ox", origmeta.x, origmeta.w); 146 | console.log("oy", origmeta.y, origmeta.h); 147 | 148 | //apply absolute, store the new location and reset to the original positioning 149 | //this gives us any positioning deltas we need to apply to the CSS when we create the custom CSS 150 | 151 | //need to only handle inline styles !! 152 | //so we access the element.style AND NOT THE computed style 153 | //this shows style entries that are actually inline and ignores those from stylesheets 154 | 155 | var originalposition = element.style.position; 156 | 157 | element.style.position = 'absolute'; 158 | 159 | var absmeta = getcurrentmeta(element); 160 | 161 | if (originalposition == '') { 162 | element.style.removeProperty('position'); 163 | } 164 | else { 165 | element.style.position = originalposition; 166 | } 167 | 168 | var absdeltaLeft = (absmeta.x - (absmeta.w / 2)) - origLeft; 169 | var absdeltaTop = (absmeta.y - (absmeta.h / 2)) - origTop; 170 | 171 | var offsetX = element.offsetLeft - origLeft - absdeltaLeft; 172 | var offsetY = element.offsetTop - origTop - absdeltaTop; 173 | 174 | //and store them in the element 175 | 176 | setcss(element, offsetX, offsetY); 177 | 178 | //add a couple of tracking elements and check if this is absolute positioned at any specificity 179 | 180 | setstate(element, false, false, (window.getComputedStyle(element, null).position == 'absolute')); 181 | 182 | //add an observer to catch a change to the position (made by the main.js as part of hiding/showing modules, animating transitions) 183 | //so we can override and keep them visible at all times 184 | 185 | // Select the node that will be observed for mutations 186 | const targetNode = element; 187 | 188 | // Options for the observer (which mutations to observe) 189 | const config = { attributes: true, attributeFilter: ["style"], attributeOldValue: true, }; 190 | 191 | // Callback function to execute when mutations are observed 192 | // only actually fire once the target element is active 193 | const callback = function (mutationsList, observer) { 194 | // Use traditional 'for loops' for IE 11 195 | for (let mutation of mutationsList) { 196 | if (mutation.target.dataset != null) { 197 | var state = getstate(mutation.target); 198 | // start this as soon as we have loaded as we need to show the module in the location we want and not have 199 | // the static position override the absolute if we need it 200 | if (state.active || state.absolute) { 201 | var oldvalue = getstyleasjson(mutation.oldValue); 202 | if (oldvalue != null) { 203 | //console.log('The ' + mutation.attributeName + ' attribute of element ' + mutation.target.id + ' was modified. Old value was ' + oldvalue.position); 204 | //console.log('The new position is ' + mutation.target.style.position); 205 | if (mutation.target.style.postion != 'absolute') { 206 | mutation.target.style.position = 'absolute' 207 | }; 208 | } 209 | } 210 | } 211 | } 212 | }; 213 | 214 | function getstyleasjson(stylestring) { 215 | if (stylestring == null) { return null }; 216 | var temp = ''; 217 | var obj = stylestring.split(";"); 218 | obj.forEach(function (pair) { 219 | if (pair != "") { 220 | var jpair = pair.split(":"); 221 | temp = temp + '"' + jpair[0].trim() + '":"' + jpair[1].trim() + '",'; 222 | } 223 | }) 224 | temp = "{" + temp.substr(0, temp.length - 1) + "}" 225 | return JSON.parse(temp); 226 | } 227 | 228 | // Create an observer instance linked to the callback function 229 | const observer = new MutationObserver(callback); 230 | 231 | // Start observing the target node for configured mutations 232 | observer.observe(targetNode, config); 233 | 234 | // Later, you can stop observing 235 | //observer.disconnect(); 236 | 237 | } 238 | 239 | function makeresizable(element) { 240 | 241 | element.classList.add("resizable"); 242 | 243 | var divtl = document.createElement('div'); 244 | divtl.classList.add("resizer"); 245 | divtl.classList.add("top-left"); 246 | element.appendChild(divtl); 247 | 248 | var divtr = document.createElement('div'); 249 | divtr.classList.add("resizer"); 250 | divtr.classList.add("top-right"); 251 | element.appendChild(divtr); 252 | 253 | var divbl = document.createElement('div'); 254 | divbl.classList.add("resizer"); 255 | divbl.classList.add("bottom-left"); 256 | element.appendChild(divbl); 257 | 258 | var divbr = document.createElement('div'); 259 | divbr.classList.add("resizer"); 260 | divbr.classList.add("bottom-right"); 261 | element.appendChild(divbr); 262 | 263 | divtl.addEventListener("mousedown", mouseDownListener, false); 264 | divtr.addEventListener("mousedown", mouseDownListener, false); 265 | divbl.addEventListener("mousedown", mouseDownListener, false); 266 | divbr.addEventListener("mousedown", mouseDownListener, false); 267 | 268 | setmeta(element, getcurrentmeta(element), getcurrentmeta(element), getcurrentmeta(element), { x: 0, y: 0, w: 0, h: 0 }); 269 | 270 | } 271 | 272 | //get the actual element not the resizer 273 | function getelement(element, getparent = false) { 274 | 275 | if (element.classList == null) { //must be over the body or elsewhere if this fires 276 | return currentelement; 277 | } 278 | 279 | if (element.classList.contains('drag')) { 280 | return element; 281 | } 282 | 283 | if (element.classList.contains('resizer')) { // we manage all movement based on the resizer circles, only redrawing is at parent level 284 | if (getparent) { 285 | return element.parentElement; 286 | } 287 | else { 288 | return element; 289 | } 290 | } 291 | 292 | //must be over the body or elsewhere 293 | 294 | return currentelement; 295 | 296 | } 297 | 298 | function getmouseposition(mouseevent) { 299 | 300 | //additional support for a canvas that hasn't yet been populated (like body) 301 | //it takes a default value of the window 302 | 303 | var defaultheight = window.innerHeight; 304 | var defaultwidth = window.innerWidth; 305 | 306 | //getting mouse position correctly 307 | var bRect = theCanvas.getBoundingClientRect(); 308 | mouseX = (mouseevent.clientX - bRect.left) * (((theCanvas.clientWidth == 0) ? defaultwidth : theCanvas.clientWidth) / ((bRect.width == 0) ? defaultwidth : bRect.width)); 309 | mouseY = (mouseevent.clientY - bRect.top) * (((theCanvas.clientHeight == 0) ? defaultheight : theCanvas.clientHeight) / ((bRect.height == 0) ? defaultheight : bRect.height)); 310 | 311 | return { mouseX: mouseX, mouseY: mouseY }; 312 | 313 | } 314 | 315 | //mousedown supports both resize and draggable 316 | //theCanvas is whatever element is used to constrain the action 317 | 318 | function mouseDownListener(event) { 319 | 320 | //stop a resizer mousedown from bubbling up to the parent and vice versa 321 | 322 | event.stopPropagation(); 323 | 324 | var mouse = getmouseposition(event); 325 | 326 | var mouseX = mouse.mouseX; 327 | var mouseY = mouse.mouseY; 328 | 329 | //determine if we are dragging or resizing 330 | 331 | dragging = true; //we found something to drag //this should always be true as the mousedown events are only linked to draggable and re-sizable elements 332 | 333 | //but, if there is an outstanding timer, (about to be orphaned) , don't let the action start 334 | 335 | if (Object.keys(timers).length > 0) { 336 | dragging = false; 337 | } 338 | 339 | if (dragging) { 340 | 341 | event.currentTarget.removeEventListener("mousedown", mouseDownListener, false); 342 | 343 | //determine who we are dealing with 344 | //element is the mousedown, that may be a resizer, in which case we need the parent 345 | 346 | var element = getelement(event.currentTarget); 347 | var parentelement = getelement(event.currentTarget, true); 348 | 349 | //pop the div to the top level so absolute actual works 350 | //and make it absolute here so we have correct initial positioning 351 | //before we do this we set the new location to the original location before we apply the positioning 352 | //absolut positioning will overide vertain #CSS settings and the element may move when it is made absolute 353 | //and we get the latest values for w/h/x/y because they have changed since last we were here for this element 354 | //and depending on its contents the w/h may change 355 | 356 | setmeta(parentelement, getmeta(parentelement).original, getcurrentmeta(parentelement), getcurrentmeta(parentelement), { x: 0, y: 0, w: 0, h: 0 }) 357 | var currentmeta = getmeta(parentelement); 358 | 359 | //move the element 360 | parentelement.style.top = Math.round(currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 361 | parentelement.style.left = Math.round(currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 362 | 363 | parentelement.style.width = Math.round(currentmeta.current.w).toString() + 'px'; 364 | parentelement.style.height = Math.round(currentmeta.current.h).toString() + 'px'; 365 | 366 | parentelement.style.position = 'absolute'; 367 | 368 | setstate(parentelement, getstate(parentelement).amended, true, getstate(parentelement).absolute); //set active to true 369 | 370 | document.body.append(parentelement); 371 | 372 | //tell the mutation observer for this element to start observing. 373 | 374 | //parentelement. 375 | 376 | //check if we are actually resizing 377 | 378 | if (element != parentelement) { 379 | resizing = true; 380 | } 381 | 382 | if (!resizing) { 383 | document.body.style.cursor = "move"; 384 | }; 385 | 386 | //store the current element 387 | currentelement = element; 388 | 389 | window.addEventListener("mousemove", mouseMoveListener, false); 390 | window.addEventListener("mouseup", mouseUpListener, false); 391 | 392 | //store the current mouse position 393 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: 0, deltaY: 0 }); 394 | 395 | //adjust the element target to be same as location (it should be anyway) 396 | currentmeta = getmeta((resizing) ? parentelement : element); 397 | setmeta((resizing) ? parentelement : element, getmeta((resizing) ? parentelement : element).original, currentmeta.current, currentmeta.current, currentmeta.step); 398 | 399 | timer = setInterval(onTimerTick, 1000 / interval); 400 | timers[timer] = timer; 401 | 402 | //code below prevents the mouse down from having an effect on the main browser window: 403 | if (event.preventDefault) { 404 | event.preventDefault(); 405 | } //standard 406 | else if (event.returnValue) { 407 | event.returnValue = false; 408 | } //older IE 409 | return false; 410 | } 411 | } 412 | 413 | function mouseMoveListener(event) { 414 | 415 | //work out the delta of mouse 416 | //new mouse becomes target 417 | //after clamping to the canvas 418 | 419 | //because we are moving we stick with the current element and dont try to determine who we are moving over 420 | //otherwise the mouseover finds another valid element 421 | 422 | var element = currentelement; 423 | 424 | //as the element has been moved, we assume it has been amended 425 | //resizing works differently to dragging so we have to split the parts of the code - really ! 426 | 427 | if (resizing) { 428 | var currentmeta = getmeta(element.parentElement); 429 | setstate(element, true, getstate(element.parentElement).active, getstate(element.parentElement).absolute); //set amended true 430 | } 431 | else { 432 | var currentmeta = getmeta(element); 433 | setstate(element, true, getstate(element).active, getstate(element).absolute); //set amended true 434 | } 435 | 436 | var checkmeta = { target: currentmeta.target }; 437 | 438 | //get new mouse position 439 | var mouse = getmouseposition(event); 440 | var mouseX = mouse.mouseX; 441 | var mouseY = mouse.mouseY; 442 | 443 | //determine if new target is in bounds 444 | //test is based on the existing target being adjusted by the delta NOT the current location of the element as that is being ticked 445 | //we need to take into account that resizing adjusts x or y and h or w as deltaX an deltaY change by creating a temporary new target before testing it 446 | //delta(x,y) applied to oldtarget (x,y) must be between min(x,y) and max(x,y) 447 | //otherwise clamp the delta to a value to adhere to the above rule 448 | 449 | //mouse delta, try this first 450 | var deltaX = mouseX - getmousemeta(element).mousemeta.x; 451 | var deltaY = mouseY - getmousemeta(element).mousemeta.y; 452 | 453 | if (resizing) { 454 | 455 | //calculate the new element size and centre 456 | checkmeta.target = getresizedelement(element, deltaX, deltaY, true); 457 | 458 | } 459 | 460 | //calculate the bounds based on the new target size 461 | var minX = (checkmeta.target.w / 2); 462 | var maxX = (((theCanvas.clientWidth == 0) ? window.innerWidth : theCanvas.clientWidth) - (checkmeta.target.w / 2)); 463 | var minY = (checkmeta.target.h / 2); 464 | var maxY = (((theCanvas.clientHeight == 0) ? window.innerHeight : theCanvas.clientHeight) - (checkmeta.target.h / 2)); 465 | 466 | //check the centre fits within the bounds 467 | 468 | if (resizing) { 469 | //checkmax returns a -value if out of bounds 470 | //checkmin returns a +value if out of bounds 471 | var checkmaxX = (maxX - checkmeta.target.x); 472 | var checkmaxY = (maxY - checkmeta.target.y); 473 | var checkminX = (minX - checkmeta.target.x); 474 | var checkminY = (minY - checkmeta.target.y); 475 | } 476 | else { 477 | 478 | var checkmaxX = Math.round(maxX - (checkmeta.target.x + deltaX)); 479 | var checkmaxY = Math.round(maxY - (checkmeta.target.y + deltaY)); 480 | var checkminX = Math.round(minX - (checkmeta.target.x + deltaX)); 481 | var checkminY = Math.round(minY - (checkmeta.target.y + deltaY)); 482 | } 483 | 484 | //adjust the new mouse position to take into account any out of bounds amounts 485 | //if any neg max values or pos min values 486 | mouseX = mouseX + ((checkminX > 0) ? checkminX : 0) + ((checkmaxX < 0) ? checkmaxX : 0); 487 | mouseY = mouseY + ((checkminY > 0) ? checkminY : 0) + ((checkmaxY < 0) ? checkmaxY : 0); 488 | 489 | //recalculate the mouse delta based on the revised mouse position 490 | var deltaX = mouseX - getmousemeta(element).mousemeta.x; 491 | var deltaY = mouseY - getmousemeta(element).mousemeta.y; 492 | 493 | //store the new mouse location 494 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: deltaX, deltaY: deltaY }); 495 | 496 | if (resizing) 497 | //store the new target 498 | { 499 | currentmeta.target = getresizedelement(element, deltaX, deltaY); 500 | setmeta(element.parentElement, getmeta(element.parentElement).original, currentmeta.current, currentmeta.target, currentmeta.step); 501 | setgrid(element.parentElement.id, getmeta(element.parentElement).current); 502 | } 503 | else { 504 | //store the new mouse location 505 | //the target is the current target + the calculated delta, 506 | //as the currentX represents some position between originalx and the target, we add the delta to the target 507 | currentmeta.target.x = Math.round(currentmeta.target.x + deltaX); 508 | currentmeta.target.y = Math.round(currentmeta.target.y + deltaY); 509 | 510 | //store the new target 511 | setmeta(element, getmeta(element).original, currentmeta.current, currentmeta.target, currentmeta.step); 512 | setgrid(element.id, getmeta(element).current); 513 | } 514 | 515 | 516 | } 517 | 518 | function getresizedelement(element, deltaX, deltaY, roundvalues = false) { 519 | 520 | var currentmeta = getmeta(element.parentElement); 521 | var tempmeta = currentmeta.target; 522 | 523 | if (element.classList.contains('bottom-right')) { 524 | const width = currentmeta.target.w + deltaX; 525 | const height = currentmeta.target.h + deltaY; 526 | if (width > minimum_size) { 527 | tempmeta.w = width; 528 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 529 | 530 | } 531 | if (height > minimum_size) { 532 | tempmeta.h = height; 533 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 534 | } 535 | } 536 | 537 | else if (element.classList.contains('bottom-left')) { 538 | const height = currentmeta.target.h + deltaY; 539 | const width = currentmeta.target.w - deltaX; 540 | if (height > minimum_size) { 541 | tempmeta.h = height; 542 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 543 | } 544 | if (width > minimum_size) { 545 | tempmeta.w = width; 546 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 547 | } 548 | } 549 | 550 | else if (element.classList.contains('top-right')) { 551 | const width = currentmeta.target.w + deltaX; 552 | const height = currentmeta.target.h - deltaY; 553 | if (width > minimum_size) { 554 | tempmeta.w = width; 555 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 556 | } 557 | if (height > minimum_size) { 558 | tempmeta.h = height; 559 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 560 | 561 | } 562 | } 563 | 564 | else {//top-left 565 | const width = currentmeta.target.w - deltaX; 566 | const height = currentmeta.target.h - deltaY; 567 | if (width > minimum_size) { 568 | tempmeta.w = width; 569 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 570 | } 571 | if (height > minimum_size) { 572 | tempmeta.h = height; 573 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 574 | } 575 | } 576 | 577 | if (roundvalues) { 578 | tempmeta.w = Math.round(tempmeta.w); 579 | tempmeta.h = Math.round(tempmeta.h); 580 | tempmeta.x = Math.round(tempmeta.x); 581 | tempmeta.y = Math.round(tempmeta.y); 582 | } 583 | 584 | return tempmeta; 585 | 586 | } 587 | 588 | function onTimerTick() { 589 | 590 | //get the correct element to action 591 | 592 | var actionelement = (resizing) ? currentelement.parentElement : currentelement; 593 | 594 | var currentmeta = getmeta(actionelement); 595 | 596 | //calculate the step 597 | currentmeta.step.x = easeAmount * (currentmeta.target.x - currentmeta.current.x); 598 | currentmeta.step.y = easeAmount * (currentmeta.target.y - currentmeta.current.y); 599 | currentmeta.step.w = easeAmount * (currentmeta.target.w - currentmeta.current.w); 600 | currentmeta.step.h = easeAmount * (currentmeta.target.h - currentmeta.current.h); 601 | 602 | //adjust the current location 603 | currentmeta.current.x = currentmeta.current.x + currentmeta.step.x; 604 | currentmeta.current.y = currentmeta.current.y + currentmeta.step.y; 605 | currentmeta.current.w = currentmeta.current.w + currentmeta.step.w; 606 | currentmeta.current.h = currentmeta.current.h + currentmeta.step.h; 607 | 608 | //stop the timer when the target position is reached (close enough) 609 | if ( 610 | (!dragging) && 611 | (Math.abs(currentmeta.current.x - currentmeta.target.x) < 0.1) && 612 | (Math.abs(currentmeta.current.y - currentmeta.target.y) < 0.1) 613 | && 614 | (Math.abs(currentmeta.current.w - currentmeta.target.w) < 0.1) && 615 | (Math.abs(currentmeta.current.h - currentmeta.target.h) < 0.1) 616 | ) { 617 | currentmeta.current.x = currentmeta.target.x; 618 | currentmeta.current.y = currentmeta.target.y; 619 | currentmeta.current.w = currentmeta.target.w; 620 | currentmeta.current.h = currentmeta.target.h; 621 | 622 | //stop timer: 623 | 624 | delete timers[timer]; 625 | 626 | clearInterval(timer); 627 | } 628 | 629 | //save the new location 630 | setmeta(actionelement, getmeta(actionelement).original, currentmeta.current, currentmeta.target, currentmeta.step) 631 | 632 | //move the element 633 | actionelement.style.top = Math.round(currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 634 | actionelement.style.left = Math.round(currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 635 | 636 | actionelement.style.width = Math.round(currentmeta.current.w).toString() + 'px'; 637 | actionelement.style.height = Math.round(currentmeta.current.h).toString() + 'px'; 638 | 639 | } 640 | 641 | function mouseUpListener(event) { 642 | 643 | //because we were moving we stick with the current element and dont try to determine who we are moving over 644 | //otherwise the mouseover finds another valid dragme and attachs the mouse down to the wrong element 645 | var element = currentelement; //getelement(event.target); 646 | 647 | element.addEventListener("mousedown", mouseDownListener, false); 648 | window.removeEventListener("mouseup", mouseUpListener, false); 649 | if (dragging) { 650 | dragging = false; 651 | if (resizing) { 652 | resizing = false; 653 | currentelement = currentelement.parentElement; // as we loose the resizer indicator, we need to let tick tock know which element to actually ease out 654 | } 655 | document.body.style.cursor = "default" 656 | window.removeEventListener("mousemove", mouseMoveListener, false); 657 | } 658 | } 659 | 660 | -------------------------------------------------------------------------------- /archive/smoothpositioning.3.4.js: -------------------------------------------------------------------------------- 1 | 2 | //create 3.3. 3 | //calculate the offset of the current element/module from its parent(or we need to find a parent positioned element) 4 | //use this offset to amend the final location so that we adjust back from being relative to body to being relative to its real parent 5 | 6 | //create 3.4 7 | //add snap to grid and toggle switches to enable grid snapping 8 | //uses grid to control size of grid - 1 should ensure current activity 9 | //applied when calculating the new target. uses code from fabricjs 10 | 11 | // JavaScript source code 12 | 13 | //each element carries 3 sets of information: 14 | //1) where am i (x,y,w,h) (current/also obtainable from the element itself) 15 | //2) where am i going (x,y,w,h) (target) 16 | //3) how do i get there (xstep,ystep,wstep,hstep) the value towards target controlled by the easing % 17 | 18 | // 0 - all locations held in the element relate to its centre, so need to be adjusted for showing 19 | // 1 - initialise all elements with current,target and steps 20 | // 2 - add the resizing stuff to the element, and make it absolute 21 | // 3 - listen for mousedown on an element and reset the details for the element(or parent) 22 | // 4 - listen for mousedown on an elements resizer (how do we know ? ask the parent) ditto 23 | // 5 - start timer, tell the world we are dragging, do some javascript stuff and set window level event listeners for move and up 24 | // 6 - and may be add a mouse out for the canvas to capture when some one goes awol 25 | // 7 - on each mouse move, update the target, recalc the steps from the current using easing % 26 | // 8 - the timer simply draws where we are based on current + step and adjusts current 27 | // 9 - on mouseup we stop dragging and let the timer take the element to its target, one step at a time 28 | 29 | //default local variables 30 | 31 | var easeAmount = 0.30 // percentage of delta to step 32 | var FPS = 15 // frames per second 33 | var interval = 1000 / FPS //how long each frame lasts for 34 | var minimum_size = 50; 35 | var grid = 10; // needs to be low otherwise dragging can stall 36 | 37 | //---------------- 38 | 39 | var usegrid = false; 40 | var currentelement; 41 | var timers = {}; 42 | var dragging = false; 43 | var resizing = false; 44 | var stallmeta = {element:{x:0, y:0}, mousedown: {x:0,y:0}} 45 | 46 | function smoothpositioninginit(smoothpositioningconfig) { 47 | 48 | //add verification that the config has been set 49 | //TODO - support null canvasid = viewable window 50 | 51 | if (smoothpositioningconfig.canvasid.toLowerCase() == 'body') { 52 | theCanvas = document.body; 53 | } 54 | else { 55 | theCanvas = document.getElementById(smoothpositioningconfig.canvasid); 56 | } 57 | 58 | //set the local variables 59 | 60 | easeAmount = smoothpositioningconfig.easeAmount; 61 | FPS = smoothpositioningconfig.FPS; 62 | interval = 1000 / FPS; 63 | minimum_size = smoothpositioningconfig.minimum_size; 64 | grid = smoothpositioningconfig.grid; 65 | 66 | } 67 | 68 | function setmeta(element, original, current, target, step) { 69 | element.dataset.meta = JSON.stringify({ original:original, current: current, target: target, step: step }); 70 | } 71 | 72 | function setstate(element, amended, active, absolute) { 73 | element.dataset.state = JSON.stringify({ amended: amended, active: active, absolute: absolute}); 74 | } 75 | 76 | function setcss(element, offsetX, offsetY) { 77 | element.dataset.cssoffset = JSON.stringify({ offsetX: offsetX, offsetY: offsetY }); 78 | } 79 | 80 | function getcss(element) { 81 | return JSON.parse(element.dataset.cssoffset); 82 | } 83 | 84 | function setmousemeta(element, mousemeta) { 85 | element.dataset.mousemeta = JSON.stringify({ mousemeta: mousemeta }); 86 | } 87 | 88 | function getmeta(element) { 89 | return JSON.parse(element.dataset.meta); 90 | //({ original:original, current: current, target: target, step: step }); 91 | } 92 | 93 | function getstate(element) { 94 | return JSON.parse(element.dataset.state); 95 | } 96 | 97 | function getmousemeta(element) { 98 | return JSON.parse(element.dataset.mousemeta); 99 | } 100 | 101 | function getcurrentmeta(element) { 102 | 103 | //adjust the x,y to the centre of the element 104 | //x,y this is its apparent absolute position, manually taking into account margins etc 105 | //we know this is relative to the body 106 | 107 | var temp = { x: 0, y: 0, w: 0, h: 0 }; 108 | 109 | var trueoffset = getmouseposition({ clientX: element.offsetLeft, clientY: element.offsetTop }); 110 | 111 | //alert(JSON.stringify(element.offsetLeft)); 112 | 113 | //get the style information 114 | var tempstyle = theCanvas.currentStyle || window.getComputedStyle(theCanvas); 115 | 116 | temp.x = -parseFloat(tempstyle.marginLeft.replace('px', '')) + (element.getBoundingClientRect().left + element.getBoundingClientRect().width / 2); 117 | temp.y = -parseFloat(tempstyle.marginTop.replace('px', '')) + (element.getBoundingClientRect().top + element.getBoundingClientRect().height / 2); 118 | 119 | temp.w = element.getBoundingClientRect().width; 120 | temp.h = element.getBoundingClientRect().height; 121 | 122 | return temp; 123 | } 124 | 125 | function togglegrid() { 126 | 127 | if (grid > 0) { 128 | usegrid = document.getElementById('gridtoggle').checked; 129 | } 130 | } 131 | 132 | function setgrid(name, meta) { 133 | var t; 134 | t = document.getElementById('metagridname') 135 | t.innerHTML = name; 136 | t = document.getElementById('metagridx') 137 | t.innerHTML = 'X: ' + meta.x.toFixed(2); 138 | t = document.getElementById('metagridy') 139 | t.innerHTML = 'Y: ' + meta.y.toFixed(2); 140 | t = document.getElementById('metagridw') 141 | t.innerHTML = 'W: ' + meta.w.toFixed(2); 142 | t = document.getElementById('metagridh') 143 | t.innerHTML = 'H: ' + meta.h.toFixed(2); 144 | } 145 | 146 | function makedraggable(element) { 147 | 148 | element.classList.add("drag"); 149 | element.addEventListener("mousedown", mouseDownListener, false); 150 | 151 | //get the original location based on whatever the CSS is at the time of loading the element 152 | var origmeta = getcurrentmeta(element); 153 | 154 | var origLeft = (origmeta.x - (origmeta.w / 2)); 155 | var origTop = (origmeta.y - (origmeta.h / 2)); 156 | 157 | setmeta(element, origmeta, origmeta, origmeta, { x: 0, y: 0, w: 0, h: 0 }); 158 | 159 | //console.log("ox", origmeta.x, origmeta.w); 160 | //console.log("oy", origmeta.y, origmeta.h); 161 | 162 | //apply absolute, store the new location and reset to the original positioning 163 | //this gives us any positioning deltas we need to apply to the CSS when we create the custom CSS 164 | 165 | //need to only handle inline styles !! 166 | //so we access the element.style AND NOT THE computed style 167 | //this shows style entries that are actually inline and ignores those from stylesheets 168 | 169 | var originalposition = element.style.position; 170 | 171 | element.style.position = 'absolute'; 172 | 173 | var absmeta = getcurrentmeta(element); 174 | 175 | if (originalposition == '') { 176 | element.style.removeProperty('position'); 177 | } 178 | else { 179 | element.style.position = originalposition; 180 | } 181 | 182 | var absdeltaLeft = (absmeta.x - (absmeta.w / 2)) - origLeft; 183 | var absdeltaTop = (absmeta.y - (absmeta.h / 2)) - origTop; 184 | 185 | var offsetX = element.offsetLeft - origLeft - absdeltaLeft; 186 | var offsetY = element.offsetTop - origTop - absdeltaTop; 187 | 188 | //and store them in the element 189 | 190 | setcss(element, offsetX, offsetY); 191 | 192 | //add a couple of tracking elements and check if this is absolute positioned at any specificity 193 | 194 | setstate(element, false, false, (window.getComputedStyle(element, null).position == 'absolute')); 195 | 196 | //add an observer to catch a change to the position (made by the main.js as part of hiding/showing modules, animating transitions) 197 | //so we can override and keep them visible at all times 198 | 199 | // Select the node that will be observed for mutations 200 | const targetNode = element; 201 | 202 | // Options for the observer (which mutations to observe) 203 | const config = { attributes: true, attributeFilter: ["style"], attributeOldValue: true,}; 204 | 205 | // Callback function to execute when mutations are observed 206 | // only actually fire once the target element is active 207 | const callback = function (mutationsList, observer) { 208 | // Use traditional 'for loops' for IE 11 209 | for (let mutation of mutationsList) { 210 | if (mutation.target.dataset != null) { 211 | var state = getstate(mutation.target); 212 | // start this as soon as we have loaded as we need to show the module in the location we want and not have 213 | // the static position override the absolute if we need it 214 | if (state.active || state.absolute) { 215 | var oldvalue = getstyleasjson(mutation.oldValue); 216 | if (oldvalue != null) { 217 | //console.log('The ' + mutation.attributeName + ' attribute of element ' + mutation.target.id + ' was modified. Old value was ' + oldvalue.position); 218 | //console.log('The new position is ' + mutation.target.style.position); 219 | if (mutation.target.style.postion != 'absolute') { 220 | mutation.target.style.position = 'absolute' 221 | }; 222 | } 223 | } 224 | } 225 | } 226 | }; 227 | 228 | function getstyleasjson(stylestring) { 229 | if (stylestring == null) { return null }; 230 | var temp = ''; 231 | var obj = stylestring.split(";"); 232 | obj.forEach(function (pair) { 233 | if (pair != "") { 234 | var jpair = pair.split(":"); 235 | temp = temp + '"' + jpair[0].trim() + '":"' + jpair[1].trim() + '",'; 236 | } 237 | }) 238 | temp = "{" + temp.substr(0, temp.length - 1) + "}" 239 | return JSON.parse(temp); 240 | } 241 | 242 | // Create an observer instance linked to the callback function 243 | const observer = new MutationObserver(callback); 244 | 245 | // Start observing the target node for configured mutations 246 | observer.observe(targetNode, config); 247 | 248 | // Later, you can stop observing 249 | //observer.disconnect(); 250 | 251 | } 252 | 253 | function makeresizable(element) { 254 | 255 | element.classList.add("resizable"); 256 | 257 | var divtl = document.createElement('div'); 258 | divtl.classList.add("resizer"); 259 | divtl.classList.add("top-left"); 260 | element.appendChild(divtl); 261 | 262 | var divtr = document.createElement('div'); 263 | divtr.classList.add("resizer"); 264 | divtr.classList.add("top-right"); 265 | element.appendChild(divtr); 266 | 267 | var divbl = document.createElement('div'); 268 | divbl.classList.add("resizer"); 269 | divbl.classList.add("bottom-left"); 270 | element.appendChild(divbl); 271 | 272 | var divbr = document.createElement('div'); 273 | divbr.classList.add("resizer"); 274 | divbr.classList.add("bottom-right"); 275 | element.appendChild(divbr); 276 | 277 | divtl.addEventListener("mousedown", mouseDownListener, false); 278 | divtr.addEventListener("mousedown", mouseDownListener, false); 279 | divbl.addEventListener("mousedown", mouseDownListener, false); 280 | divbr.addEventListener("mousedown", mouseDownListener, false); 281 | 282 | setmeta(element, getcurrentmeta(element), getcurrentmeta(element), getcurrentmeta(element), { x: 0, y: 0, w: 0, h: 0 }); 283 | 284 | } 285 | 286 | //get the actual element not the resizer 287 | function getelement(element,getparent=false) { 288 | 289 | if (element.classList == null) { //must be over the body or elsewhere if this fires 290 | return currentelement; 291 | } 292 | 293 | if (element.classList.contains('drag')) { 294 | return element; 295 | } 296 | 297 | if (element.classList.contains('resizer')) { // we manage all movement based on the resizer circles, only redrawing is at parent level 298 | if (getparent) { 299 | return element.parentElement; 300 | } 301 | else { 302 | return element; 303 | } 304 | } 305 | 306 | //must be over the body or elsewhere 307 | 308 | return currentelement; 309 | 310 | } 311 | 312 | function getmouseposition(mouseevent) { 313 | 314 | //additional support for a canvas that hasn't yet been populated (like body) 315 | //it takes a default value of the window 316 | 317 | var defaultheight = window.innerHeight; 318 | var defaultwidth = window.innerWidth; 319 | 320 | //getting mouse position correctly 321 | var bRect = theCanvas.getBoundingClientRect(); 322 | mouseX = (mouseevent.clientX - bRect.left) * (((theCanvas.clientWidth == 0) ? defaultwidth : theCanvas.clientWidth) / ((bRect.width == 0) ? defaultwidth : bRect.width) ); 323 | mouseY = (mouseevent.clientY - bRect.top) * (((theCanvas.clientHeight == 0) ? defaultheight : theCanvas.clientHeight) / ((bRect.height == 0) ? defaultheight : bRect.height)); 324 | 325 | return { mouseX: mouseX, mouseY: mouseY }; 326 | 327 | } 328 | 329 | //mousedown supports both resize and draggable 330 | //theCanvas is whatever element is used to constrain the action 331 | 332 | function mouseDownListener(event) { 333 | 334 | //stop a resizer mousedown from bubbling up to the parent and vice versa 335 | 336 | event.stopPropagation(); 337 | 338 | var mouse = getmouseposition(event); 339 | 340 | var mouseX = mouse.mouseX; 341 | var mouseY = mouse.mouseY; 342 | 343 | //store the meta for the mousedown and element 344 | //so we can determine if the element dragging has stalled 345 | 346 | stallmeta.mousedown.x = mouseX; 347 | stallmeta.mousedown.y = mouseY; 348 | 349 | //determine if we are dragging or resizing 350 | 351 | dragging = true; //we found something to drag //this should always be true as the mousedown events are only linked to draggable and re-sizable elements 352 | 353 | //but, if there is an outstanding timer, (about to be orphaned) , don't let the action start 354 | 355 | if (Object.keys(timers).length > 0) { 356 | dragging = false; 357 | } 358 | 359 | if (dragging) { 360 | 361 | event.currentTarget.removeEventListener("mousedown", mouseDownListener, false); 362 | 363 | //determine who we are dealing with 364 | //element is the mousedown, that may be a resizer, in which case we need the parent 365 | 366 | var element = getelement(event.currentTarget); 367 | var parentelement = getelement(event.currentTarget, true); 368 | 369 | //pop the div to the top level so absolute actual works 370 | //and make it absolute here so we have correct initial positioning 371 | //before we do this we set the new location to the original location before we apply the positioning 372 | //absolute positioning will override certain #CSS settings and the element may move when it is made absolute 373 | //and we get the latest values for w/h/x/y because they have changed since last we were here for this element 374 | //and depending on its contents the w/h may change 375 | 376 | setmeta(parentelement, getmeta(parentelement).original, getcurrentmeta(parentelement), getcurrentmeta(parentelement), {x:0,y:0,w:0,h:0}) 377 | var currentmeta = getmeta(parentelement); 378 | 379 | //move the element 380 | parentelement.style.top = Math.round(currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 381 | parentelement.style.left = Math.round(currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 382 | 383 | parentelement.style.width = Math.round(currentmeta.current.w).toString() + 'px'; 384 | parentelement.style.height = Math.round(currentmeta.current.h).toString() + 'px'; 385 | 386 | parentelement.style.position = 'absolute'; 387 | 388 | setstate(parentelement, getstate(parentelement).amended, true, getstate(parentelement).absolute,); //set active to true 389 | 390 | document.body.append(parentelement); 391 | 392 | //tell the mutation observer for this element to start observing. 393 | 394 | //parentelement. 395 | 396 | //check if we are actually resizing 397 | 398 | if (element != parentelement) { 399 | resizing = true; 400 | } 401 | 402 | if (!resizing) { 403 | document.body.style.cursor = "move"; 404 | }; 405 | 406 | //store the current element 407 | currentelement = element; 408 | 409 | window.addEventListener("mousemove", mouseMoveListener, false); 410 | window.addEventListener("mouseup", mouseUpListener, false); 411 | 412 | //store the current mouse position 413 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: 0, deltaY: 0 }); 414 | 415 | //adjust the element target to be same as location (it should be anyway) 416 | currentmeta = getmeta((resizing) ? parentelement : element); 417 | setmeta((resizing) ? parentelement : element, getmeta((resizing) ? parentelement : element).original, currentmeta.current, currentmeta.current, currentmeta.step); 418 | 419 | timer = setInterval(onTimerTick, 1000 / interval); 420 | timers[timer]=timer; 421 | 422 | //code below prevents the mouse down from having an effect on the main browser window: 423 | if (event.preventDefault) { 424 | event.preventDefault(); 425 | } //standard 426 | else if (event.returnValue) { 427 | event.returnValue = false; 428 | } //older IE 429 | return false; 430 | } 431 | } 432 | 433 | function mouseMoveListener(event) { 434 | 435 | //work out the delta of mouse 436 | //new mouse becomes target 437 | //after clamping to the canvas 438 | 439 | //because we are moving we stick with the current element and dont try to determine who we are moving over 440 | //otherwise the mouseover finds another valid element 441 | 442 | var element = currentelement; 443 | 444 | //as the element has been moved, we assume it has been amended 445 | //resizing works differently to dragging so we have to split the parts of the code - really ! 446 | 447 | if (resizing) { 448 | var currentmeta = getmeta(element.parentElement); 449 | setstate(element, true, getstate(element.parentElement).active, getstate(element.parentElement).absolute); //set amended true 450 | } 451 | else { 452 | var currentmeta = getmeta(element); 453 | setstate(element, true, getstate(element).active, getstate(element).absolute); //set amended true 454 | } 455 | 456 | var checkmeta = { target: currentmeta.target }; 457 | 458 | //get new mouse position 459 | var mouse = getmouseposition(event); 460 | var mouseX = mouse.mouseX; 461 | var mouseY = mouse.mouseY; 462 | 463 | //determine if new target is in bounds 464 | //test is based on the existing target being adjusted by the delta NOT the current location of the element as that is being ticked 465 | //we need to take into account that resizing adjusts x or y and h or w as deltaX an deltaY change by creating a temporary new target before testing it 466 | //delta(x,y) applied to oldtarget (x,y) must be between min(x,y) and max(x,y) 467 | //otherwise clamp the delta to a value to adhere to the above rule 468 | 469 | //mouse delta, try this first 470 | var deltaX = mouseX - getmousemeta(element).mousemeta.x; 471 | var deltaY = mouseY - getmousemeta(element).mousemeta.y; 472 | 473 | if (resizing) { 474 | 475 | //calculate the new element size and centre 476 | checkmeta.target = getresizedelement(element, deltaX, deltaY, true); 477 | 478 | } 479 | 480 | //calculate the bounds based on the new target size 481 | var minX = (checkmeta.target.w / 2); 482 | var maxX = (((theCanvas.clientWidth == 0) ? window.innerWidth : theCanvas.clientWidth) - (checkmeta.target.w / 2)); 483 | var minY = (checkmeta.target.h / 2); 484 | var maxY = (((theCanvas.clientHeight == 0) ? window.innerHeight : theCanvas.clientHeight) - (checkmeta.target.h / 2)); 485 | 486 | //check the centre fits within the bounds 487 | 488 | if (resizing) { 489 | //checkmax returns a -value if out of bounds 490 | //checkmin returns a +value if out of bounds 491 | var checkmaxX = (maxX - checkmeta.target.x); 492 | var checkmaxY = (maxY - checkmeta.target.y); 493 | var checkminX = (minX - checkmeta.target.x); 494 | var checkminY = (minY - checkmeta.target.y); 495 | } 496 | else { 497 | 498 | var checkmaxX = Math.round(maxX - (checkmeta.target.x + deltaX)); 499 | var checkmaxY = Math.round(maxY - (checkmeta.target.y + deltaY)); 500 | var checkminX = Math.round(minX - (checkmeta.target.x + deltaX)); 501 | var checkminY = Math.round(minY - (checkmeta.target.y + deltaY)); 502 | } 503 | 504 | //adjust the new mouse position to take into account any out of bounds amounts 505 | //if any neg max values or pos min values 506 | mouseX = mouseX + ((checkminX > 0) ? checkminX : 0) + ((checkmaxX < 0) ? checkmaxX : 0); 507 | mouseY = mouseY + ((checkminY > 0) ? checkminY : 0) + ((checkmaxY < 0) ? checkmaxY : 0); 508 | 509 | //recalculate the mouse delta based on the revised mouse position 510 | var deltaX = mouseX - getmousemeta(element).mousemeta.x; 511 | var deltaY = mouseY - getmousemeta(element).mousemeta.y; 512 | 513 | //store the new mouse location 514 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: deltaX, deltaY: deltaY }); 515 | 516 | if (resizing) 517 | //store the new target 518 | { 519 | currentmeta.target = getresizedelement(element, deltaX, deltaY); 520 | 521 | if (usegrid) { 522 | //console.log("B", currentmeta.target.x, currentmeta.target.y); 523 | currentmeta.target.x = Math.round(currentmeta.target.x / grid) * grid; 524 | currentmeta.target.y = Math.round(currentmeta.target.y / grid) * grid; 525 | currentmeta.target.w = Math.round(currentmeta.target.w / grid) * grid; 526 | currentmeta.target.h = Math.round(currentmeta.target.h / grid) * grid; 527 | 528 | console.log("X", deltaX, grid, Math.abs(deltaX) < grid / 2); 529 | 530 | if (deltaX != 0 && Math.abs(deltaX) < grid / 2) { 531 | 532 | console.log(mouseX, stallmeta.mousedown.x, Math.abs(mouseX - stallmeta.mousedown.x) > grid / 2); 533 | 534 | if (Math.abs(mouseX - stallmeta.mousedown.x) > grid / 2) { 535 | var tgrid = grid * ((mouseX > stallmeta.mousedown.x) ? 1 : -1.05); 536 | console.log("tgrid", tgrid, currentmeta.target.x); 537 | currentmeta.target.x = Math.round((currentmeta.target.x + (tgrid / 2)) / grid) * grid; 538 | console.log("adjusted", currentmeta.target.x, currentmeta.current.x); 539 | stallmeta.mousedown.x = mouseX; 540 | } 541 | 542 | } 543 | console.log("Y", deltaY, grid, Math.abs(deltaY) < grid / 2); 544 | if (deltaY != 0 && Math.abs(deltaY) < grid / 2) { 545 | 546 | if (Math.abs(mouseY - stallmeta.mousedown.y) > grid / 2) { 547 | var tgrid = grid * ((mouseY > stallmeta.mousedown.y) ? 1 : -1.05); 548 | currentmeta.target.y = Math.round((currentmeta.target.y + (tgrid / 2)) / grid) * grid; 549 | stallmeta.mousedown.y = mouseY; 550 | } 551 | 552 | } 553 | } 554 | setmeta(element.parentElement, getmeta(element.parentElement).original, currentmeta.current, currentmeta.target, currentmeta.step); 555 | setgrid(element.parentElement.id, getmeta(element.parentElement).current); 556 | } 557 | else { 558 | 559 | //the target is the current target + the calculated delta, 560 | //as the currentX represents some position between originalx and the target, we add the delta to the target 561 | //use grid calculation to adjust the target to snap to the grid 562 | 563 | //calculate the real target 564 | currentmeta.target.x = Math.round(currentmeta.target.x + deltaX); 565 | currentmeta.target.y = Math.round(currentmeta.target.y + deltaY); 566 | 567 | //adjust to snap if active 568 | 569 | if (usegrid) { 570 | //console.log("B", currentmeta.target.x, currentmeta.target.y); 571 | currentmeta.target.x = Math.round(currentmeta.target.x / grid) * grid; 572 | currentmeta.target.y = Math.round(currentmeta.target.y / grid) * grid; 573 | //console.log("A", currentmeta.target.x, currentmeta.target.y); 574 | //check for stalled movement 575 | //we moved but less than the amount needed to move to the next grid position 576 | //so we check the absolute movement since mousedown and if it is enough to move to the next grid adjust the target, either + or - depending on current travel direction 577 | //and then readjust the mousedown to the current mouse location so that we restart the process 578 | 579 | console.log("X", deltaX, grid, Math.abs(deltaX) < grid / 2); 580 | 581 | if (deltaX != 0 && Math.abs(deltaX) < grid / 2) { 582 | 583 | console.log(mouseX, stallmeta.mousedown.x, Math.abs(mouseX - stallmeta.mousedown.x) > grid / 2); 584 | 585 | if (Math.abs(mouseX - stallmeta.mousedown.x) > grid / 2) { 586 | var tgrid = grid * ((mouseX > stallmeta.mousedown.x) ? 1 : -1.05); 587 | console.log("tgrid", tgrid, currentmeta.target.x ); 588 | currentmeta.target.x = Math.round((currentmeta.target.x + (tgrid / 2)) / grid) * grid; 589 | console.log("adjusted", currentmeta.target.x, currentmeta.current.x); 590 | stallmeta.mousedown.x = mouseX; 591 | } 592 | 593 | } 594 | console.log("Y",deltaY, grid, Math.abs(deltaY) < grid / 2); 595 | if (deltaY != 0 && Math.abs(deltaY) < grid / 2) { 596 | 597 | if (Math.abs(mouseY - stallmeta.mousedown.y) > grid / 2) { 598 | var tgrid = grid * ((mouseY > stallmeta.mousedown.y) ? 1 : -1.05); 599 | currentmeta.target.y = Math.round((currentmeta.target.y + (tgrid / 2)) / grid) * grid; 600 | stallmeta.mousedown.y = mouseY; 601 | } 602 | 603 | } 604 | 605 | } 606 | 607 | //store the new target 608 | setmeta(element, getmeta(element).original, currentmeta.current, currentmeta.target, currentmeta.step); 609 | setgrid(element.id, getmeta(element).current); 610 | } 611 | 612 | 613 | } 614 | 615 | function getresizedelement(element, deltaX, deltaY,roundvalues=false) { 616 | 617 | var currentmeta = getmeta(element.parentElement); 618 | var tempmeta = currentmeta.target; 619 | 620 | if (element.classList.contains('bottom-right')) { 621 | const width = currentmeta.target.w + deltaX; 622 | const height = currentmeta.target.h + deltaY; 623 | if (width > minimum_size) { 624 | tempmeta.w = width; 625 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 626 | 627 | } 628 | if (height > minimum_size) { 629 | tempmeta.h = height; 630 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 631 | } 632 | } 633 | 634 | else if (element.classList.contains('bottom-left')) { 635 | const height = currentmeta.target.h + deltaY; 636 | const width = currentmeta.target.w - deltaX; 637 | if (height > minimum_size) { 638 | tempmeta.h = height; 639 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 640 | } 641 | if (width > minimum_size) { 642 | tempmeta.w = width; 643 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 644 | } 645 | } 646 | 647 | else if (element.classList.contains('top-right')) { 648 | const width = currentmeta.target.w + deltaX; 649 | const height = currentmeta.target.h - deltaY; 650 | if (width > minimum_size) { 651 | tempmeta.w = width; 652 | tempmeta.x = currentmeta.target.x + (deltaX / 2); 653 | } 654 | if (height > minimum_size) { 655 | tempmeta.h = height; 656 | tempmeta.y = currentmeta.target.y + (deltaY / 2); 657 | 658 | } 659 | } 660 | 661 | else {//top-left 662 | const width = currentmeta.target.w - deltaX; 663 | const height = currentmeta.target.h - deltaY; 664 | if (width > minimum_size) { 665 | tempmeta.w = width; 666 | tempmeta.x = currentmeta.target.x + (deltaX/2); 667 | } 668 | if (height > minimum_size) { 669 | tempmeta.h = height; 670 | tempmeta.y = currentmeta.target.y + (deltaY/2); 671 | } 672 | } 673 | 674 | if (roundvalues) { 675 | tempmeta.w = Math.round(tempmeta.w); 676 | tempmeta.h = Math.round(tempmeta.h); 677 | tempmeta.x = Math.round(tempmeta.x); 678 | tempmeta.y = Math.round(tempmeta.y); 679 | } 680 | 681 | return tempmeta; 682 | 683 | } 684 | 685 | function onTimerTick() { 686 | 687 | //get the correct element to action 688 | 689 | var actionelement = (resizing) ? currentelement.parentElement : currentelement; 690 | 691 | var currentmeta = getmeta(actionelement); 692 | 693 | //calculate the step 694 | currentmeta.step.x = easeAmount * (currentmeta.target.x - currentmeta.current.x); 695 | currentmeta.step.y = easeAmount * (currentmeta.target.y - currentmeta.current.y); 696 | currentmeta.step.w = easeAmount * (currentmeta.target.w - currentmeta.current.w); 697 | currentmeta.step.h = easeAmount * (currentmeta.target.h - currentmeta.current.h); 698 | 699 | //adjust the current location 700 | currentmeta.current.x = currentmeta.current.x + currentmeta.step.x; 701 | currentmeta.current.y = currentmeta.current.y + currentmeta.step.y; 702 | currentmeta.current.w = currentmeta.current.w + currentmeta.step.w; 703 | currentmeta.current.h = currentmeta.current.h + currentmeta.step.h; 704 | 705 | //stop the timer when the target position is reached (close enough) 706 | if ( 707 | (!dragging) && 708 | (Math.abs(currentmeta.current.x - currentmeta.target.x) < 0.1) && 709 | (Math.abs(currentmeta.current.y - currentmeta.target.y) < 0.1) 710 | && 711 | (Math.abs(currentmeta.current.w - currentmeta.target.w) < 0.1) && 712 | (Math.abs(currentmeta.current.h - currentmeta.target.h) < 0.1) 713 | ) 714 | { 715 | currentmeta.current.x = currentmeta.target.x; 716 | currentmeta.current.y = currentmeta.target.y; 717 | currentmeta.current.w = currentmeta.target.w; 718 | currentmeta.current.h = currentmeta.target.h; 719 | 720 | //stop timer: 721 | 722 | delete timers[timer]; 723 | 724 | clearInterval(timer); 725 | } 726 | 727 | //save the new location 728 | setmeta(actionelement, getmeta(actionelement).original,currentmeta.current, currentmeta.target, currentmeta.step) 729 | 730 | //move the element 731 | actionelement.style.top = Math.round(currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 732 | actionelement.style.left = Math.round(currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 733 | 734 | actionelement.style.width = Math.round(currentmeta.current.w).toString() + 'px'; 735 | actionelement.style.height = Math.round(currentmeta.current.h).toString() + 'px'; 736 | 737 | } 738 | 739 | function mouseUpListener(event) { 740 | 741 | //because we were moving we stick with the current element and dont try to determine who we are moving over 742 | //otherwise the mouseover finds another valid dragme and attachs the mouse down to the wrong element 743 | var element = currentelement; //getelement(event.target); 744 | 745 | element.addEventListener("mousedown", mouseDownListener, false); 746 | window.removeEventListener("mouseup", mouseUpListener, false); 747 | if (dragging) { 748 | dragging = false; 749 | if (resizing) { 750 | resizing = false; 751 | currentelement = currentelement.parentElement; // as we loose the resizer indicator, we need to let tick tock know which element to actually ease out 752 | } 753 | document.body.style.cursor = "auto" 754 | window.removeEventListener("mousemove", mouseMoveListener, false); 755 | } 756 | } 757 | 758 | 759 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /* Magic Mirror Config Sample 2 | * 3 | * By Michael Teeuw http://michaelteeuw.nl 4 | * MIT Licensed. 5 | * 6 | * For more information how you can configurate this file 7 | * See https://github.com/MichMich/MagicMirror#configuration 8 | * 9 | */ 10 | 11 | var config = { 12 | address: "localhost", // Address to listen on, can be: 13 | // - "localhost", "127.0.0.1", "::1" to listen on loopback interface 14 | // - another specific IPv4/6 to listen on a specific interface 15 | // - "", "0.0.0.0", "::" to listen on any interface 16 | // Default, when address config is left out, is "localhost" 17 | port: 8080, 18 | ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses 19 | // or add a specific IPv4 of 192.168.1.5 : 20 | // ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"], 21 | // or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format : 22 | // ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"], 23 | 24 | language: "en", 25 | timeFormat: 24, 26 | units: "metric", 27 | // serverOnly: true/false/"local" , 28 | // local for armv6l processors, default 29 | // starts serveronly and then starts chrome browser 30 | // false, default for all NON-armv6l devices 31 | // true, force serveronly mode, because you want to.. no UI on this device 32 | 33 | // module position can be any of the following: 34 | 35 | //top_bar, top_left, top_center, top_right, 36 | //upper_third, middle_center, lower_third, 37 | //bottom_left, bottom_center, bottom_right, bottom_bar, 38 | //fullscreen_above, and fullscreen_below 39 | 40 | modules: [ 41 | //if using MMM-carousel, add/update the ignoreModules line to include module position 42 | //{ 43 | // module: 'MMM-Carousel', 44 | // config: { 45 | // ignoreModules:['MMM-ModulePosition'], 46 | // } 47 | //}, 48 | 49 | { 50 | module: "alert", 51 | }, 52 | { 53 | module: "updatenotification", 54 | position: "top_bar", 55 | }, 56 | { 57 | module: "clock", 58 | position: "top_left", 59 | config: { 60 | displayType: "digital", 61 | 62 | } 63 | }, 64 | { 65 | module: "clock", 66 | position: "top_right", 67 | config: { 68 | displayType: "analog", 69 | } 70 | }, 71 | { 72 | module: "compliments", 73 | position: "lower_third", 74 | }, 75 | { 76 | module: "weather", 77 | position: "top_right", 78 | config: { 79 | weatherProvider: "openmeteo", 80 | type: "current", 81 | lat: 40.776676, 82 | lon: -73.971321 83 | } 84 | }, 85 | { 86 | module: "weather", 87 | position: "top_right", 88 | header: "Weather Forecast", 89 | config: { 90 | weatherProvider: "openmeteo", 91 | type: "forecast", 92 | lat: 40.776676, 93 | lon: -73.971321 94 | } 95 | }, 96 | { 97 | module: "newsfeed", 98 | position: "bottom_bar", 99 | config: { 100 | feeds: [ 101 | { 102 | title: "BBC UK", 103 | url: "https://feeds.bbci.co.uk/news/uk/rss.xml", 104 | }, 105 | 106 | { 107 | title: "sky news", 108 | url: "https://feeds.skynews.com/feeds/rss/home.xml", 109 | }, 110 | ], 111 | showSourceTitle: true, 112 | showPublishDate: true, 113 | showDescription: true, 114 | broadcastNewsFeeds: false, 115 | broadcastNewsUpdates: false 116 | } 117 | }, 118 | { 119 | module: "MMM-ModulePosition", 120 | position: "fullscreen_below", 121 | }, 122 | ] 123 | 124 | }; 125 | 126 | /*************** DO NOT EDIT THE LINE BELOW ***************/ 127 | if (typeof module !== "undefined") { module.exports = config; } 128 | 129 | -------------------------------------------------------------------------------- /configscripts.js: -------------------------------------------------------------------------------- 1 | // JavaScript source code 2 | 3 | //this must be declared first to ensure that the setconfig is run before the 4 | //code in script1 tries to use the variables 5 | 6 | //will need to get the instance divs from the array of all modules + their identity 7 | 8 | var modules = []; 9 | var modulepositions = []; 10 | var configpositions = []; 11 | 12 | function setconfig(config) { 13 | 14 | //find all the modules in the config 15 | 16 | var configmodules = config.modules; 17 | 18 | configmodules.forEach(module => 19 | modules.push(module.module) 20 | ); 21 | configmodules.forEach(module => 22 | configpositions.push(module.position) 23 | ); 24 | configmodules.forEach(module => 25 | modulepositions[module.module] = {} 26 | ); 27 | 28 | } 29 | 30 | //button handler 31 | 32 | function saveFunction() { 33 | 34 | console.log("saving config"); 35 | 36 | //we want to save a revised config with the new modpos values 37 | //as opposed to using CSS ?? 38 | //it is more flexible (if modpos exists, use them to setup the position of the class/module name) 39 | //though CSS might be a good way to do it initially 40 | //so 41 | //can we read the custom.css to merge the new details as a module css entry 42 | //or overwrite an existing one 43 | 44 | //here we will need to write the file out useing the nodehelper 45 | // custom.css.timestamp 46 | // config.js.timestamp 47 | 48 | } 49 | 50 | //config handler 51 | 52 | // need to get access to the full config so we load it as a script which might break something 53 | 54 | setconfig(config); 55 | -------------------------------------------------------------------------------- /css/example.custom.css.1738597574256: -------------------------------------------------------------------------------- 1 | .MMM-FlipClock { 2 | left:0px; 3 | top:443px; 4 | width:2432px; 5 | height:182px; 6 | position:absolute !important; /*included to overide the transition static positioning that is added inline */ 7 | } 8 | .calendar { 9 | left:742px; 10 | top:-166px; 11 | width:347.90625px; 12 | height:343px; 13 | position:absolute !important; /*included to overide the transition static positioning that is added inline */ 14 | } 15 | -------------------------------------------------------------------------------- /images/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/after.png -------------------------------------------------------------------------------- /images/before.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/before.gif -------------------------------------------------------------------------------- /images/screenshot_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/screenshot_edit.png -------------------------------------------------------------------------------- /images/screenshot_edit2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/screenshot_edit2.png -------------------------------------------------------------------------------- /images/screenshot_edit3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/screenshot_edit3.png -------------------------------------------------------------------------------- /images/screenshot_read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/screenshot_read.png -------------------------------------------------------------------------------- /images/screenshot_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBodger/MMM-ModulePosition/2c8a6701ffbffa8074699cb83c44b99e209d04f6/images/screenshot_save.png -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | // This logger is very simple, but needs to be extended. 2 | (function (root, factory) { 3 | if (typeof exports === "object") { 4 | if (process.env.JEST_WORKER_ID === undefined) { 5 | const colors = require("ansis"); 6 | 7 | // add timestamps in front of log messages 8 | require("console-stamp")(console, { 9 | format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :msg", 10 | tokens: { 11 | label: (arg) => { 12 | const { method, defaultTokens } = arg; 13 | let label = defaultTokens.label(arg); 14 | if (method === "error") { 15 | label = colors.red(label); 16 | } else if (method === "warn") { 17 | label = colors.yellow(label); 18 | } else if (method === "debug") { 19 | label = colors.bgBlue(label); 20 | } else if (method === "info") { 21 | label = colors.blue(label); 22 | } 23 | return label; 24 | }, 25 | msg: (arg) => { 26 | const { method, defaultTokens } = arg; 27 | let msg = defaultTokens.msg(arg); 28 | if (method === "error") { 29 | msg = colors.red(msg); 30 | } else if (method === "warn") { 31 | msg = colors.yellow(msg); 32 | } else if (method === "info") { 33 | msg = colors.blue(msg); 34 | } 35 | return msg; 36 | } 37 | } 38 | }); 39 | } 40 | // Node, CommonJS-like 41 | module.exports = factory(root.config); 42 | } else { 43 | // Browser globals (root is window) 44 | root.Log = factory(root.config); 45 | } 46 | }(this, function (config) { 47 | let logLevel; 48 | let enableLog; 49 | if (typeof exports === "object") { 50 | // in nodejs and not running with jest 51 | enableLog = process.env.JEST_WORKER_ID === undefined; 52 | } else { 53 | // in browser and not running with jsdom 54 | enableLog = typeof window === "object" && window.name !== "jsdom"; 55 | } 56 | 57 | if (enableLog) { 58 | logLevel = { 59 | debug: Function.prototype.bind.call(console.debug, console), 60 | log: Function.prototype.bind.call(console.log, console), 61 | info: Function.prototype.bind.call(console.info, console), 62 | warn: Function.prototype.bind.call(console.warn, console), 63 | error: Function.prototype.bind.call(console.error, console), 64 | group: Function.prototype.bind.call(console.group, console), 65 | groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console), 66 | groupEnd: Function.prototype.bind.call(console.groupEnd, console), 67 | time: Function.prototype.bind.call(console.time, console), 68 | timeEnd: Function.prototype.bind.call(console.timeEnd, console), 69 | timeStamp: Function.prototype.bind.call(console.timeStamp, console) 70 | }; 71 | 72 | logLevel.setLogLevel = function (newLevel) { 73 | if (newLevel) { 74 | Object.keys(logLevel).forEach(function (key) { 75 | if (!newLevel.includes(key.toLocaleUpperCase())) { 76 | logLevel[key] = function () {}; 77 | } 78 | }); 79 | } 80 | }; 81 | } else { 82 | logLevel = { 83 | debug () {}, 84 | log () {}, 85 | info () {}, 86 | warn () {}, 87 | error () {}, 88 | group () {}, 89 | groupCollapsed () {}, 90 | groupEnd () {}, 91 | time () {}, 92 | timeEnd () {}, 93 | timeStamp () {} 94 | }; 95 | 96 | logLevel.setLogLevel = function () {}; 97 | } 98 | 99 | return logLevel; 100 | })); 101 | -------------------------------------------------------------------------------- /modPos.njsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14.0 4 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 5 | modPos 6 | modPos 7 | SAK 8 | SAK 9 | SAK 10 | SAK 11 | 12 | 13 | 14 | Debug 15 | 2.0 16 | e8ca2d15-194c-4b29-9f0c-0bddf1d606fc 17 | . 18 | server.js 19 | 20 | 21 | . 22 | . 23 | v4.0 24 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 25 | 1337 26 | true 27 | 28 | 29 | true 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | False 45 | True 46 | 0 47 | / 48 | http://localhost:48022/ 49 | False 50 | True 51 | http://localhost:1337 52 | False 53 | 54 | 55 | 56 | 57 | 58 | 59 | CurrentPage 60 | True 61 | False 62 | False 63 | False 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | False 73 | False 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /modPos.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "modPos", "modPos.njsproj", "{E8CA2D15-194C-4B29-9F0C-0BDDF1D606FC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6B40E0C-DCBF-45F7-AEE8-B8B05D094B28}" 9 | ProjectSection(SolutionItems) = preProject 10 | config.js = config.js 11 | HTMLPage1.html = HTMLPage1.html 12 | HTMLPage2.html = HTMLPage2.html 13 | HTMLPage3.html = HTMLPage3.html 14 | interact.js = interact.js 15 | Script1.js = Script1.js 16 | Script2.js = Script2.js 17 | StyleSheet1.css = StyleSheet1.css 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {E8CA2D15-194C-4B29-9F0C-0BDDF1D606FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {E8CA2D15-194C-4B29-9F0C-0BDDF1D606FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {E8CA2D15-194C-4B29-9F0C-0BDDF1D606FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {E8CA2D15-194C-4B29-9F0C-0BDDF1D606FC}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {2FE71FAB-075E-43AC-BEBC-5695976D7D1D} 36 | EndGlobalSection 37 | GlobalSection(TeamFoundationVersionControl) = preSolution 38 | SccNumberOfProjects = 2 39 | SccEnterpriseProvider = {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C} 40 | SccTeamFoundationServer = https://vincentscott.visualstudio.com/ 41 | SccLocalPath0 = . 42 | SccProjectUniqueName1 = modPos.njsproj 43 | SccLocalPath1 = . 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | /* global Module, MMM-ModulePosition */ 2 | 3 | /* Magic Mirror 4 | * Module: node_helper 5 | * 6 | * By Neil Scott 7 | * MIT Licensed. 8 | */ 9 | 10 | var NodeHelper = require("node_helper"); 11 | 12 | var moment = require("moment"); 13 | //fs = require('fs'); 14 | const fs = require('node:fs/promises'); 15 | 16 | const Log = require("logger"); 17 | 18 | //pseudo structures for commonality across all modules 19 | //obtained from a helper file of modules 20 | 21 | var LOG = require('../MMM-FeedUtilities/LOG'); 22 | var RSS = require('../MMM-FeedUtilities/RSS'); 23 | 24 | //// get required structures and utilities 25 | 26 | //const structures = require("../MMM-ChartUtilities/structures"); 27 | //const utilities = require("../MMM-ChartUtilities/common"); 28 | 29 | //const JSONutils = new utilities.JSONutils(); 30 | //const configutils = new utilities.configutils(); 31 | 32 | module.exports = NodeHelper.create({ 33 | 34 | start: function () { 35 | 36 | this.debug = true; 37 | 38 | console.log(this.name + ' is started!'); 39 | Log.info(`MMM-ModulePosition node_helper.js started`); 40 | 41 | this.consumerstorage = {}; // contains the config and feedstorage 42 | 43 | this.currentmoduleinstance = ''; 44 | this.logger = {}; 45 | 46 | }, 47 | 48 | setconfig: function (aconfig) { 49 | 50 | var moduleinstance = aconfig.moduleinstance; 51 | var config = aconfig.config; 52 | 53 | //store a local copy so we dont have keep moving it about 54 | 55 | this.consumerstorage[moduleinstance] = { config: config, feedstorage: {} }; 56 | 57 | //additional work to simplify the config for use in the module 58 | }, 59 | 60 | showstatus: function (moduleinstance) { 61 | //console.log("MMM Module: " + moduleinstance); 62 | console.log('============================ start of status ========================================'); 63 | 64 | console.log('config for consumer: ' + moduleinstance); 65 | 66 | console.log(this.consumerstorage[moduleinstance].config); 67 | 68 | console.log('============================= end of status ========================================='); 69 | 70 | }, 71 | 72 | showElapsed: function () { 73 | endTime = new Date(); 74 | var timeDiff = endTime - startTime; //in ms 75 | // strip the ms 76 | timeDiff /= 1000; 77 | 78 | // get seconds 79 | var seconds = Math.round(timeDiff); 80 | return (" " + seconds + " seconds"); 81 | }, 82 | 83 | stop: function () { 84 | console.log("Shutting down node_helper"); 85 | }, 86 | 87 | socketNotificationReceived: function (notification, payload) { 88 | //console.log(this.name + " NODE_HELPER received a socket notification: " + notification + " - Payload: " + payload); 89 | 90 | //we will receive a payload with the moduleinstance of the consumerid in it so we can store data and respond to the correct instance of 91 | //the caller - i think that this may be possible!! 92 | 93 | if (this.logger[payload.moduleinstance] == null) { 94 | 95 | this.logger[payload.moduleinstance] = LOG.createLogger("logfile_" + payload.moduleinstance + ".log", payload.moduleinstance); 96 | 97 | }; 98 | 99 | this.currentmoduleinstance = payload.moduleinstance; 100 | 101 | switch (notification) { 102 | case "CONFIG": this.setconfig(payload); break; 103 | case "RESET": this.reset(payload); break; 104 | case "WRITE_THIS": this.writethis(payload); break; 105 | case "STATUS": this.showstatus(payload); break; 106 | } 107 | }, 108 | 109 | writethis: function (payload) { 110 | 111 | //1) create a custom_css set of entries at the module and actual names levels if a duplicate 112 | //using IDs not classes 113 | //ignore all ignorable modules and any not active or amended 114 | 115 | var modules = payload.payload; 116 | var css = ''; 117 | var modCount = 0; 118 | 119 | for (var module in modules) { 120 | 121 | var thismod = modules[module]; 122 | 123 | //may need to have them next to each other not a space, a space means any ? 124 | 125 | if (!thismod.ignore && thismod.state.active && thismod.state.amended) { 126 | 127 | css = css + '.' + thismod.name + 128 | ((thismod.duplicate) ? '#' + module : '') + 129 | ' {' + 130 | '\n\tleft:' + thismod.modpos.x + "px;" + 131 | '\n\ttop:' + thismod.modpos.y + "px;" + 132 | '\n\twidth:' + thismod.modpos.w + "px;" + 133 | '\n\theight:' + thismod.modpos.h + "px;" + 134 | '\n\tposition:' + 'absolute' + " !important; /*included to overide the transition static positioning that is added inline */" + 135 | '\n}' + "\r\n" 136 | modCount = modCount + 1; 137 | 138 | } 139 | } 140 | 141 | // dont check if there are no modpos ! just write it 142 | 143 | //create a directory to store these - not under modpos or add the directory as a gitignore 144 | 145 | //alert("BANG"); 146 | 147 | var cssfilename = 'modules/MMM-ModulePosition/css/custom.css.' + new Date().getTime(); //simplest format though smelly 148 | //Log.info(`about to call module writeCSSfile`); 149 | 150 | this.writeCSSfile(cssfilename, css, modCount) 151 | 152 | //fs.writeFile(cssfilename, css, 'utf8', (err) => { 153 | // if(err) console.error(err); 154 | // console.log('The file has been saved!'); 155 | // if (this.consumerstorage[this.currentmoduleinstance].config.showAlerts) this.sendNotificationToMasterModule("ALERT", " FILE:" + cssfilename + " saved with " + modCount + " module" + ((modCount>1) ? 's' : '') +" positioned."); 156 | //}); 157 | 158 | }, 159 | 160 | sendNotificationToMasterModule: function (stuff, stuff2) { 161 | this.sendSocketNotification(stuff, stuff2); 162 | }, 163 | 164 | writeCSSfile: function (filename, filecontent, modcount) { 165 | //Log.info(`In module writeCSSfile`); 166 | try { 167 | fs.writeFile(filename, filecontent); 168 | setTimeout(() => {}, 1000); 169 | Log.info(`The file has been saved: ${filename}`); 170 | if (this.consumerstorage[this.currentmoduleinstance].config.showAlerts) this.sendNotificationToMasterModule("ALERT", " NEW CSS FILE:" + filename + " saved with " + modcount + " module" + ((modcount > 1) ? 's' : '') + " positioned."); 171 | } catch (err) { 172 | Log.error(err); 173 | } 174 | }, 175 | 176 | }); 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MMM-ModulePosition", 3 | "version": "1.0.0", 4 | "description": "Magic Mirror Module for positioning modules in an absolute manner", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/TheBodger/MMM-ModulePosition" 8 | }, 9 | "author": { 10 | "name": "N Scott" 11 | }, 12 | "keywords": [ 13 | "magic mirror", 14 | "smart mirror", 15 | "Module Positioning", 16 | "modules" 17 | ], 18 | "dependencies": {}, 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/TheBodger/MMM-ModulePosition/issues" 22 | }, 23 | "homepage": "https://github.com/TheBodger/MMM-ModulePosition#readme" 24 | } 25 | -------------------------------------------------------------------------------- /smoothpositioning.3.5.js: -------------------------------------------------------------------------------- 1 | 2 | //create 3.3. 3 | //calculate the offset of the current element/module from its parent(or we need to find a parent positioned element) 4 | //use this offset to amend the final location so that we adjust back from being relative to body to being relative to its real parent 5 | 6 | //create 3.4 7 | //add snap to grid and toggle switches to enable grid snapping 8 | //uses grid to control size of grid - 1 should ensure current activity 9 | //applied when calculating the new target. uses code from fabricjs 10 | 11 | // create 3.5 12 | //target location is calculated absolute 13 | //new target = (adjusted) mouse = mousedown delta 14 | //mousedown delta = mousedown - current 15 | //rename stallmeta to mousedown 16 | //adjust delta to grid before resizing 17 | //move grid snapping to edges - leading edge(s) 18 | //stop the grid allowing elements to go out of bounds for certain values - mostly 19 | 20 | //todo - amend grid size in meta screen display 21 | //todo - drag the meta display 22 | //todo - 23 | 24 | // JavaScript source code 25 | 26 | //each element carries 3 sets of information: 27 | //1) where am i (x,y,w,h) (current/also obtainable from the element itself) 28 | //2) where am i going (x,y,w,h) (target) 29 | //3) how do i get there (xstep,ystep,wstep,hstep) the value towards target controlled by the easing % 30 | 31 | // 0 - all locations held in the element relate to its centre, so need to be adjusted for showing 32 | // 1 - initialise all elements with current,target and steps 33 | // 2 - add the resizing stuff to the element, and make it absolute 34 | // 3 - listen for mousedown on an element and reset the details for the element(or parent) 35 | // 4 - listen for mousedown on an elements resizer (how do we know ? ask the parent) ditto 36 | // 5 - start timer, tell the world we are dragging, do some javascript stuff and set window level event listeners for move and up 37 | // 6 - and may be add a mouse out for the canvas to capture when some one goes awol 38 | // 7 - on each mouse move, update the target, recalc the steps from the current using easing % 39 | // 8 - the timer simply draws where we are based on current + step and adjusts current 40 | // 9 - on mouseup we stop dragging and let the timer take the element to its target, one step at a time 41 | 42 | //default local variables 43 | 44 | var easeAmount = 0.30 // percentage of delta to step 45 | var FPS = 15 // frames per second 46 | var interval = 1000 / FPS //how long each frame lasts for 47 | var minimum_size = 50; 48 | var grid = 10; // needs to be low otherwise dragging can stall 49 | 50 | //---------------- 51 | 52 | var usegrid = false; 53 | var currentelement; 54 | var timers = {}; 55 | var dragging = false; 56 | var resizing = false; 57 | var mousedown = { element: { x: 0, y: 0, w: 0, h: 0 }, mouse: { x: 0, y: 0 }, mousemoved: { x: 0, y: 0 }, snappedx: false, snappedy: false} 58 | 59 | function smoothpositioninginit(smoothpositioningconfig) { 60 | 61 | //add verification that the config has been set 62 | //TODO - support null canvasid = viewable window 63 | 64 | if (smoothpositioningconfig.canvasid.toLowerCase() == 'body') { 65 | theCanvas = document.body; 66 | } 67 | else { 68 | theCanvas = document.getElementById(smoothpositioningconfig.canvasid); 69 | } 70 | 71 | //set the local variables 72 | 73 | easeAmount = smoothpositioningconfig.easeAmount; 74 | FPS = smoothpositioningconfig.FPS; 75 | interval = 1000 / FPS; 76 | minimum_size = smoothpositioningconfig.minimum_size; 77 | grid = smoothpositioningconfig.grid; 78 | 79 | } 80 | 81 | function setmeta(element, original, current, target, step) { 82 | element.dataset.meta = JSON.stringify({ original:original, current: current, target: target, step: step }); 83 | } 84 | 85 | function setstate(element, amended, active, absolute) { 86 | element.dataset.state = JSON.stringify({ amended: amended, active: active, absolute: absolute}); 87 | } 88 | 89 | function setcss(element, offsetX, offsetY) { 90 | element.dataset.cssoffset = JSON.stringify({ offsetX: offsetX, offsetY: offsetY }); 91 | } 92 | 93 | function getcss(element) { 94 | return JSON.parse(element.dataset.cssoffset); 95 | } 96 | 97 | function setmousemeta(element, mousemeta) { 98 | element.dataset.mousemeta = JSON.stringify({ mousemeta: mousemeta }); 99 | } 100 | 101 | function getmeta(element) { 102 | return JSON.parse(element.dataset.meta); 103 | //({ original:original, current: current, target: target, step: step }); 104 | } 105 | 106 | function getstate(element) { 107 | return JSON.parse(element.dataset.state); 108 | } 109 | 110 | function getmousemeta(element) { 111 | return JSON.parse(element.dataset.mousemeta); 112 | } 113 | 114 | function getcurrentmeta(element) { 115 | 116 | //adjust the x,y to the centre of the element 117 | //x,y this is its apparent absolute position, manually taking into account margins etc 118 | //we know this is relative to the body 119 | 120 | var temp = { x: 0, y: 0, w: 0, h: 0 }; 121 | 122 | var trueoffset = getmouseposition({ clientX: element.offsetLeft, clientY: element.offsetTop }); 123 | 124 | //alert(JSON.stringify(element.offsetLeft)); 125 | 126 | //get the style information 127 | var tempstyle = theCanvas.currentStyle || window.getComputedStyle(theCanvas); 128 | 129 | temp.x = -parseFloat(tempstyle.marginLeft.replace('px', '')) + (element.getBoundingClientRect().left + element.getBoundingClientRect().width / 2); 130 | temp.y = -parseFloat(tempstyle.marginTop.replace('px', '')) + (element.getBoundingClientRect().top + element.getBoundingClientRect().height / 2); 131 | 132 | temp.w = element.getBoundingClientRect().width; 133 | temp.h = element.getBoundingClientRect().height; 134 | 135 | return temp; 136 | } 137 | 138 | function togglegrid() { 139 | 140 | if (grid > 0) { 141 | usegrid = document.getElementById('gridtoggle').checked; 142 | } 143 | } 144 | 145 | function setgrid(name, meta) { 146 | var t; 147 | t = document.getElementById('metagridname') 148 | t.innerHTML = name; 149 | t = document.getElementById('metagridx') 150 | t.innerHTML = 'X: ' + meta.x.toFixed(2); 151 | t = document.getElementById('metagridy') 152 | t.innerHTML = 'Y: ' + meta.y.toFixed(2); 153 | t = document.getElementById('metagridw') 154 | t.innerHTML = 'W: ' + meta.w.toFixed(2); 155 | t = document.getElementById('metagridh') 156 | t.innerHTML = 'H: ' + meta.h.toFixed(2); 157 | } 158 | 159 | function makedraggable(element) { 160 | 161 | element.classList.add("drag"); 162 | element.addEventListener("mousedown", mouseDownListener, false); 163 | 164 | //get the original location based on whatever the CSS is at the time of loading the element 165 | var origmeta = getcurrentmeta(element); 166 | 167 | var origLeft = (origmeta.x - (origmeta.w / 2)); 168 | var origTop = (origmeta.y - (origmeta.h / 2)); 169 | 170 | setmeta(element, origmeta, origmeta, origmeta, { x: 0, y: 0, w: 0, h: 0 }); 171 | 172 | //apply absolute, store the new location and reset to the original positioning 173 | //this gives us any positioning deltas we need to apply to the CSS when we create the custom CSS 174 | 175 | //need to only handle inline styles !! 176 | //so we access the element.style AND NOT THE computed style 177 | //this shows style entries that are actually inline and ignores those from stylesheets 178 | 179 | var originalposition = element.style.position; 180 | 181 | element.style.position = 'absolute'; 182 | 183 | var absmeta = getcurrentmeta(element); 184 | 185 | if (originalposition == '') { 186 | element.style.removeProperty('position'); 187 | } 188 | else { 189 | element.style.position = originalposition; 190 | } 191 | 192 | var absdeltaLeft = (absmeta.x - (absmeta.w / 2)) - origLeft; 193 | var absdeltaTop = (absmeta.y - (absmeta.h / 2)) - origTop; 194 | 195 | var offsetX = element.offsetLeft - origLeft - absdeltaLeft; 196 | var offsetY = element.offsetTop - origTop - absdeltaTop; 197 | 198 | //and store them in the element 199 | 200 | setcss(element, offsetX, offsetY); 201 | 202 | //add a couple of tracking elements and check if this is absolute positioned at any specificity 203 | 204 | setstate(element, false, false, (window.getComputedStyle(element, null).position == 'absolute')); 205 | 206 | //add an observer to catch a change to the position (made by the main.js as part of hiding/showing modules, animating transitions) 207 | //so we can override and keep them visible at all times 208 | 209 | // Select the node that will be observed for mutations 210 | const targetNode = element; 211 | 212 | // Options for the observer (which mutations to observe) 213 | const config = { attributes: true, attributeFilter: ["style"], attributeOldValue: true,}; 214 | 215 | // Callback function to execute when mutations are observed 216 | // only actually fire once the target element is active 217 | const callback = function (mutationsList, observer) { 218 | // Use traditional 'for loops' for IE 11 219 | for (let mutation of mutationsList) { 220 | if (mutation.target.dataset != null) { 221 | var state = getstate(mutation.target); 222 | // start this as soon as we have loaded as we need to show the module in the location we want and not have 223 | // the static position override the absolute if we need it 224 | if (state.active || state.absolute) { 225 | var oldvalue = getstyleasjson(mutation.oldValue); 226 | if (oldvalue != null) { 227 | if (mutation.target.style.postion != 'absolute') { 228 | mutation.target.style.position = 'absolute' 229 | }; 230 | } 231 | } 232 | } 233 | } 234 | }; 235 | 236 | function getstyleasjson(stylestring) { 237 | if (stylestring == null) { return null }; 238 | var temp = ''; 239 | var obj = stylestring.split(";"); 240 | obj.forEach(function (pair) { 241 | if (pair != "") { 242 | var jpair = pair.split(":"); 243 | temp = temp + '"' + jpair[0].trim() + '":"' + jpair[1].trim() + '",'; 244 | } 245 | }) 246 | temp = "{" + temp.substr(0, temp.length - 1) + "}" 247 | return JSON.parse(temp); 248 | } 249 | 250 | // Create an observer instance linked to the callback function 251 | const observer = new MutationObserver(callback); 252 | 253 | // Start observing the target node for configured mutations 254 | observer.observe(targetNode, config); 255 | 256 | // Later, you can stop observing 257 | //observer.disconnect(); 258 | 259 | } 260 | 261 | function makeresizable(element) { 262 | 263 | element.classList.add("resizable"); 264 | 265 | var divtl = document.createElement('div'); 266 | divtl.classList.add("resizer"); 267 | divtl.classList.add("top-left"); 268 | element.appendChild(divtl); 269 | 270 | var divtr = document.createElement('div'); 271 | divtr.classList.add("resizer"); 272 | divtr.classList.add("top-right"); 273 | element.appendChild(divtr); 274 | 275 | var divbl = document.createElement('div'); 276 | divbl.classList.add("resizer"); 277 | divbl.classList.add("bottom-left"); 278 | element.appendChild(divbl); 279 | 280 | var divbr = document.createElement('div'); 281 | divbr.classList.add("resizer"); 282 | divbr.classList.add("bottom-right"); 283 | element.appendChild(divbr); 284 | 285 | divtl.addEventListener("mousedown", mouseDownListener, false); 286 | divtr.addEventListener("mousedown", mouseDownListener, false); 287 | divbl.addEventListener("mousedown", mouseDownListener, false); 288 | divbr.addEventListener("mousedown", mouseDownListener, false); 289 | 290 | setmeta(element, getcurrentmeta(element), getcurrentmeta(element), getcurrentmeta(element), { x: 0, y: 0, w: 0, h: 0 }); 291 | 292 | } 293 | 294 | //get the actual element not the resizer 295 | function getelement(element,getparent=false) { 296 | 297 | if (element.classList == null) { //must be over the body or elsewhere if this fires 298 | return currentelement; 299 | } 300 | 301 | if (element.classList.contains('drag')) { 302 | return element; 303 | } 304 | 305 | if (element.classList.contains('resizer')) { // we manage all movement based on the resizer circles, only redrawing is at parent level 306 | if (getparent) { 307 | return element.parentElement; 308 | } 309 | else { 310 | return element; 311 | } 312 | } 313 | 314 | //must be over the body or elsewhere 315 | 316 | return currentelement; 317 | 318 | } 319 | 320 | function getmouseposition(mouseevent) { 321 | 322 | //additional support for a canvas that hasn't yet been populated (like body) 323 | //it takes a default value of the window 324 | 325 | var defaultheight = window.innerHeight; 326 | var defaultwidth = window.innerWidth; 327 | 328 | //getting mouse position correctly 329 | var bRect = theCanvas.getBoundingClientRect(); 330 | mouseX = (mouseevent.clientX - bRect.left) * (((theCanvas.clientWidth == 0) ? defaultwidth : theCanvas.clientWidth) / ((bRect.width == 0) ? defaultwidth : bRect.width) ); 331 | mouseY = (mouseevent.clientY - bRect.top) * (((theCanvas.clientHeight == 0) ? defaultheight : theCanvas.clientHeight) / ((bRect.height == 0) ? defaultheight : bRect.height)); 332 | 333 | return { mouseX: mouseX, mouseY: mouseY }; 334 | 335 | } 336 | 337 | //mousedown supports both resize and draggable 338 | //theCanvas is whatever element is used to constrain the action 339 | 340 | function mouseDownListener(event) { 341 | 342 | //stop a resizer mousedown from bubbling up to the parent and vice versa 343 | 344 | event.stopPropagation(); 345 | 346 | var mouse = getmouseposition(event); 347 | 348 | var mouseX = mouse.mouseX; 349 | var mouseY = mouse.mouseY; 350 | 351 | //store the meta for the mousedown and element and the mouse movement direction calculations (mousemoved) 352 | 353 | mousedown.mouse.x = mouseX; 354 | mousedown.mouse.y = mouseY; 355 | 356 | //store the new mouse location 357 | 358 | mousedown.mousemoved.x = mouseX; 359 | mousedown.mousemoved.y = mouseY; 360 | 361 | // and for snap control in grids / resizing reset snapped to false 362 | 363 | mousedown.snapped = false; 364 | 365 | //determine if we are dragging or resizing 366 | 367 | dragging = true; //we found something to drag //this should always be true as the mousedown events are only linked to draggable and re-sizable elements 368 | 369 | //but, if there is an outstanding timer, (about to be orphaned) , don't let the action start 370 | 371 | if (Object.keys(timers).length > 0) { 372 | dragging = false; 373 | } 374 | 375 | if (dragging) { 376 | 377 | event.currentTarget.removeEventListener("mousedown", mouseDownListener, false); 378 | 379 | //determine who we are dealing with 380 | //element is the mousedown, that may be a resizer, in which case we need the parent 381 | 382 | var element = getelement(event.currentTarget); 383 | var parentelement = getelement(event.currentTarget, true); 384 | 385 | //pop the div to the top level so absolute actual works 386 | //and make it absolute here so we have correct initial positioning 387 | //before we do this we set the new location to the original location before we apply the positioning 388 | //absolute positioning will override certain #CSS settings and the element may move when it is made absolute 389 | //and we get the latest values for w/h/x/y because they have changed since last we were here for this element 390 | //and depending on its contents the w/h may change 391 | 392 | setmeta(parentelement, getmeta(parentelement).original, getcurrentmeta(parentelement), getcurrentmeta(parentelement), {x:0,y:0,w:0,h:0}) 393 | var currentmeta = getmeta(parentelement); 394 | 395 | //store the location for mousedown delta calculation and resizing 396 | mousedown.element.x = currentmeta.current.x; 397 | mousedown.element.y = currentmeta.current.y; 398 | mousedown.element.w = currentmeta.current.w; 399 | mousedown.element.h = currentmeta.current.h; 400 | 401 | //move the element 402 | parentelement.style.top = (currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 403 | parentelement.style.left = (currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 404 | 405 | parentelement.style.width = (currentmeta.current.w).toString() + 'px'; 406 | parentelement.style.height = (currentmeta.current.h).toString() + 'px'; 407 | 408 | parentelement.style.position = 'absolute'; 409 | 410 | setstate(parentelement, getstate(parentelement).amended, true, getstate(parentelement).absolute,); //set active to true 411 | 412 | document.body.append(parentelement); 413 | 414 | //tell the mutation observer for this element to start observing. 415 | 416 | //parentelement. 417 | 418 | //check if we are actually resizing 419 | 420 | if (element != parentelement) { 421 | resizing = true; 422 | } 423 | 424 | if (!resizing) { 425 | document.body.style.cursor = "move"; 426 | }; 427 | 428 | //store the current element 429 | currentelement = element; 430 | 431 | window.addEventListener("mousemove", mouseMoveListener, false); 432 | window.addEventListener("mouseup", mouseUpListener, false); 433 | 434 | //store the current mouse position 435 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: 0, deltaY: 0 }); 436 | 437 | //adjust the element target to be same as location (it should be anyway) 438 | currentmeta = getmeta((resizing) ? parentelement : element); 439 | setmeta((resizing) ? parentelement : element, getmeta((resizing) ? parentelement : element).original, currentmeta.current, currentmeta.current, currentmeta.step); 440 | 441 | timer = setInterval(onTimerTick, 1000 / interval); 442 | timers[timer]=timer; 443 | 444 | //code below prevents the mouse down from having an effect on the main browser window: 445 | if (event.preventDefault) { 446 | event.preventDefault(); 447 | } //standard 448 | else if (event.returnValue) { 449 | event.returnValue = false; 450 | } //older IE 451 | return false; 452 | } 453 | } 454 | 455 | function mouseMoveListener(event) { 456 | 457 | //work out the delta of mouse 458 | //new mouse becomes target 459 | //after clamping to the canvas 460 | 461 | //because we are moving we stick with the current element and don't try to determine who we are moving over 462 | //otherwise the mouseover finds another valid element 463 | 464 | //added detection of the leading edge(s) when using the grid. 465 | //the leading edge(s) will snap to the nearest grid - in the direction of travel only 466 | //so the snap only occurs if the move/resize of the element will be in the expected direction 467 | 468 | var element = currentelement; 469 | 470 | //as the element has been moved, we assume it has been amended 471 | //resizing works differently to dragging so we have to split the parts of the code - really ! 472 | 473 | if (resizing) { 474 | var currentmeta = getmeta(element.parentElement); 475 | setstate(element, true, getstate(element.parentElement).active, getstate(element.parentElement).absolute); //set amended true 476 | } 477 | else { 478 | var currentmeta = getmeta(element); 479 | setstate(element, true, getstate(element).active, getstate(element).absolute); //set amended true 480 | } 481 | 482 | //warning dont assign objects to variables, it links them !! 483 | 484 | var checkmeta = {target: { x: currentmeta.target.x, y: currentmeta.target.y, w: currentmeta.target.w, h: currentmeta.target.h} }; 485 | 486 | //get new mouse position 487 | var mouse = getmouseposition(event); 488 | var mouseX = mouse.mouseX; 489 | var mouseY = mouse.mouseY; 490 | 491 | //determine if new target is in bounds 492 | //test is based on the existing target being moved to the new mouse location (-mousedown delta) 493 | //otherwise clamp the mouse to a value to adhere to the above rule 494 | //takes into account that a grid snap may take the element out of bounds 495 | 496 | //the following are used to track the leading edge(s) by determining travel direction 497 | //0 = no travel - ignore 498 | //+ve towards right/down 499 | //-ve towards left/top 500 | 501 | //calculate direction and stores as -1 0 or +1 502 | //calculates each time there is a new move, relative to the last move, NOT the mouse down location 503 | 504 | var dx = mouseX - mousedown.mousemoved.x; 505 | var dy = mouseY - mousedown.mousemoved.y; 506 | 507 | dx = (dx < 0) ? -1 : (dx > 0 ? 1 : 0); 508 | dy = (dy < 0) ? -1 : (dy > 0 ? 1 : 0); 509 | 510 | if (resizing) { 511 | 512 | checkmeta.target = getresizedelement(element, mouseX, mouseY); 513 | 514 | } 515 | else 516 | { 517 | checkmeta.target.x = mouseX - (mousedown.mouse.x - mousedown.element.x); 518 | checkmeta.target.y = mouseY - (mousedown.mouse.y - mousedown.element.y); 519 | 520 | if (usegrid) { 521 | 522 | //only try and snap the edges that are leading and only in the direction of travel 523 | //if snap is +ve and travel -ve ignore and vice versa 524 | //requires calculating the edge location now we have done first move but not checked for bounds 525 | 526 | //calculate the new leading edge(s) x and y 527 | 528 | var lex = checkmeta.target.x + ((currentmeta.current.w / 2) * dx); 529 | var ley = checkmeta.target.y + ((currentmeta.current.h / 2) * dy); 530 | 531 | //check if a snap is in the right direction 532 | //and then calculate a new target to make the element move the leading edge to the correct position 533 | 534 | if ((dx == -1 && lex > Math.round(lex / grid) * grid) || (dx == 1 && lex < Math.round(lex / grid) * grid)) { 535 | checkmeta.target.x = (Math.round(lex / grid) * grid) - ((currentmeta.current.w / 2) * dx); 536 | } 537 | 538 | if ((dy == -1 && ley > Math.round(ley / grid) * grid) || (dy == 1 && ley < Math.round(ley / grid) * grid)) { 539 | checkmeta.target.y = (Math.round(ley / grid) * grid) - ((currentmeta.current.h / 2) * dy); 540 | } 541 | 542 | } 543 | 544 | } 545 | 546 | //calculate the bounds based on the new target size 547 | var minX = (checkmeta.target.w / 2); 548 | var maxX = (((theCanvas.clientWidth == 0) ? window.innerWidth : theCanvas.clientWidth) - (checkmeta.target.w / 2)); 549 | var minY = (checkmeta.target.h / 2); 550 | var maxY = (((theCanvas.clientHeight == 0) ? window.innerHeight : theCanvas.clientHeight) - (checkmeta.target.h / 2)); 551 | 552 | //check the centre fits within the bounds 553 | 554 | //checkmax returns a -value if out of bounds 555 | //checkmin returns a +value if out of bounds 556 | var checkmaxX = (maxX - checkmeta.target.x); 557 | var checkmaxY = (maxY - checkmeta.target.y); 558 | var checkminX = (minX - checkmeta.target.x); 559 | var checkminY = (minY - checkmeta.target.y); 560 | 561 | //adjust the new mouse position to take into account any out of bounds amounts 562 | //if any neg max values or pos min values 563 | mouseX = mouseX + ((checkminX > 0) ? checkminX : 0) + ((checkmaxX < 0) ? checkmaxX : 0); 564 | mouseY = mouseY + ((checkminY > 0) ? checkminY : 0) + ((checkmaxY < 0) ? checkmaxY : 0); 565 | 566 | //store the new mouse location 567 | setmousemeta(element, { x: mouseX, y: mouseY, deltaX: 0, deltaY: 0 }); 568 | 569 | if (resizing) 570 | //store the new target after grid adjusting 571 | { 572 | currentmeta.target = getresizedelement(element, mouseX,mouseY); 573 | 574 | setmeta(element.parentElement, getmeta(element.parentElement).original, currentmeta.current, currentmeta.target, currentmeta.step); 575 | setgrid(element.parentElement.id, getmeta(element.parentElement).current); 576 | } 577 | else { 578 | 579 | //the target is the adjusted mouse inbounds 580 | //use grid calculation to adjust the target to snap to the grid 581 | //adjust to snap if active 582 | 583 | //recalculate the target based on the revised mouse position 584 | 585 | checkmeta.target.x = mouseX - (mousedown.mouse.x - mousedown.element.x); 586 | checkmeta.target.y = mouseY - (mousedown.mouse.y - mousedown.element.y); 587 | 588 | if (usegrid) { 589 | 590 | //recalculate the new leading edge(s) x and y 591 | 592 | var lex = checkmeta.target.x + ((currentmeta.current.w / 2) * dx); 593 | var ley = checkmeta.target.y + ((currentmeta.current.h / 2) * dy); 594 | 595 | //check if a snap is in the right direction 596 | //and then calculate a new target to make the element move the leading edge to the correct position 597 | 598 | if ((dx == -1 && lex > Math.round(lex / grid) * grid) || (dx == 1 && lex < Math.round(lex / grid) * grid)) { 599 | currentmeta.target.x = (Math.round(lex / grid) * grid) - ((currentmeta.current.w / 2) * dx); 600 | } 601 | 602 | if ((dy == -1 && ley > Math.round(ley / grid) * grid) || (dy == 1 && ley < Math.round(ley / grid) * grid)) { 603 | currentmeta.target.y = (Math.round(ley / grid) * grid) - ((currentmeta.current.h / 2) * dy); 604 | } 605 | 606 | } 607 | else { 608 | 609 | //no grid so just use the new location 610 | currentmeta.target.x = checkmeta.target.x; 611 | currentmeta.target.y = checkmeta.target.y; 612 | } 613 | 614 | //store the new target 615 | setmeta(element, getmeta(element).original, currentmeta.current, currentmeta.target, currentmeta.step); 616 | setgrid(element.id, getmeta(element).current); 617 | } 618 | 619 | //store the new mouse location 620 | 621 | mousedown.mousemoved.x = mouseX; 622 | mousedown.mousemoved.y = mouseY; 623 | 624 | } 625 | 626 | function getgriddelta(dx, dy, dxt, dyt, deltaX, deltaY) { 627 | 628 | // calc new leading edge positions 629 | var lex = (mousedown.element.x + (mousedown.element.w / 2) * dx) + (deltaX); 630 | var ley = (mousedown.element.y + (mousedown.element.h / 2) * dy) + (deltaY); 631 | 632 | //and snap to the nearest grid position 633 | //adjust the delta by the difference between the leading edge and the nearest snap, regardless of direction of movement 634 | //once we have have determined we can start snapping 635 | //we need accurate direction of travel until first snap 636 | 637 | //calculate the nearest grid 638 | var snaptox = (Math.round(lex / grid) * grid); 639 | var snaptoy = (Math.round(ley / grid) * grid); 640 | 641 | if (!mousedown.snappedx) { 642 | 643 | //if the leading edge is in the snap zone based on the direction of travel, start snapping 644 | //otherwise wait until it is 645 | //snap zone is 1/2 grid either side of the grid depending on the direction of travel 646 | 647 | if ( 648 | (dxt == 1 && (lex > snaptox - ((grid / 2) * dxt) && lex < snaptox)) || 649 | (dxt == -1 && (lex < snaptox - ((grid / 2) * dxt) && lex > snaptox)) 650 | ) { 651 | mousedown.snappedx = true; 652 | } 653 | else { 654 | deltaX = 0; 655 | } 656 | } 657 | 658 | if (mousedown.snappedx) { 659 | deltaX = deltaX + (snaptox - lex); 660 | } 661 | 662 | if (!mousedown.snappedy) { 663 | if ( 664 | (dyt == 1 && (ley > snaptoy - ((grid / 2) * dyt) && ley < snaptoy)) || 665 | (dyt == -1 && (ley < snaptoy - ((grid / 2) * dyt) && ley > snaptoy)) 666 | ) { 667 | mousedown.snappedy = true; 668 | } 669 | else { 670 | deltaY = 0; 671 | } 672 | } 673 | 674 | if (mousedown.snappedy) { 675 | deltaY = deltaY + (snaptoy - ley); 676 | } 677 | 678 | return {deltaX,deltaY} 679 | } 680 | 681 | function getresizedelement(element, mouseX, mouseY, roundvalues = false) { 682 | 683 | //we get the resize element and the new mouse location 684 | //we know the original mouse location and the centre of the element at mousedown 685 | 686 | //calculate new resizer locations based on the delta between mouse new and mouse down 687 | //applied to the current location 688 | 689 | var deltaX = (mouseX - mousedown.mouse.x) 690 | var deltaY = (mouseY - mousedown.mouse.y) 691 | 692 | //variables that hold the direction of the moose movement towards 0 relative height/size 693 | 694 | var dx = 0; 695 | var dy = 0; 696 | 697 | var dxt = (mouseX > mousedown.mousemoved.x) ? 1 : (mouseX < mousedown.mousemoved.x ? -1 : 0); 698 | var dyt = (deltaY < 0) ? -1 : (deltaY > 0 ? 1 : 0); 699 | 700 | var currentmeta = getmeta(element.parentElement); 701 | var tempmeta = currentmeta.target; 702 | 703 | var width = 0, height = 0; 704 | 705 | if (element.classList.contains('bottom-right')) { 706 | 707 | //set the direction for right and bottom 708 | 709 | dx = 1; 710 | dy = 1; 711 | 712 | if (usegrid) { 713 | var deltas = getgriddelta(dx, dy, dxt, dyt, deltaX, deltaY); 714 | deltaX = deltas.deltaX; 715 | deltaY = deltas.deltaY; 716 | } 717 | 718 | width = mousedown.element.w + deltaX; 719 | height = mousedown.element.h + deltaY; 720 | 721 | if (width >= minimum_size) { 722 | tempmeta.w = width; 723 | tempmeta.x = mousedown.element.x + (deltaX / 2); 724 | } 725 | 726 | if (height >= minimum_size) { 727 | tempmeta.h = height; 728 | tempmeta.y = mousedown.element.y + (deltaY / 2); 729 | } 730 | } 731 | 732 | else if (element.classList.contains('bottom-left')) { 733 | 734 | dx = -1; 735 | dy = 1; 736 | 737 | if (usegrid) { 738 | 739 | var deltas = getgriddelta(dx, dy, dxt, dyt, deltaX, deltaY); 740 | deltaX = deltas.deltaX; 741 | deltaY = deltas.deltaY; 742 | 743 | } 744 | 745 | width = mousedown.element.w - deltaX; 746 | height = mousedown.element.h + deltaY; 747 | 748 | if (height >= minimum_size) { 749 | tempmeta.h = height; 750 | tempmeta.y = mousedown.element.y + (deltaY / 2); 751 | } 752 | 753 | if (width >= minimum_size) { 754 | tempmeta.w = width; 755 | tempmeta.x = mousedown.element.x + (deltaX / 2); 756 | } 757 | } 758 | 759 | else if (element.classList.contains('top-right')) { 760 | dx = 1; 761 | dy = -1; 762 | 763 | if (usegrid) { 764 | 765 | var deltas = getgriddelta(dx, dy, dxt, dyt, deltaX, deltaY); 766 | deltaX = deltas.deltaX; 767 | deltaY = deltas.deltaY; 768 | 769 | } 770 | 771 | width = mousedown.element.w + deltaX; 772 | height = mousedown.element.h - deltaY; 773 | 774 | if (width >= minimum_size) { 775 | tempmeta.w = width; 776 | tempmeta.x = mousedown.element.x + (deltaX / 2); 777 | } 778 | 779 | if (height >= minimum_size) { 780 | tempmeta.h = height; 781 | tempmeta.y = mousedown.element.y + (deltaY / 2); 782 | 783 | } 784 | } 785 | 786 | else {//top-left 787 | dx = -1; 788 | dy = -1; 789 | 790 | if (usegrid) { 791 | 792 | var deltas = getgriddelta(dx, dy, dxt, dyt, deltaX, deltaY); 793 | deltaX = deltas.deltaX; 794 | deltaY = deltas.deltaY; 795 | 796 | } 797 | 798 | width = mousedown.element.w - deltaX; 799 | height = mousedown.element.h - deltaY; 800 | 801 | if (width >= minimum_size) { 802 | tempmeta.w = width; 803 | tempmeta.x = mousedown.element.x + (deltaX/2); 804 | } 805 | if (height >= minimum_size) { 806 | tempmeta.h = height; 807 | tempmeta.y = mousedown.element.y + (deltaY/2); 808 | } 809 | } 810 | 811 | 812 | if (width < minimum_size) { 813 | tempmeta.w = minimum_size; 814 | var adjustx = minimum_size - width; 815 | tempmeta.x = mousedown.element.x + ((deltaX + adjustx * dx) / 2); 816 | } 817 | 818 | if (height < minimum_size) { 819 | tempmeta.h = minimum_size; 820 | var adjusty = minimum_size - height; 821 | tempmeta.y = mousedown.element.y + ((deltaY + adjusty * dy) / 2); 822 | } 823 | 824 | return tempmeta; 825 | 826 | } 827 | 828 | function onTimerTick() { 829 | 830 | //get the correct element to action 831 | 832 | var actionelement = (resizing) ? currentelement.parentElement : currentelement; 833 | 834 | var currentmeta = getmeta(actionelement); 835 | 836 | //calculate the step 837 | currentmeta.step.x = easeAmount * (currentmeta.target.x - currentmeta.current.x); 838 | currentmeta.step.y = easeAmount * (currentmeta.target.y - currentmeta.current.y); 839 | currentmeta.step.w = easeAmount * (currentmeta.target.w - currentmeta.current.w); 840 | currentmeta.step.h = easeAmount * (currentmeta.target.h - currentmeta.current.h); 841 | 842 | //adjust the current location 843 | currentmeta.current.x = currentmeta.current.x + currentmeta.step.x; 844 | currentmeta.current.y = currentmeta.current.y + currentmeta.step.y; 845 | currentmeta.current.w = currentmeta.current.w + currentmeta.step.w; 846 | currentmeta.current.h = currentmeta.current.h + currentmeta.step.h; 847 | 848 | //stop the timer when the target position is reached (close enough) 849 | if ( 850 | (!dragging) && 851 | (Math.abs(currentmeta.current.x - currentmeta.target.x) < 0.1) && 852 | (Math.abs(currentmeta.current.y - currentmeta.target.y) < 0.1) 853 | && 854 | (Math.abs(currentmeta.current.w - currentmeta.target.w) < 0.1) && 855 | (Math.abs(currentmeta.current.h - currentmeta.target.h) < 0.1) 856 | ) 857 | { 858 | currentmeta.current.x = currentmeta.target.x; 859 | currentmeta.current.y = currentmeta.target.y; 860 | currentmeta.current.w = currentmeta.target.w; 861 | currentmeta.current.h = currentmeta.target.h; 862 | 863 | //stop timer: 864 | 865 | delete timers[timer]; 866 | 867 | clearInterval(timer); 868 | } 869 | 870 | //save the new location 871 | setmeta(actionelement, getmeta(actionelement).original, currentmeta.current, currentmeta.target, currentmeta.step) 872 | 873 | //move the element 874 | actionelement.style.top = (currentmeta.current.y - (currentmeta.current.h / 2)).toString() + 'px'; 875 | actionelement.style.left = (currentmeta.current.x - (currentmeta.current.w / 2)).toString() + 'px'; 876 | 877 | actionelement.style.width =(currentmeta.current.w).toString() + 'px'; 878 | actionelement.style.height = (currentmeta.current.h).toString() + 'px'; 879 | 880 | } 881 | 882 | function mouseUpListener(event) { 883 | 884 | //because we were moving we stick with the current element and dont try to determine who we are moving over 885 | //otherwise the mouseover finds another valid dragme and attachs the mouse down to the wrong element 886 | var element = currentelement; //getelement(event.target); 887 | 888 | element.addEventListener("mousedown", mouseDownListener, false); 889 | window.removeEventListener("mouseup", mouseUpListener, false); 890 | if (dragging) { 891 | dragging = false; 892 | if (resizing) { 893 | resizing = false; 894 | currentelement = currentelement.parentElement; // as we loose the resizer indicator, we need to let tick tock know which element to actually ease out 895 | } 896 | document.body.style.cursor = "auto" 897 | window.removeEventListener("mousemove", mouseMoveListener, false); 898 | } 899 | 900 | mousedown.snappedx = false; 901 | mousedown.snappedy = false; 902 | } 903 | --------------------------------------------------------------------------------