Jump to content

HTML DOM Event capturing


setun-90

Recommended Posts

Hello all,

 

I have an SPA with a UI element akin to a directory tree (the grey column on the left) which allows the users to navigate the site's main content:

post-199509-0-30657600-1465675502_thumb.png

The base of the code is implemented in the following HTML/CSS:

<ul id="nav">
	<li class="i">
		<a href="Services">Services</a>
		<ul class="d">
			<li>
				<a href="Services/Certificates">Certificates</a> </li>
			<li class="i">
				<a href="Services/Responsabilities">Responsabilities</a>
				<ul class="d">
					<li>
						<a href="Services/Responsabilities/Transportation">Transportation</a> </li>
					<li>
						<a href="Services/Responsabilities/Hospitality">Hospitality</a> </li></ul></li></ul></li>
	<li class="i">
		<a href="Encyclopedia">Encyclopedia</a>
		<ul class="d">
			<li>
				<a href="Encyclopedia/Society">Society</a> </li>
			<li>
				<a href="Encyclopedia/Arts">Arts</a> </li>
			<li>
				<a href="Encyclopedia/Technology">Technology</a> </li></ul></li>
	<li class="i">
		<a href="Activities">Activity</a> </li></ul>
.i,.i>.d {position:relative}

.i>.d {display:none}

.i:hover>.d {display:inherit}

The <ul>s behave as intended on hover; the objective here is to work out a solution which will override '.i>.d {display:none}' when the user clicks on an <a>. That way, the user's current location in the website will be evident just from the links which are visible - like the left pane of File Manager on Windows.

 

Now, since CSS doesn't have any parent selectors, I must resort to using JavaScript Event Listeners to listen for clicks on an <a> and style the relevant parents accordingly. To update the displayed location on the click of an <a>, all styles in #nav must be reset to default, and then the parents of the <a> that was clicked on must be styled through event propagation.

 

It turns out that the capturing mode is best because then I can implement a clearing routine on the #nav, and the relevant '.i's and '.d's will get the event and handle it, bug-free. Only problem is, AFAIK, only event listeners which are added dynamically can catch events in the capture phase. This means I must do a big:

'use strict';
var nav=document.getElementById('nav'),
    navd=nav.getElementsByClassName('d');

/* event listeners defined here */

