<!DOCTYPE html>
<html>

  <head>
    <link href="style.css" rel="stylesheet" />
    <script data-require="angular.js@1.4.0-beta.3" data-semver="1.4.0-beta.3" src="https://code.angularjs.org/1.4.0-beta.3/angular.js"></script>
    <script src="app.js"></script>
    <script src="trapFactory.js"></script>
  </head>

  <body ng-app="myApp" ng-controller="myCtrl">
    <h1>Trap focus in modal</h1>
    <button ng-click="showModal()">modal</button>
    <br />
    <br />
    <a href="javascript:void();">That's no moon</a>
    <br />
    <br />
    <a href="javascript:void();">Aren't you a little short for a storm trooper?</a>
    
    <div id="modal" ng-if="modalVisible" class="modal">Luke!
      <button ng-click="hideModal()">it's a trap!</button>
      <a href='javascript:void();'>a trap!</a>
    </div>
  </body>

</html>
/* Styles go here */

.modal {
  position: fixed;
  top:25%;
  left:25%;
  width:50%;
  height:50%;
  border:1px solid black;
  background:white;
  padding:10px;
}


    app.factory('trap', function($timeout){

        function onTab(container, elt, goReverse) {
            var $getFocus = getFocusableElementsInModal(container),
                curElt = elt,
                index, nextIndex, prevIndex, lastIndex;

            var focusableArray = [];
            
            //create an array out of the list of focusable elements in the modal
            for(var key in $getFocus){
                if($getFocus.hasOwnProperty(key)){
                  if(parseInt(key) !== NaN){
                    focusableArray.push($getFocus[key]);
                  }
                }
            }
            
            while(elt === elt.ownerDocument.activeElement){

                if(focusableArray.length === 1){ //if the array of elements to tab through is only 1, then just return, all the stuff below just screws things up
                    return true;
                }
                else {
                
                    focusableArray.forEach(function(v, k){
                        if(v == curElt){
                            index = k;
                        }
                    });
                    
                    nextIndex = index + 1;
                    prevIndex = index - 1;
                    lastIndex = $getFocus.length - 1;

                    switch(index) {
                        case -1:
                            return false;
                        case 0:
                            prevIndex = lastIndex;
                            break;
                        case lastIndex:
                            nextIndex = 0;
                            break;
                    }
                                    
                    if (goReverse) {
                        nextIndex = prevIndex;
                    }
                    
                    curElt = focusableArray[nextIndex];
                    try {
                        curElt.focus();
                    } catch(e) {
                    }
                }
            }

            return true;        
        }

        function keepSpecialTabindex(currentEl) {
            return currentEl.tabIndex > 0;
        }

        function keepNormalTabindex(currentEl) {
            return !currentEl.tabIndex; // true if no tabIndex or tabIndex == 0
        }

        function sortFocusable(a, b) {
            return (a.t - b.t) || (a.i - b.i);
        }

        function getFocusableElementsInModal(modal) {
            var $modal = modal;
            var result = [],
                cnt = 0;

            fixIndexSelector.enable && fixIndexSelector.enable();

            var normalFocusable = $modal.querySelectorAll('button, a, link, [draggable=true], [contenteditable=true], [tabindex="0"], input, textarea, select');
            for (var i = 0; i < normalFocusable.length; i++) {
                if(keepNormalTabindex(normalFocusable[i])){
                    result.push({
                        v: normalFocusable[i], // value
                        t: 0, // tabIndex
                        i: cnt++ // index for stable sort
                    });  
                }
            }

            var specialFocusable = $modal.querySelectorAll('[tabindex]');
            for (var i = 0; i < specialFocusable.length; i++) {
                if(keepSpecialTabindex(specialFocusable[i])){
                    result.push({
                        v: specialFocusable[i], // value
                        t: specialFocusable[i].tabIndex, // tabIndex
                        i: cnt++ // index for stable sort
                    });  
                }
            }

            fixIndexSelector.disable && fixIndexSelector.disable();
            
            var sortResult = result.sort(sortFocusable);
            var mappedResult = sortResult.map(function(val) {
                return val.v;
            });   
            
            return mappedResult;
            
        }

        var fixIndexSelector = {};

        return function(element){
            $timeout(function(){//wait until DOM is loaded (or at least the next $digest cycle...ish)
                element.querySelector('button, a, link, [draggable=true], [contenteditable=true], [tabindex], input, textarea, select').focus(); //focus on first element in modal

                element.onkeydown=function(e){
                    if (e.keyCode === 9) {
                        var goReverse = !!(e.shiftKey);
                        if (onTab(this, e.target, goReverse)) {
                            e.preventDefault();
                            e.stopPropagation();
                        }
                    }
                };
            },0);
            
            return element;
        };         
             
	});
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, trap, $timeout) {
  $scope.modalVisible = false;
    
    $scope.showModal = function(){
      $scope.modalVisible = true;
      $timeout(function(){
        var modal = document.getElementById('modal');
        trap(modal);
      },0);
    };
    
    $scope.hideModal = function(){
      $scope.modalVisible = false;
    };
});