4

I am trying to use Javascript to emulate the CSS :target pseudo-class so as to capture all events that result in an element on page being targeted. I've identified 3 trigger events:

  1. window.location.hash already targets an element of the same ID on initialisation
  2. An anchor targeting the element is clicked
  3. The hashchange event is fired independently of the above (for example via the window.history API)

Scenario 2 is important as a distinct case since I would want to invoke the click event's preventDefault. The simplified code for this scenario follows:

$('body').on('click', 'a[href*=#]', function filterTarget(clickEvent){
    $(this.hash).trigger('target', [clickEvent]);
});

The problem comes when trying to implement scenario 3:

$(window).on('hashchange', function filterTarget(hashChangeEvent){
    $(this.hash).trigger('target', [hashChangeEvent]);
});

If a target handler doesn't cancel the native behaviour for scenario 2, it will be triggered again when the native behaviour causes the resulting hashchange event. How can I filter out these edge cases?

POST-SOLUTION EDIT:

roasted's answer held the key — handle a namespaced hashchange event, then unbind and rebind the handler based on logic handled inside the click handler and its preventDefault. I wrote up the full plugin here.

I. J. Kennedy
  • 24,725
  • 16
  • 62
  • 87
Barney
  • 16,181
  • 5
  • 62
  • 76
  • Could you give an example of how a user might use the system you're describing? – Samuel Reid Jul 03 '13 at 15:31
  • @SamuelReid I'm working on a rich single-page web app with a strong focus on progressive enhancement: all interactions and navigations that are conceptually feasible for user agents without Javascript are handled with form elements, and (most often) links — a lot of the time hashes are used to navigate to or reveal elements within the page. But when Javascript is applied, I would often want to interrupt those events and replace native behaviour. – Barney Jul 03 '13 at 15:33
  • @SamuelReid for a specific example: there is something like a 'footnotes viewer' that displays extensive citations. Native behaviour is to simply jump to the citation in question, but enhanced behaviour opens up a modal interface to better interact with citations and source material references holistically. I want people to be able to share permalinks to footnotes, but if JS is available I still want the 'footnotes viewer' where possible. – Barney Jul 03 '13 at 15:36
  • So you need it so that if scenario 2 does not cancel the default event, scenario 3 will run? – Samuel Reid Jul 03 '13 at 15:51
  • @SamuelReid no — I'm saying that a hashchange resulting from a click should not trigger another target event, because in holistic terms that targeting event has already been captured. – Barney Jul 03 '13 at 16:19

3 Answers3

5

If i understand it, you don't want the hashchange event to be fired if an anchor tag is clicked. You could then set your logic using namespaced events:

DEMO

$('body').on('click', 'a[href*=#]', function (clickEvent) {
    filterTarget(clickEvent,this);  
    $(window).off('hashchange.filter').on('hashchange.tmp', function () {
          $(this).off('hashchange.tmp').on('hashchange.filter', filterTarget);
    });
});
$(window).on('hashchange.filter', filterTarget);

function filterTarget(event,elem) {
    $(elem?elem.hash:window.location.hash).trigger('target', [event]);
    //you could filter depending event.type
    alert(event.type + '::'+ (elem?elem.hash:window.location.hash));
}
A. Wolff
  • 74,033
  • 9
  • 94
  • 155
  • This is pretty much exactly what I want — the event nesting and chaining threw me but now that I've got time to work through it, I'm convinced namespace filtering is the solution. I'm going to extend the captured `clickEvent`'s `preventDefault` to re-instate the `hashchange` handler and post results when I've got it sorted. – Barney Jul 30 '13 at 16:47
  • Sorry for not awarding the bounty sooner — I was waiting til I found the time to srite up my completed script — but the point is your solution works absolutely beautifully and the result is doing wonders for me. Thanks! – Barney Aug 02 '13 at 17:11
1

if the click is setting the hash with the fragment anyway, just throw away duplicates in the hash change event:

onhashchange=function(e){
  if(e.newURL == e.oldURL ){return; }
 //do your normal hashchange event stuff below:

};

ref: https://developer.mozilla.org/en-US/docs/Web/API/window.onhashchange

this fixes cascade issues no matter what invoked the change.

dandavis
  • 16,370
  • 5
  • 40
  • 36
  • `hashchange` deals with this implicitly, in that the theoretical event where `e.newURL == e.oldURL` wouldn't trigger a `hashchange` (there's no change). In the context of the actual `hashchange` event I do want to stop (where the `e.newURL != e.oldURL`), I need to know whether the root cause was a link click or not — which would already have triggered a `target` event. – Barney Jul 30 '13 at 14:54
0

Seems like you could use mousedown instead of click, if you're going to be calling preventDefault on it. Then presumably the hashchange would not be triggered.

vaporbook
  • 275
  • 1
  • 7
  • Thanks for the suggestion. The challenge lies in not firing `target` once based on the click event and then again based on `hashchange`: In effect, I only want `target` to fire as a result of a `hashchange` if that `hashchange` is not itself the result of a `click` (which will already have triggered the `target` event once). – Barney Jul 29 '13 at 13:54