Thursday, 13 February 2014

Complete HTML5 portlet management system

Hello,

Last day I had a discussion with one of my friend regarding portlets. He asked me whether it is possible to have a portlet management system completely on the client side. In this article I shall try to find out a client only solution (which you may or may not agree).

Java Portlets, Liferay portlets

You may be already knowing about portlets in Java, JSF and Liferay. JavaScript cannot compete with any of these as it is processed on the server side. JavaScript does not have the power of permanent storage too. The only option left to us is the use of localStorage or file API (which is not yet implemented in many of the browsers).



Portlets using Drag and Drop API and LocalStorage

Copy paste the following code to your favorite editor and open it in the (HTML5 compliant) browser.
<html>
    <title>Complete HTML5 portlet management system</title>
    <style type="text/css">
        header{color:#000;text-shadow:#000 0 1px;font-size:20px;text-align:center}[draggable]{-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;user-select:none;-khtml-user-drag:element;-webkit-user-drag:element}.slot{height:15%;width:95%;background-color:silver;text-align:center;margin:1%}.slot.drgbl{border:3px dashed #00f}.slot.over{border:3px dashed red}.component input,.component select{height:70%;width:50%;border:1% solid #000;border-radius:4%;-webkit-border-radius:4%;-ms-border-radius:4%;-moz-border-radius:4%;margin:1%}.toolbar{background-color:orange;text-align:right}
    </style>
    <script id="tpl1" type="text/template">
        <div id="com1" class="component">
            <select>
                <option>A</option>
                <option>B</option>
            </select>
        </div>    
    </script>
    <script id="tpl2" type="text/template">
        <div id="com2" class="component">
            <input type="submit" 
                    value="Search">        
        </div>        
    </script>
    <script id="tpl3" type="text/template">
        <div id="com3" class="component">
            <input type="email" name="email" required 
                    placeholder="uname@email.com">
        </div>    
    </script>
    <script id="tpl4" type="text/template">
        <div id="com4" class="component">
            <input name="q" placeholder="Go to a Website"/>            
        </div>    
    </script>
    <body>
        <header>Complete HTML5 portlet management system</header>
        <div id="slot1" class="slot" draggable="true"></div>
        <div id="slot2" class="slot" draggable="true"></div>
        <div id="slot3" class="slot" draggable="true"></div>
        <div id="slot4" class="slot" draggable="true"></div>
        <footer class="toolbar">
            <fieldset>
                <button id="rearrange">Rearrange</button>
                <button id="save" disabled="true">Save</button>
            </fieldset>
        </footer>

        <script>
        window.onload = function() {
            var slots = document.querySelectorAll('.slot');
            loadTemplates();
            var dragSrcEl = null;

            function handleDragStart(e) {
                if(!draggable) 
                    return false;
                this.style.opacity = '0.4';  // this / e.target is the source node.
                dragSrcEl = this;
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/html', this.innerHTML);
            }

            function handleDragEnter(e) {
                // this / e.target is the current hover target.
                this.classList.add('over');
            }

            function handleDragOver(e) {
                if (e.preventDefault) {
                    e.preventDefault(); // Necessary. Allows us to drop.
                }
                e.dataTransfer.dropEffect = 'move'; 
                return false;
            }

            function handleDragLeave(e) {
                  this.classList.remove('over');  // this / e.target is previous target element.
            }

            function handleDrop(e) {
                if(!draggable) 
                    return false;
                // this/e.target is current target element.

                if (e.stopPropagation) {
                    e.stopPropagation(); // Stops some browsers from redirecting.
                }

                if (dragSrcEl != this) {
                    dragSrcEl.innerHTML = this.innerHTML;
                    this.innerHTML = e.dataTransfer.getData('text/html');
                }

                return false;
            }

            function handleDragEnd(e) {
                // this/e.target is the source node.
                [].forEach.call(slots, function (slot) {
                    slot.classList.remove('over');
                    slot.style.opacity = 1;
                });
            }

            [].forEach.call(slots, function(slot) {
              slot.addEventListener('dragstart', handleDragStart, false);
              slot.addEventListener('dragenter', handleDragEnter, false)
              slot.addEventListener('dragover', handleDragOver, false);
              slot.addEventListener('dragleave', handleDragLeave, false);
              slot.addEventListener('drop', handleDrop, false);
              slot.addEventListener('dragend', handleDragEnd, false);
            });
            var rearrange = document.getElementById("rearrange");
            var save = document.getElementById("save");
            rearrange.addEventListener('click', rearrangeAction, false);
            save.addEventListener('click', saveAction, false);
            var draggable = false;
            function rearrangeAction(e){
                [].forEach.call(slots, function(slot) {
                    slot.classList.add('drgbl');
                });
                rearrange.disabled = true;
                save.disabled = false;
                draggable = true;                                    
            }

            function saveAction(e) {
                [].forEach.call(slots, function(slot) {
                    slot.classList.remove('drgbl');
                });
                rearrange.disabled = false;
                save.disabled = true;
                draggable = false;
                saveOrder();
            }

            function loadTemplates(){
                [].forEach.call(slots, function(slot) {
                    slot.innerHTML = document.querySelector("#tpl"+getOrder(slot.id)).innerHTML;
                });
            }

            function getOrder(id){
                if(localStorage && localStorage[id]){
                    return localStorage[id].substring(3);    
                } else {
                    return id.substring(4);
                }
            }

            function saveOrder(){
                if(localStorage) {
                    [].forEach.call(slots, function(slot) {
                        localStorage[slot.id] = slot.children[0].id;
                    });                    
                }
            }
        };
        </script>
    </body>
</html>


If you inspect the HTML code, you can see that there are 4 slots which are draggable. During onload event these slots will be filled with the templates based on the order which are saved in localStorage. Each template is a UI component. Elements can be dragged into different slots when user clicks on "rearrange" button. On "save" click, the data is stored onto the LocalStorage in the form of key(slot id) -value(component id) pair.
Now refresh your page or reopen. You can find the UI components in the order you saved.

You can find the same on jsfiffle.

References:


  • https://developer.mozilla.org/en-US/docs/DragDrop/Drag_and_Drop
  • http://www.html5rocks.com/en/tutorials/dnd/basics/


1 comment: