Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to handle javascript keybindings considering a stacked layer context?

Lets say I have a <body> with a full modal opened. This modal can be closed by pressing [ESC] key. Within this full modal, the user can open another, smaller modal, which can also be closed by pressing [ESC] key. How do you handle the [ESC] key and close the 'higher' layer, preventing the keypress to propagate and close the other layers that are listening to keypress?

I'm expecting a straight answer, using preventDefault or something similar.. I'm not considering to setup some kind of service that makes many checks before deciding which layer should be closed.. To me, this thing should work kinda like clicks events, propagating upwards. Is this doable?

like image 896
enapupe Avatar asked Nov 11 '15 12:11

enapupe


3 Answers

There's no straight .preventDefault() or some black magic which prevents you to block bubbling down an event listener unless you are going to create a new handler everytime you want to attach an event listener specifically for that modal which does not interfere with the other handlers.

Instead of having an array of handlers and lots of event listeners why don't you stack the modals inside an array and then destroy them one by one?

This is the shortest code I can think of, see the comments inside the code for a step-by-step explanation.

// Global variable, we use this to store DOM objects.
var modalStack = [];

// My function to create/open my modals.
function openModal() {
    var modalHTML = '<div id="modal-'+modalStack.length+'" class="modal"><button onclick="openModal()">Open modal '+(modalStack.length+1)+'</button></div>'; // I populate the modal with my stuff and assign it an ID containing the current stack size. 
    document.body.insertAdjacentHTML( 'beforeend', modalHTML ); // Add into the body
    modalStack.push( document.getElementById('modal-'+modalStack.length) ); // And push my DOM object I just created into the array.
}


// My ESC event listener
window.addEventListener('keyup', function(event) {
    var lastIndex = modalStack.length-1; // This gets the last element on the stack.
    if (event.keyCode == 27 && lastIndex >= 0) {
        var thisModal = modalStack[ lastIndex ]; // Just to make sense, I could've called the removeChild directly from the array.
        thisModal.parentNode.removeChild(thisModal); // Destroy the current element.
        modalStack.pop(); // Remove the associated last DOM object from the array.
    }
}, false);

jsFiddle Demo

like image 84
MacK Avatar answered Oct 26 '22 16:10

MacK


We can solve this by using Stack data structure, that can be used to maintain states of the modal windows getting created and removed.

The top-most modal will be the Top/Head of the stack, which will be removed first on escape. Below is the simple demonstration how this can be implemented.

Lastly, there is no need of extra effort to create stack implementation, JavaScript Array has in-built push and pop method and we will make use of those in below implementation.

var Modal = (function() {

    //Just to give different margins for each modal
    var BASE_MARGIN = 20;
    
    //STACK, in this array we will store all modals
    var modalStack = [];
    
    //Creates all DOM and attach it to document's body
    function createModal() {
        var modal = document.createElement('div');
        modal.className = 'modal';
        modal.style.margin = ((modalStack.length + 1) * BASE_MARGIN) + 'px';
        
        var header = document.createElement('div');
        header.className = 'modalHeader';
        header.innerHTML = 'Level-' + (modalStack.length + 1);
        
        var close = document.createElement('div');
        close.className = 'closeModal';
        close.innerHTML = 'X';
        close.addEventListener("click", function() {
            var index = modalStack.indexOf(modal);
            for(var i = modalStack.length - 1; i >= index; i--) {
		var div = modalStack.pop(); 
               //no need of i, because pop will always get the last element
		document.body.removeChild(div);
            }
        });
        
        header.appendChild(close);
	   
        var createModalButton = document.createElement('button');
        createModalButton.className = 'createButton';
        createModalButton.addEventListener("click", createModal);
        createModalButton.innerHTML = "Create Modal";
        
	modal.appendChild(header);
        modal.appendChild(createModalButton);
        
        document.body.appendChild(modal);
        modalStack.push(modal);
    }
    
    /**
    * Should be called on dom-loaded
    */
    function initialize() {
         document.addEventListener("keyup", function(ev) {
		if (ev.keyCode == 27) { // escape key maps to keycode `27`
			var div = modalStack.pop();
			document.body.removeChild(div);
		}
         });
    }
    
    return {
        createModal : createModal,
        initialize: initialize
    }
    
})();
div.modal {
    position: fixed;
    top: 0px;
    bottom: 0px;
    left: 0px;
    right: 0px;
    margin:20px;
    z-index = 100;
    background-color: #fff;
    border: 2px solid #333;
}

button.createButton {
    display: block;
    margin: auto;
    margin-top: 4px;
}

