The following code makes jQuery trigger a new target
event on elements when they become the target of the fragment identifier.
For a very basic example, the following target
event handler:
$('.question-page .answer').on('target', function highlight(){
$(this).stop().css({'background':'orange'}).animate({'background':'white'}, 300);
});
…Would emulate what following CSS would express with the :target
pseudo-selector:
.question-page .answer:target {
animation: 0.3s highight;
}
@keyframes highlight {
from {
background: orange;
}
to {
background: white;
}
}
The plugin code follows. My concerns are mostly on the sanity of the event triggering, particularly the handleHashChange
and ignoreHashChange
functions which attempt to mitigate a click
triggering a target
event for an element, and that same click
bubbling up to cause a hashchange
(which without this code, would trigger another target
event — although it is holistically the same target
event).
I'm also interested in style and integration with jQuery API — I have doubts as to whether passing the originalEvent
as a second argument is a good idea or not (should I just fold that event's preventDefault
into the target
event?), particularly in the case of the fake ready event constructor.
Any other criticism more than welcome.
void function jQueryTargetEventClosure($){
// Returns a 'ready' event to pass when 'target' is triggered by initial hash state on document load:
// Prevents critical failure when attempting to invoke event methods on 'target' handlers
var readyEventConstructor = (function readyEventConstructorClosure() {
var history = window.history;
var historySupport = history.replaceState && typeof history.replaceState === 'function';
return function readyEventConstructor() {
var location = window.location;
var readyEvent = $.Event( 'ready' );
// In case of history support, allow preventDefault to remove the window location's hash
if( historySupport && location.hash ){
readyEvent.preventDefault = function preventDomReadyTargetDefault() {
history.replaceState( void 0, void 0, location.href.split( location.hash )[ 0 ] );
// ...but then hand over to jQuery's own preventDefault for internal statefulness etc
return ( $.Event.prototype.preventDefault || $.noop ).call( readyEvent );
};
}
return readyEvent;
};
}());
// Utility: removes the hash from any passed URI(-component)-like string
function unHash( uriString ) {
// String.prototype.link converts a string into an anchor, allowing us to use the DOM's URI abstraction properties
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/link
var link = $( ''.link( uriString ))[ 0 ];
// Splitting with an empty string (no hash) is not what we want. Replace falsy with null:
return link.href.split( link.hash || void 0 )[ 0 ];
}
// Hashchange event handlers:
// Alternate to prevent duplicates (otherwise a click will bubble to trigger a hashchange - creating another target)
function filterHashChangeTarget( hashChangeEvent ) {
var $subject = $( window.location.hash );
$subject.trigger( 'target', [ hashChangeEvent ]);
}
// Bind the above handler
function handleHashChange( hashChangeEvent ) {
$( window )
.off( 'hashchange.ignore' )
.on( 'hashchange.handle', filterHashChangeTarget );
}
// Unbind the next instance
function ignoreHashChange( hashChangeEvent ){
$( window )
.off( 'hashchange.handle' )
.on( 'hashchange.ignore', handleHashChange );
}
// For link clicks
$( 'body' ).on( 'click', 'a[href*=#]', function filterClickTarget( clickEvent ) {
var link = this;
// The hash is effectively a selector for the targetted element
var $subject = $( link.hash );
void function handlePropagation(){
// noop, in case preventDefault is not available
var originalPreventDefault = clickEvent.preventDefault || $.noop();
// Don't handle the next hash change
ignoreHashChange();
// ...Unless default's prevented
clickEvent.preventDefault = function hashChangeInterrupt(){
// Reinstate the hash change handler
handleHashChange();
return originalPreventDefault();
};
}();
// Only apply to in-page links: minus the hash, link & location must match
if ( unHash( link.href ) === unHash( window.location.href )) {
$subject.trigger( 'target', [ clickEvent ]);
}
});
// On DOM ready
$(function readyTargetCheck(){
$( window.location.hash ).trigger( 'target', readyEventConstructor() );
});
}(jQuery);