3
\$\begingroup\$

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);
\$\endgroup\$
  • \$\begingroup\$ No, probably not — you could use a straight transition. The question's about the jQuery plugin. \$\endgroup\$ – Barney Jan 26 '14 at 12:14
2
\$\begingroup\$

to start with a terrible pun; I would avoid void where possible,
history.replaceState( '', '', location.href.split( location.hash )[ 0 ] ); makes more sense to me. Also for your return statements I would rather see

return link.hash ?  link.href.split( link.hash )[ 0 ] : link.href;

Furthermore, it seems that a lot of your code could be replaced with jQuery's event.stopPropagation(), am I missing something?

Other than that the code is very well documented, and teaches the reader some new tricks.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.