.modalHeader {
    background-color: lightsteelblue;
    border-bottom: 1px solid #555;
    color: white;
    padding: 6px 6px 6px 24px;
}

.closeModal {
    color: red;
    cursor: pointer;
    display: inline-block;
    float: right;
    margin-right: 14px;
}
<html>
<head>
</head>
<body onload="Modal.initialize();">
   <!--    Initial button to create child modals -->
   <input style="margin:50px 200px" class="createButton" 
       type="button" onclick="Modal.createModal();" value="Create Modal" />
</body>
</html>

Since we have control over all the modal windows, we can change other modal's opacity or make the parent modal show/hide based on new window creation or deletion, like shown below

var Modal = (function() {

    //Just to give different margins for each modal
    var BASE_MARGIN = 20;
    
    //STACK, in this array we will store all modals
    var modalStack = [];
    
    //Creates all DOM and attach it to document's body
    function createModal() {
        var modal = document.createElement('div');
        modal.className = 'modal';
        modal.style.margin = ((modalStack.length + 1) * BASE_MARGIN) + 'px';
        
        var header = document.createElement('div');
        header.className = 'modalHeader';
        header.innerHTML = 'Level-' + (modalStack.length + 1);
        
        var close = document.createElement('div');
        close.className = 'closeModal';
        close.innerHTML = 'X';
        close.addEventListener("click", function() {
            var index = modalStack.indexOf(modal);
            for(var i = modalStack.length - 1; i >= index; i--) {
		var div = modalStack.pop(); 
              if(modalStack.length > 0) modalStack[modalStack.length - 1].style.display = 'block';
               //no need of i, because pop will always get the last element
		document.body.removeChild(div);
            }
        });
        
        header.appendChild(close);
	   
        var createModalButton = document.createElement('button');
        createModalButton.className = 'createButton';
        createModalButton.addEventListener("click", createModal);
        createModalButton.innerHTML = "Create Modal";
        
	modal.appendChild(header);
        modal.appendChild(createModalButton);
        
        document.body.appendChild(modal);
        
        
      if(modalStack.length > 0) modalStack[modalStack.length - 1].style.display = 'none';
      modalStack.push(modal);
    }
    
    /**
    * Should be called on dom-loaded
    */
    function initialize() {
         document.addEventListener("keyup", function(ev) {
		if (ev.keyCode == 27) { // escape key maps to keycode `27`
			var div = modalStack.pop();
			document.body.removeChild(div);
            if(modalStack.length > 0) modalStack[modalStack.length - 1].style.display = 'block';
		}
         });
    }
    
    return {
        createModal : createModal,
        initialize: initialize
    }
    
})();
div.modal {
    position: fixed;
    top: 0px;
    bottom: 0px;
    left: 0px;
    right: 0px;
    margin:20px;
    z-index = 100;
    background-color: #fff;
    border: 2px solid #333;
}

button.createButton {
    display: block;
    margin: auto;
    margin-top: 4px;
}

.modalHeader {
    background-color: lightsteelblue;
    border-bottom: 1px solid #555;
    color: white;
    padding: 6px 6px 6px 24px;
}

.closeModal {
    color: red;
    cursor: pointer;
    display: inline-block;
    float: right;
    margin-right: 14px;
}
<html>
<head>
</head>
<body onload="Modal.initialize();">
   <!--    Initial button to create child modals -->
   <input style="margin:50px 200px" class="createButton" 
       type="button" onclick="Modal.createModal();" value="Create Modal" />
</body>
</html>
like image 41
rajuGT Avatar answered Oct 26 '22 18:10

rajuGT


The easiest way I can think pf, using JS only:

function openModalOnTop(modalHtml)
{
  $modalsContainer = document.getElementById("modalscontainer");
  var modalContainerTemplate = document.createElement("div");
  modalContainerTemplate.className = "modal";
  modalContainerTemplate.innerHTML  = modalHtml;
  $modalsContainer.appendChild(modalContainerTemplate);
}

function closeTopModal()
{
  $modalsContainer = document.getElementById("modalscontainer");
  $modalsContainer.lastChild.remove();
}

window.addEventListener('keyup', function(event) {
    if (event.keyCode == 79) {
      console.log("open");
      openModalOnTop("someHTMLHere");
    }
}, false);


window.addEventListener('keyup', function(event) {
    if (event.keyCode == 27) {
      console.log("close");
      closeTopModal();
    }
}, false);
.modal {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(0,0,0,0.2);
}
<div id="modalscontainer"></div>

Open with o key, close with esc key.

like image 37
RobertoNovelo Avatar answered Oct 26 '22 17:10

RobertoNovelo