maaartinus
maaartinus

Reputation: 46492

How to find out if something in a given div has focus?

Using angularjs, I'm showing a 2-level list like this

- first main item
  - first subitem of the first main item
  - second subitem of the first main item
  - AN EMPTY ITEM AS PLACEHOLDER TO ENTER THE NEXT SUBITEM
- second main item
  - first subitem of the second main item
  - second subitem of the second main item
  - AN EMPTY ITEM AS PLACEHOLDER TO ENTER THE NEXT SUBITEM

In order to save place, I'd like to show the PLACEHOLDER only if anything in the corresponding div has focus, so that there's only one such placeholder. I know that there's ngFocus, but I'd prefer something simpler than creating tons of event handlers. Maybe something like this :

<div ng-focus-model="mainItem.hasFocus" ng-repeat="mainItem in list">
   ... main item line
   ... all subitems
</div>

A unidirectional binding would be sufficient as I don't need to set the focus.

Upvotes: 5

Views: 8035

Answers (4)

Trevor
Trevor

Reputation: 13457

You could use the focus event of the '.parent *' selector to capture any focus events, then loop through each of the parent DIVs and use the :focus JQuery selector to check for child elements with focus, then add a class to the parent DIV and use that class to show/hide the placholder (see this jsfiddle):

$(function(){
    $('.parent *').focus(function(){
        $('.selected').removeClass('selected');
        $('.parent').each(function(index, el){
            (function($el){
                setTimeout(function(){
                    console.log($el.attr('id'));
                    if($el.find(':focus').length){
                        $el.addClass('selected');
                    }    
                });
            })($(el));
        });
    });
});
.parent{
    padding:1rem;
    margin:1rem;
    border:solid 1px green;
}
.selected{
    border:solid 1px red;
}
.parent .placeholder{
    display:none;
}
.parent.selected .placeholder{
    display:block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='parent' id='div1'>
    <input type="text" />
    <div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div2'>
    <input type="text" />
    <div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div3'>
    <input type="text" />
    <div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div4'>
    <input type="text" />
    <div class='placeholder'>Placeholder</div>
</div>

Upvotes: 0

jcz
jcz

Reputation: 903

The problem here is the following; we want to avoid adding event listener to each and every child, but add it only to the parent. The parent will be responsible for taking the appropriate action. The general solution to this, is to use even propagation (delegation). We attach only one listener to the parent, when an event occurs on the child (focus on input element in this example), it will bubble up to the parent and the parent will execute the listener.

Here's the directive:

app.directive('ngFocusModel', function () {
    return function (scope, element) {

      var focusListener = function () {
          scope.hasFocus = true;
          scope.$digest();
      };

      var blurListener = function () {
          scope.hasFocus = false;
          scope.$digest();
      };


      element[0].addEventListener('focus', focusListener, true);
      element[0].addEventListener('blur', blurListener, true);
   };
});

The directive listens for events and accordingly sets the value on scope, so we can make conditional changes.

There are several things to notice here.

focus and blur events don't "bubble", we need to use "event capturing" to catch them. That's why element.on('focus/blur') is not used (it doesn't allow for capture, afaik) but an addEventListener method. This method allows us to specify if the listener will be executed on "event bubbling" or "event capturing" by setting the third argument to false or true accordingly.

We could have used focusin and focusout events which "bubble", unfortunatelly these aren't supported in Firefox (focusin and focusout).

Here's a plunker with the implementation.

Update: It occurred to me that this can be done with pure CSS using the :focus pseudo-class, the only downside is that the placeholder needs to be in proper position (sibling) relative to the input elements. See codepen.

Upvotes: 9

artur grzesiak
artur grzesiak

Reputation: 20348

DEMO

I created a small directive that can be used for what you need:

app.directive('childFocus', function($window){

  var registered = [];

  // observing focus events in single place
  $window.addEventListener('focus', function(event){
    registered.forEach(function(element){
      if(element.contains(event.target)){
        // if element with focus is a descendant of the 
        // element with our directive then action is triggered
        element._scope.$apply(element._scope.action);
      } 
    });
  }, true)

  return {
    scope : {
      action : '&childFocus' // you can pass whatever expression here
    },
    link : function(scope, element){

      // keep track ref to scope object
      element[0]._scope = scope;

      // (probably better would be to register
      // scope with attached element)
      registered.push(element[0]);

      scope.$on('destroy', function(){
        registered.splice(registered.indexOf(element[0]),1);
      }); 
    }
  }
});

Upvotes: 2

SKYWALKR
SKYWALKR

Reputation: 620

Unfortunately the only rock solid way to do what you want is to respond to the focus\blur events on the inputs...that's the only way to get notified.

You could put a hidden input as the first element in each div and put the NgFocus attribute on it but that only works if a user tabs into it.

Upvotes: 2

Related Questions