├── .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 | 
10 | 
11 | 
12 | 
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 | 
15 | This shows the result after the custom.css was applied and moduleposition removed.
16 | 
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 |
--------------------------------------------------------------------------------