1
\$\begingroup\$

I've got this method in a class that searches the graph of DOM nodes above an element for a click handler attached with Blaze or jQuery (Note that Blaze uses jQuery under the hood).

The class it's attached to fires click events after a delay and displays a visual indicator while waiting. The purpose of this method is to determine if there is a handler at all and therefore if the visual indicator should be displayed.

Because it's a core UI function, it needs to run at (or as close to) 60 fps / in less than 16ms.

I'm a little worried about the nesting of for loops in this thing, and also feel a bit off about the number of comments.

I'm erring on the verbose because I want other developers to feel safe messing with this if the have to later on (eg. to detecting other event types). Also because other devs that have worked here don't generally have the background to know words like memoization

Can this be done more efficiently / cleaned up / etc ?

/**
 * Searches through the path of it's parents to find a Blaze event listener
 * that would fire if the element was clicked.
 *
 * Because Blaze delegates event handlers to the parent element of a
 * template, we need to search through each parent until we find a delegated
 * handler that covers the DOMRange of the element. Then we need to confirm
 * that the handler would fire for the element given the selector given in
 * the EventMap.
 *
 * Because we might have nested click handlers, we need to repeat this
 * search for each element in the path.
 *
 * @param    {HTMLElement}  element - The element to search from
 * @return   {Boolean}
 * @private
 */
_hasClickHandlerInPath (element) {
    // Get memoized result if exists
    const cache = this.__hasClickHandlerInPathCache;
    if ( cache.has(element) ) {
        return cache.get(element);
    }

    // Test for directly attached (not delegated) jQuery handler
    const jData = jQuery._data(element);
    if (jData && jData.events && jData.events.click) {
        for (const handler of jData.events.click) {
            if (handler.selector === undefined) {
                cache.set(element, true);
                return true;
            }
        }
    }

    const elementPath = this._computeEventPath(element);
    for (const currentElem of elementPath) {

        // Get memoized result if exists
        if ( cache.has(currentElem) ) {
            return cache.get(currentElem);
        }

        const currentPath = this._computeEventPath(currentElem);
        for (const parent of currentPath) {
            // Here we look for a click handler in both jQuery's delegated
            // events system as well as Blaze's. Because Blaze handlers
            // scope their events to the DOMRange that the template
            // has authority over, we prefer to check if the element is in
            // the DOMRange of the Blaze handler and use jQuery's stored
            // selector as a fallback when a handler was added outside of
            // Blaze.

            // Look for delegated jquery handler
            const jData = jQuery._data(parent);
            const hasJQueryHander = jData &&
                jData.events &&
                jData.events.click;

            if (!hasJQueryHander) {
                continue;
            }

            // Check if there are also any blaze handlers
            const hasBlazeHandler = parent &&
                parent.$blaze_events &&
                parent.$blaze_events.click &&
                parent.$blaze_events.click.handlers;

            for (const jHandleObj of jData.events.click) {
                let isElemInDOMRange;
                let isElemSelected;

                if (hasBlazeHandler) {
                    const handlers = parent.$blaze_events.click.handlers;
                    for (const bHandleObj of handlers) {
                        // Look for a blaze handler that matches the jQuery
                        // handler
                        if (bHandleObj.delegatedHandler.guid !== jHandleObj.guid) {
                            continue;
                        }
                        // Now check Blaze's DOMRange and selector
                        isElemInDOMRange = bHandleObj.recipient.containsElement(currentElem);
                        isElemSelected = (
                            [...parent.querySelectorAll(bHandleObj.selector)]
                                .includes(currentElem)
                        );
                        break;
                    }
                } else {
                    // If there are no blaze handlers, check the jQuery
                    // selector.
                    isElemSelected = (
                        [...parent.querySelectorAll(jHandleObj.selector)]
                            .includes(currentElem)
                    );
                    // Everything is in range of a jQuery handler
                    isElemInDOMRange = true;
                }

                if (isElemInDOMRange && isElemSelected) {
                    // save memoized result
                    cache.set(currentElem, true);
                    cache.set(element, true);
                    return true;
                }
            }

            // Don't save memoized result, as this can lead to false negatives
        }
    }
    // save memoized result
    cache.set(element, false);
    return false;
}
__hasClickHandlerInPathCache = new WeakMap(); // Memoization store for function

_computeEventPath (element) {
    if (!element || !element.parentElement) {
        return [];
    }
    let pathArr = [element];
    while (element.parentElement) {
        element = element.parentElement;
        pathArr.push(element);
    }
    pathArr.push(document);
    pathArr.push(window);
    return pathArr;
}
\$\endgroup\$

Your Answer

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

Browse other questions tagged or ask your own question.