Blog-Archiv

Dienstag, 26. Februar 2019

How to Flatten AMD JavaScript

I have written a lot of JavaScript goodies which I would like to apply sometimes here to make things more fancy. As this Blog doesn't allow me to upload scripts, I need to copy & paste the script code into the HTML.

Now my script goodies are written as AMD modules. AMD is a special way how you can organize dependencies, I would say it is the safest and most advanced, free of magic, dynamical by nature. But AMD-packed JS code is not so easy to use for copy & paste, there have been things done to avoid exactly this, and pasting also the AMD loader into each Blog page is definitely too much. The following shows a quick way how to "flatten" AMD-packed code.

AMD has not yet been fully replaced by ES6 imports. At the time being, all imported ES6 code is loaded initially by the browser, which is NOT dynamical!

Understanding AMD

  1. First AMD requires to load the AMD loader itself into the HTML page. Assumed the loader implementation is in file lib/define.js below the page, this could be done by following HTML code:

      <script src="lib/define.js" data-path="lib"></script>
    

    Mind that you need a closing </script> tag here, it would not work with "/>".

    The src attribute tells the browser where to load the script from. The data-path attribute tells the AMD loader the base path where the AMD libraries are, relatively to the page (the loader will search its own HTML element and read this attribute). Such a base path is needed when you have HTML pages in different directories, but all want to refer to the same JS libraries.

  2. After this you can use the global define() function to load dependencies, e.g. by following code in the HTML page that retrieves an alert-message from dependency "jokes/bouuh.js":

    <script>
        define(
          [
              "jokes/bouuh.js"
          ],
          function(message) {
              alert(message);
          }
        );
    </script>
    

    This define() call receives two parameters that are associated with each other. The first is an array of dependencies, given as paths relative to data-path. The second is a function that accepts as many parameters as the dependency-array has members, each dependency-member will yield an object that gets passed as parameter to that function. That means, function(message) receives whatever the execution of "jokes/bouuh.js" returns. Of course these return-objects are cached by the loader, they will be singletons, and every other "jokes/bouuh.js" dependency will receive the same object.

    Modules that do not have dependencies can leave out the first parameter, the loader will detect that and will just execute the function-parameter.

    This example is a top-level call, so it will be executed when the script element gets loaded. The define() being nested inside some other function, dependencies would get loaded deferred at the time the function was executed - NOT possible with ES6 imports!

  3. Every AMD script file returns exactly one singleton. The singletons are meant to be factories for objects in case instances are needed. Here is an example AMD module file:

    lib/jokes/bouuh.js
    define(
        function() {
            return "Bouuh!";
        }
    );
    

    The dependency-array is missing, define() has just one parameter, this is legal. The function inside define() will be executed by the AMD loader, and its return will be buffered as singleton for the key "jokes/bouuh.js". The return is the string "Bouuh!", thus the alert() call above will show us "Bouuh!".

How to Flatten an AMD Dependency Tree

Nice things, but all in the way when you are forced to abandon dynamic dependency management and do primitive copy & paste work for some Blog article.

Knowing how this works we can simulate what an AMD loader does:

  • load dependencies and pass their return objects as parameters to the function (2nd parameter),
  • cache the function's return as singleton and pass it further to any dependent module.

Assign AMD Module to Variable

Dig down the dependency root and find the AMD modules that do not depend on anything. For each, create a variable and assign the return of the executed module function to it. Here I did this for "jokes/bouuh.js":

var jokesBouuh = function() {
    return "Bouuh!"
}();

Try to name the variable like the module file name. And mind that you need these IIFE parentheses after the closing curly brace! This stands for the function execution through the AMD loader, replacing the define() call.

Pass Variable as Parameter

To any dependent module, pass the variable(s) as parameter(s).

function(message) {
    alert(message);
}(jokesBouuh);

Of course it will not be so simple as shown here, but that is all you need to flatten AMD scripts.

Resume