nav.addEventListener(reset,'click',true);
for (i=navd.length; i-- navd[i].addEventListener(set,'click',true);

in the global scope of a loaded and deferred .js script - which works perfectly, but is just plain ugly.

 

Is there any alternative which would allow me to do this statically (i.e. embedded in the HTML)? Or I can't do any better?

 

P.S. Don't mind the weird content and don't google it - it's not online yet.

 

P.P.S. I posted it here because the issue is with DOM event handling and propagation; if I should put this under scripting or some other topic, please let me know.

 

P.P.P.S. Sorry for the long post - here's a potato

potato.jpg

Edited by setun-90
Link to comment
Share on other sites

What exactly is contained in your set() and reset() functions? I'm not entirely sure why capturing is important here.

 

It's always better to not embed Javascript directly in the HTML. Using addEventListener() is better than using event attributes.

Link to comment
Share on other sites

function set() {this.style.display="inherit";}
function reset() {for (let i=navd.length; i-- navd[i].style.display="";}

The style.display="inherit" will be replaced eventually with something a bit more elaborate, but the principle will remain the same: the function just overrides the .i>.d {display:none} rule. reset is the whole reason why capturing is better than bubbling: with capturing, reset will be executed first; with bubbling, set will be executed first, and then reset will undo everything, forcing me to add logic to find the previous tree in some way, then clear it, etc.

 

And yes, I understand that event listeners shouldn't be added inline, but I don't think adding listeners everytime the page loads will be scalable once the tree reaches 1,000+ pages, even though it is a SPA.

 

P.S. I realized an optimization to put once the tree grows large: reset could scan the tree for anything with the style attribute set (the active dir), and then selectively clear it, instead of going through the whole class set.

Edited by setun-90
Link to comment
Share on other sites

I don't think you need to worry about scalability. I'm quite sure Javascript can handle assigning events to thousands of nodes without too much trouble.

 

You shouldn't worry about optimizing code until you're actually encountering slowdowns.

 

This code will do what you want without relying on event capturing.

var items = document.getElementById("nav").getElementsByClassName("d");
var numItems = items.length; // For efficiency
for(var i = 0; i < numItems; i++) {
  items[i].addEventListener("click", toggleAll, false);
}

function toggleAll(e) {
  // Hide all other items
  for(var i = 0; i < numItems; i++) {
    items[i].style.display = "";
  }
  // Show this item
  e.currentTarget.style.display = "inherit";
}
Link to comment
Share on other sites

function toggleAll(e) {
  // Hide all other items
  for(var i = 0; i < numItems; i++) {
    items[i].style.display = "";
  }
  // Show this item
  e.currentTarget.style.display = "inherit";
}

Uh, from what I see, the for loop will undo what the e.currentTarget.style.display = "inherit" from the previous invocation of toggleAll did. Is there anything I missed?

 

And thanks for the code, but I was just wanting to see if there were any static alternatives. The code I wrote works fine already, I was just wanting to know if there was an alternative I didn't consider.

Link to comment
Share on other sites

The toggleAll function is only called when an element is clicked. The instructions are written in order:

1. Hide all elements

2. Then show the element we want shown.

 

The reason this toggleAll function is better than what you have written is that it does not rely on the event capturing and bubbling phase. If you really want, you can embed it "statically" in your HTML like this:

<ul class="d" onclick="toggleAll(event)">

Which I would advise against, but that's what your requirement is.

 

To embed it in the HTML you just have to move the global variables into the function:

function toggleAll(e) {
  // Reference to elements
  var items = document.getElementById("nav").getElementsByClassName("d");
  var numItems = items.length; // For efficiency

  // Hide all other items
  for(var i = 0; i < numItems; i++) {
    items[i].style.display = "";
  }
  // Show this item
  e.currentTarget.style.display = "inherit";
}
Link to comment
Share on other sites

Wow. The results aren't what I predicted.

 

Yours does work... but not lists more than two levels down. Those ones have a cleared style attribute (cleared, but extant), which is only partly what I was predicting.

 

Next, I reverted to my old functions, but forgot to change the 'false' to 'true' (I used commenting to monkey patch the code)... and it still works the way it used to!

 

Now that I took a closer look, it actually makes sense. In reality, there is only ONE event listener that needs to be in the capturing phase; that one is the reset() function which goes on the #nav. The only thing that matters is that reset() happen before set().

 

Wow, that was quite a revelation. If not for you code (which seemed a good attempt nevertheless), thanks for prompting a reflection on this. :)

Edited by setun-90
Link to comment
Share on other sites

Your reset and set functions always run at the same time, so putting the code from both of them into one function does the same thing.

 

The reason it doesn't work for levels of nesting is because it has not been designed that way, the "inherit" value is going to interfere. Generally I start all the list items visible (in case the user does not have Javascript enabled), then hide them. What you're doing is starting them all off hidden and then displaying them.

 

Normally I write my code like this, but that requires taking "display:none" out of your stylesheet.

function toggleAll(e) {
  // Reference to elements
  var items = document.getElementById("nav").getElementsByClassName("d");
  var numItems = items.length; // For efficiency

  // Hide all other items
  for(var i = 0; i < numItems; i++) {
    items[i].style.display = "none";
  }
  // Show this item
  e.currentTarget.style.display = "";
}
Link to comment
Share on other sites

Generally I start all the list items visible (in case the user does not have Javascript enabled)

That's interesting, I was going to solve that problem later. I'll keep that in mind.

 

And I don't think the set and reset functions run at the same time; in fact, the whole principle is that 'reset' run before 'set'. I'm exploiting event propagation to achieve this sequencing.

Link to comment
Share on other sites

They run at the same time (on the same click event), but in sequence, you can still put both of them inside one function as long as they're in order.

What you're doing is no different than this:

element.addEventListener("click", bothActions, false);
function bothActions() {
  reset();
  set();
}

And you can further improve efficiency by just removing those function calls and putting the code right into the parent function:

element.addEventListener("click", bothActions, false);
function bothActions() {
  // reset() code pasted here
  // set() code pasted here
}

Once you've done that you end up with code that's practically the same as mine.

Link to comment
Share on other sites

Okay, I'm starting to think we have different understandings of 'at the same time'. I understand 'at the same time' as a synonym for 'concurrently'. To me, since set is called after reset, they execute in sequence. You seem to be saying 'at the same time' in the sense of 'in the same event propagation'.



They run at the same time (on the same click event), but in sequence, you can still put both of them inside one function as long as they're in order.

What you're doing is no different than this:

element.addEventListener("click", bothActions, false);
function bothActions() {
  reset();
  set();
}

Uhh... do you mean "what I'm doing" as in "what I'm doing with my current code", or "what I'm doing if I follow your advice"? Because I don't see how this:

nav.addEventListener('click',reset,true);
for (i=navd.length; i-- navd[i].addEventListener('click',set,true);

is equivalent to this:

navd[i].addEventListener('click',bothActions,false); // assuming you meant 'element' as 'navd[i]'
function bothActions() {
    reset();
    set();
}

In the first case, reset gets attached only once, whereas in the second case, reset gets bound to all the elements of navd. If I interpreted what you wrote correctly, then I don't understand your reasoning at all. Sorry.

 

In fact, it seems like you didn't understand what I was doing with reset and set. reset runs only once, while set runs once for each level. Was there something else I should have said?

Edited by setun-90
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...