JavaScript dependency management creates complicated structures. AMD is a recursive concept, and a reliable and efficient mechanism, but it is not so easy to practice and understand. Especially when it comes to using the AMD singletons as object factories, flattening such modules will not be easy. The usage of an AMD module should be documented exhaustively in a comment header, else the chance is high that it won't be understood and be applied wrongly.

Both AMD and functional inheritance use just language means to solve their problem. That makes them outstanding and clear concepts. The world is going different ways, promoting ES6 imports and classes. Although that didn't solve everything cleanly, it will be easier to read - is it?




Sonntag, 24. Februar 2019

Java 11 Windows Drive File Construction

If you have old Java applications that use File constructors for Windows drives, avoiding File.listRoots(), you may want to read this Blog, because that functionality changed a little bit in Java 11. Unix systems are not affected.

Why not use File.listRoots()? This method didn't exist in Java 1.1. Moreover, in times of floppy-drives, this call caused periodical dialogs appearing in case no floppy was inserted. Java 1.2 listRoots() was not really usable for listing Windows drives, a still persisting problem are network drives that answer very slowly. You will want to let run each of those drive-explorations in a background thread, integrating them just when they answer within a configured timeout.

Windows versus Unix File-System

The difference between Windows and Unix file systems is that Unix has exactly one file-system root ("/"), while Windows has as many roots as disks and drives of any kind are available ("A:", "B:", "C:", .... "Z:"). The Windows path-separator is "\" (backslash), while Unix has "/" (slash).

Constructor Test Results

In the following you find different calls to File constructors, and what properties they yield. I was running these tests with Java 1.8 and 11 on Windows 8.1. For comparability I also included the results from Ubuntu 18.04 Linux. Click on the expand-controls below to see how constructors behave.

  • Windows 8.1
    • Java 1.8.0_25
      • File drive = new File("C:")
        • exists()getName()getPath()getAbsolutePath()toString()
          trueC:C:\Users\fred\eclipse-workspace\friwintestC:
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          falsebinC:\binC:\binC:\bin
          falsesrcC:\srcC:\srcC:\src
      • File drive = new File("C:\")
        • exists()getName()getPath()getAbsolutePath()toString()
          trueC:\C:\C:\
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          trueProgram FilesC:\Program FilesC:\Program FilesC:\Program Files
          trueUsersC:\UsersC:\UsersC:\Users
          trueWindowsC:\WindowsC:\WindowsC:\Windows
      • File drive = new File("\")
        • exists()getName()getPath()getAbsolutePath()toString()
          true\C:\\
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          trueProgram Files\Program FilesC:\Program Files\Program Files
          trueUsers\UsersC:\Users\Users
          trueWindows\WindowsC:\Windows\Windows
    • Java 11.0.2
      • File drive = new File("C:")
        • exists()getName()getPath()getAbsolutePath()toString()
          trueC:C:\Users\fred\eclipse-workspace\friwintestC:
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          truebinC:binC:\Users\fred\eclipse-workspace\friwintest\binC:bin
          truesrcC:srcC:\Users\fred\eclipse-workspace\friwintest\srcC:src
      • File drive = new File("C:\")
        • exists()getName()getPath()getAbsolutePath()toString()
          trueC:\C:\C:\
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          trueProgram FilesC:\Program FilesC:\Program FilesC:\Program Files
          trueUsersC:\UsersC:\UsersC:\Users
          trueWindowsC:\WindowsC:\WindowsC:\Windows
      • File drive = new File("\")
        • exists()getName()getPath()getAbsolutePath()toString()
          true\C:\\
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          trueProgram Files\Program FilesC:\Program Files\Program Files
          trueUsers\UsersC:\Users\Users
          trueWindows\WindowsC:\Windows\Windows
  • Linux
    • Java 1.8.0_25
      • File drive = new File("/")
        • exists()getName()getPath()getAbsolutePath()toString()
          true///
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          truebin/bin/bin/bin
          trueusr/usr/usr/usr
    • Java 11.0.2
      • File drive = new File("/")
        • exists()getName()getPath()getAbsolutePath()toString()
          true///
          for (String name: drive.list())
              new File(drive, name);
          exists()getName()getPath()getAbsolutePath()toString()
          truebin/bin/bin/bin
          trueusr/usr/usr/usr

new File("C:")

When expanding Windows -> Java 1.8 -> new File("C:"), you will see that the absolute path doesn't match expectations. Instead of "C:\" it is the current working directory where my test-app was running. Below Java 11 you need to use new File("C:\\"), see next example.

Nevertheless, when you do new File("C:").list() and construct children with these names, and the drive as parent, the constructed files will not exist, because the list() children were taken from current working directory!

Similar problems exist in Java 11, although the list-children actually exist in this version. But look at the getPath() result: "C:bin" isn't a valid path!

new File("C:\\")

With "C:\\" everything works fine, and it's the same for Java 1.8 and Java 11. It's just the ugly file-separator in the name of the drive.

new File("\\")

This would be something similar to UNIX "/". It works, but you don't get the drive name "C:" from getPath().

By the way, toString() still delegates to getPath(), they did not change that in Java 11.

Test Source Code

You could do such a test with the following example, compiling it with Java 1.8 and then running it with both 1.8 and 11 (JRE 1.8 will not run classes compiled with 11):

public class FileConstructorTest
{
    public static void main(String [] args) {
        test("C:");
        test("C:"+File.separator);
        test(File.separator);
    }

    private static void test(String driveName) {
        final File drive = new File(driveName);
        output(drive);
        
        for (final String name : drive.list())
            output(new File(drive, name));
    }

    private static void output(File file)   {
        System.err.println(
                "exists="+file.exists()+
                ", name="+file.getName()+
                ", path="+file.getPath()+"</td>"+
                ", absolutePath="+file.getAbsolutePath()+
                ", toString="+file.toString());
    }
}



Freitag, 22. Februar 2019

JS Swappable Metro Quads

Letting the user rearrange a grid by drag & drop may be quite useful sometimes, especially when the order of things is in need of discussion. In this Blog I will present CSS and JavaScript (no JQuery:-) code showing how such could be done.

Limitations

WARNING: This example will work on HTML-5 browsers only, as it uses CSS variables!

The swappable rectangles need to have a fixed dimension.

The CSS hardcodes the number of rows and columns of the grid, thus adding grid cells will require changing the CSS.

Demo

Try to drag one of the yellow rectangles below. You can move it to another grid cell, then it will be swapped with the one residing there. You can also move it outside the grid and drop it there.

1
2
3
4
5
6
7
8
9

CSS


        :root
        {
            --dock-element-width: 10em; /* CSS variables */
            --dock-element-height: 6em; /* to bind dock-element to grid dimension */
        }
        .dock-dimension
        {
            width:  var(--dock-element-width); /* fixed dimension for all swapable rectangles */
            height: var(--dock-element-height);
        }
        .dock-container /* would be dispensable here, but not in HTML */
        {
        }
        .dock-element
        {
            background-color: #FFFF99; /* would be transparent else */
            box-sizing: border-box; /* needed when having a border */
            border: 1px solid gray; /* to better see the element when dragged */
        }

This CSS defines two variables that contain width and height of a grid cell. A CSS variable is written like a property, but it starts with two dashes "--". Both variables sit on the :root pseudo-class, which is a safe place for globals.

Their purpose is to size any HTML-element that declares class="dock-dimension". Mind that dereferencing a variable requires the var() wrapper.

For completeness I also defined the CSS class dock-container. Then there is some styling for grid cells tagged by class="dock-element". Essentially only the dimension is needed in this first CSS part.

        .table
        {
            display: table;
            width: calc(var(--dock-element-width) * 3); /* grid contains 3 columns */
            height: calc(var(--dock-element-height) * 3); /* grid contains 3 rows */
        }
        .table > div
        {
            display: table-row;
        }
        .table > div > div
        {
            display: table-cell;
        }

This is the table-layout of the grid holding the swappable cells. It needs to be exactly as wide as three cells are, same for height. CSS variables can be used inside the calc() function, and thanks to HTML-5 even the em unit survives the calculation.

The following two rule-sets define that div elements below are table-rows and -cells. You could also use a conventional HTML table, but you must size it like shown here.

HTML


    <div class="table">
        <div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">1</div>
            </div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">2</div>
            </div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">3</div>
            </div>
        </div>
        
        <div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">4</div>
            </div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">5</div>
            </div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">6</div>
            </div>
        </div>
        
        <div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">7</div>
            </div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">8</div>
            </div>
            <div class="dock-container dock-dimension">
                <div class="dock-element dock-dimension">9</div>
            </div>
        </div>
        
    </div>

Here the CSS classes are used to define all cell-elements that should be swappable, and their containers. They need to be marked with these classes not only due to the CSS dimensions, but also because JavaScript needs to find and manage them.

JavaScript


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    var zIndex = 1;
    var draggedElement;
    
    
    function moveWhenDragged(element)
    {
        var relativeOffset;

        element.addEventListener('mousedown', function(event) {
                if (event.button === 0) {  /* is left mouse button */
                    relativeOffset = {     /* remember relative click coordinates */
                        x: event.clientX - element.offsetLeft,
                        y: event.clientY - element.offsetTop 
                    };
                    element.style.zIndex = zIndex;  /* move to top */
                    zIndex++;
                }
            },
            true);
        
        document.addEventListener('mouseup', function() {
                relativeOffset = undefined;
            },
            true);
        
        document.addEventListener('mousemove', function(event) {
                if (relativeOffset !== undefined) {
                    event.preventDefault();
                    
                    element.style.left = Math.round(event.clientX - relativeOffset.x)+"px";
                    element.style.top  = Math.round(event.clientY - relativeOffset.y)+"px";
                    
                    draggedElement = element;
                }
            },
            true);
    };
        
    var dockElements = document.getElementsByClassName("dock-element");
    for (var i = 0; i < dockElements.length; i++)
        moveWhenDragged(dockElements[i]);

This will let you drag and drop all dock-elements in case their CSS position is absolute. They will be prepared that way by the initialize() function in code below.

The mouse-down callback inside moveWhenDragged() will be installed onto all elements that carry the CSS class dock-element. That function will calculate the offsets relative to the dragged rectangle.

The mouse-up callback will erase that offset-object, so that it is defined just while the user drags. Mind that all listeners except mouse-down get installed onto the document.

The mouse-move event callback will change the element's page-absolute coordinates, using the event's clientX and clientY and the current mouse-down point inside the rectangle.

The global variable zIndex is to lift any dragged element above all others.
The global variable draggedElement will be evaluated whenever an element is dragged, it will be used in further code below.

Next comes the hard part, but it's the last :-)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
    function moveToWhenOverDock(containers, elements)
    {
        function intersectionArea(r1, r2) {
            var overlapX = Math.max(0, Math.min(r1.right, r2.right) - Math.max(r1.left, r2.left));
            var overlapY = Math.max(0, Math.min(r1.bottom, r2.bottom) - Math.max(r1.top, r2.top));
            return overlapX * overlapY;        
        };
        
        function sameTopLeft(containerRect, elementRect) {
            var DEVIATION = 2;
            var top1 = Math.round(containerRect.top);
            var top2 = Math.round(elementRect.top);
            var left1 = Math.round(containerRect.left);
            var left2 = Math.round(elementRect.left);
            return Math.abs(top1 - top2) <= DEVIATION && Math.abs(left1 - left2) <= DEVIATION;
        };
        
        function indexOfMaximum(array) {
            var max = 0;
            var maxIndex = -1;
            for (var i = 0; i < array.length; i++)
                if (array[i] > max)
                    max = array[maxIndex = i];
            return maxIndex;
        };
        
        function getDockedElement(container, excludedElement) {
            var containerRect = container.getBoundingClientRect();
            for (var j = 0; j < elements.length; j++)
                if (elements[j] !== excludedElement && sameTopLeft(containerRect, elements[j].getBoundingClientRect()))
                    return elements[j];
                    
            return undefined;
        };
        
        function findNearestFreeDock(toContainer) {
            var startIndex = 0;
            for (var i = 0; i < containers.length; i++)
                if (containers[i] === toContainer)
                    startIndex = i;
            
            var index = startIndex;
            var toggle = 1;
            var leftOk = true;
            var rightOk = true;
            
            while (leftOk || rightOk) {
                leftOk = (index >= 0);
                rightOk = (index < containers.length);
                if (leftOk && rightOk && toContainer !== containers[index]) {
                    var container = containers[index];
                    
                    var isEmpty = true;
                    for (var j = 0; isEmpty && j < elements.length; j++)
                        if (sameTopLeft(container.getBoundingClientRect(), elements[j].getBoundingClientRect()))
                            isEmpty = false;
                    
                    if (isEmpty)
                        return container;
                }
                /* change index going once left and once right */
                index += (toggle % 2) ? toggle : (-toggle); 
                toggle++;
            }
            return undefined;
        };
        
        function moveToDock(container, element) {
            /* check if there is an element inside container */
            var currentlyDockedElement = getDockedElement(container, element);
            var firstFreeDock;
            if (currentlyDockedElement && (firstFreeDock = findNearestFreeDock(container)))
                move(firstFreeDock, currentlyDockedElement);  /* move this to a free container */
            
            move(container, element);
        };
        
        function move(container, element) {
            /* animate moving */
            element.style.transition = "left 0.7s, top 0.7s";
            setTimeout(function() { element.style.transition = ""; }, 700); /* after delay remove animation to enable normal drag */
            
            /* move to container */
            var containerRect = container.getBoundingClientRect();
            var scrollTop  = document.documentElement.scrollTop;
            var scrollLeft = document.documentElement.scrollLeft;
            element.style.left = (scrollLeft + containerRect.x)+"px";
            element.style.top  = (scrollTop + containerRect.y)+"px";
        };
        
        /* initialize animations */
        function initialize() {
            for (var j = 0; j < elements.length; j++) {
                var element = elements[j];
                element.style.position = "absolute";
                move(element.parentElement, element);
            }
        };
                
        document.addEventListener(
            "mouseup",
            function() {
                if ( ! draggedElement )
                    return;
                
                var overlapAreas = [];
                for (var i = 0; i < containers.length; i++) {
                    var containerRect = containers[i].getBoundingClientRect();
                    var draggedElementRect = draggedElement.getBoundingClientRect();
                    overlapAreas[i] = intersectionArea(containerRect, draggedElementRect);
                }
                
                var maxOverlapIndex = indexOfMaximum(overlapAreas);
                if (maxOverlapIndex >= 0)
                    moveToDock(containers[maxOverlapIndex], draggedElement);
                
                draggedElement = undefined;  /* release variable */
            },
            true);
        
        initialize();
        
        return initialize;
    };
    
    window.addEventListener("load", function() {
        var initialize = moveToWhenOverDock(document.getElementsByClassName("dock-container"), dockElements);
        window.addEventListener("resize", initialize);
    });

This makes the element residing where you drop go where you dragged from, i.e this implements the swap.

The intersectionArea() function calculates the intersection of two rectangles. It will be used to decide where the dropped rectangle should go when it hovers several cells.

The sameTopLeft() function will be used to find the element currently being docked to a container, this won't always be the container's HTML-child. The element will be found by its top-left coordinate that must sit on the one of the container. I needed to use a tolerance DEVIATION here, may be still buggy.

The indexOfMaximum() function is used to find the biggest overlap area on drop.

The getDockedElement() function finds the element sitting on a given dock-container, if any. It refers to the module-parameter elements that contains all dock-elements.

The findNearestFreeDock() function does a tricky thing. It iterates the module-parameter containers by jumping incrementally left and right of the initial point, so that it always finds the nearest free dock-container (as you can drag and drop elements outside, there could be several free containers).

The moveToDock() function will move a dock-element to a container, and move the element currently being there, if any, to the nearest free dock-container.

The move() function moves a given element to a given container. First it sets an animation onto the element. Then it sets a timeout that will remove the animation after the defined transition interval of 0.7 seconds. Without this removal, any drag-action would also be animated and thus quite slow!
The calculation of the target location uses the predefined getBoundingClientRect(), and it also considers the current scroll position of the page's viewport.

The initialize() function sets all dock-elements absolutely positioned. The top and left CSS properties will need page-absolute coordinates then. Then it moves any of them to their current place, which is needed to initialize the animation. This will be called on page-ready, and on any resize-event.

Finally a mouse-up event listener gets installed. Here the decision is done to which container a dropped element should go, by finding the maximum intersection area out of all overlapped dock-containers.

The moveToWhenOverDock() function now calls initialize() and returns that function. The caller will use this return as resize-event callback function.

moveToWhenOverDock() must be called when all page coodinates can be calculated by the browser, so we defer its execution to the page-load event.

All Source Code

That's it! For trying out, expand the section below and copy the complete example HTML.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
<!DOCTYPE HTML>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Metro Layout</title>
    
    <style> /* necessary CSS classes for docking */
        :root
        {
            --dock-element-width: 10em; /* CSS variables */
            --dock-element-height: 6em; /* to bind dock-element to grid dimension */
        }
        .dock-dimension
        {
            width:  var(--dock-element-width); /* fixed dimension for all swapable rectangles */
            height: var(--dock-element-height);
        }
        .dock-container /* would be dispensable here, but not in HTML */
        {
        }
        .dock-element
        {
            background-color: #FFFF99; /* would be transparent else */
            box-sizing: border-box; /* needed when having a border */
            border: 1px solid gray; /* to better see the element when dragged */
        }
    </style>
    
    <style> /* container grid */
        .table
        {
            display: table;
            width: calc(var(--dock-element-width) * 3); /* grid contains 3 columns */
            height: calc(var(--dock-element-height) * 3); /* grid contains 3 rows */
        }
        .table > div
        {
            display: table-row;
        }
        .table > div > div
        {
            display: table-cell;
        }
    </style>
    
    <style> /* displaying the grid centered */
        .centering-container
        {
            display: flex;
            justify-content: center;
            align-items: center;
            
            padding: 1em;
        }
    </style>
    
</head>

<body>
    <div class="centering-container">
    
        <div class="table">
            <div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">1</div>
                </div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">2</div>
                </div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">3</div>
                </div>
            </div>
            
            <div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">4</div>
                </div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">5</div>
                </div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">6</div>
                </div>
            </div>
            
            <div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">7</div>
                </div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">8</div>
                </div>
                <div class="dock-container dock-dimension">
                    <div class="dock-element dock-dimension">9</div>
                </div>
            </div>
            
        </div>
    </div>

    <!---------------------------------------------->

    <script>
    
    var zIndex = 1;
    var draggedElement;
    
    
    function moveWhenDragged(element)
    {
        var relativeOffset;

        element.addEventListener('mousedown', function(event) {
                if (event.button === 0) {  /* is left mouse button */
                    relativeOffset = {     /* remember relative click coordinates */
                        x: event.clientX - element.offsetLeft,
                        y: event.clientY - element.offsetTop 
                    };
                    element.style.zIndex = zIndex;  /* move to top */
                    zIndex++;
                }
            },
            true);
        
        document.addEventListener('mouseup', function() {
                relativeOffset = undefined;
            },
            true);
        
        document.addEventListener('mousemove', function(event) {
                if (relativeOffset !== undefined) {
                    event.preventDefault();
                    
                    element.style.left = Math.round(event.clientX - relativeOffset.x)+"px";
                    element.style.top  = Math.round(event.clientY - relativeOffset.y)+"px";
                    
                    draggedElement = element;
                }
            },
            true);
    };
        
    var dockElements = document.getElementsByClassName("dock-element");
    for (var i = 0; i < dockElements.length; i++)
        moveWhenDragged(dockElements[i]);
    
    
    function moveToWhenOverDock(containers, elements)
    {
        function intersectionArea(r1, r2) {
            var overlapX = Math.max(0, Math.min(r1.right, r2.right) - Math.max(r1.left, r2.left));
            var overlapY = Math.max(0, Math.min(r1.bottom, r2.bottom) - Math.max(r1.top, r2.top));
            return overlapX * overlapY;        
        };
        
        function sameTopLeft(containerRect, elementRect) {
            var DEVIATION = 2;
            var top1 = Math.round(containerRect.top);
            var top2 = Math.round(elementRect.top);
            var left1 = Math.round(containerRect.left);
            var left2 = Math.round(elementRect.left);
            return Math.abs(top1 - top2) <= DEVIATION && Math.abs(left1 - left2) <= DEVIATION;
        };
        
        function indexOfMaximum(array) {
            var max = 0;
            var maxIndex = -1;
            for (var i = 0; i < array.length; i++)
                if (array[i] > max)
                    max = array[maxIndex = i];
            return maxIndex;
        };
        
        function getDockedElement(container, excludedElement) {
            var containerRect = container.getBoundingClientRect();
            for (var j = 0; j < elements.length; j++)
                if (elements[j] !== excludedElement && sameTopLeft(containerRect, elements[j].getBoundingClientRect()))
                    return elements[j];
                    
            return undefined;
        };
        
        function findNearestFreeDock(toContainer) {
            var startIndex = 0;
            for (var i = 0; i < containers.length; i++)
                if (containers[i] === toContainer)
                    startIndex = i;
            
            var index = startIndex;
            var toggle = 1;
            var leftOk = true;
            var rightOk = true;
            
            while (leftOk || rightOk) {
                leftOk = (index >= 0);
                rightOk = (index < containers.length);
                if (leftOk && rightOk && toContainer !== containers[index]) {
                    var container = containers[index];
                    
                    var isEmpty = true;
                    for (var j = 0; isEmpty && j < elements.length; j++)
                        if (sameTopLeft(container.getBoundingClientRect(), elements[j].getBoundingClientRect()))
                            isEmpty = false;
                    
                    if (isEmpty)
                        return container;
                }
                /* change index going once left and once right */
                index += (toggle % 2) ? toggle : (-toggle); 
                toggle++;
            }
            return undefined;
        };
        
        function moveToDock(container, element) {
            /* check if there is an element inside container */
            var currentlyDockedElement = getDockedElement(container, element);
            var firstFreeDock;
            if (currentlyDockedElement && (firstFreeDock = findNearestFreeDock(container)))
                move(firstFreeDock, currentlyDockedElement);  /* move this to a free container */
            
            move(container, element);
        };
        
        function move(container, element) {
            /* animate moving */
            element.style.transition = "left 0.7s, top 0.7s";
            setTimeout(function() { element.style.transition = ""; }, 700); /* after delay remove animation to enable normal drag */
            
            /* move to container */
            var containerRect = container.getBoundingClientRect();
            var scrollTop  = document.documentElement.scrollTop;
            var scrollLeft = document.documentElement.scrollLeft;
            element.style.left = (scrollLeft + containerRect.x)+"px";
            element.style.top  = (scrollTop + containerRect.y)+"px";
        };
        
        /* initialize animations */
        function initialize() {
            for (var j = 0; j < elements.length; j++) {
                var element = elements[j];
                element.style.position = "absolute";
                move(element.parentElement, element);
            }
        };
                
        document.addEventListener(
            "mouseup",
            function() {
                if ( ! draggedElement )
                    return;
                
                var overlapAreas = [];
                for (var i = 0; i < containers.length; i++) {
                    var containerRect = containers[i].getBoundingClientRect();
                    var draggedElementRect = draggedElement.getBoundingClientRect();
                    overlapAreas[i] = intersectionArea(containerRect, draggedElementRect);
                }
                
                var maxOverlapIndex = indexOfMaximum(overlapAreas);
                if (maxOverlapIndex >= 0)
                    moveToDock(containers[maxOverlapIndex], draggedElement);
                
                draggedElement = undefined;  /* release variable */
            },
            true);
        
        initialize();
        
        return initialize;
    };
    
    window.addEventListener("load", function() {
        var initialize = moveToWhenOverDock(document.getElementsByClassName("dock-container"), dockElements);
        window.addEventListener("resize", initialize);
    });
    
    </script>

</body>
</html>