Search code examples
javascriptprototypejsdom-events

PrototypeJS Event Registry Issues


There seems to be an issue with the prototype event registry after the 1.7.3 update, I was using prototype_event_registry on the element storage to access click events so that i could replay them.

This is so that I can stop events and optionally resume them based on a callback, everything was working fine, but after looking at the diffs for 1.7.0 and 1.7.3 it seems to be removed?

I know that this was internals and I probably shouldn't have used it in the first place. Anyway, down to my question:

I have updated my code to work with 1.7.3 but it seems extremely hacky to me, is there a better way of doing this?

/**
 * creates a toggling handler for click events taking previous click events into account.
 *
 * w.r.t stopping of a click event, handles cases where the button is a submit or a normal button.
 * in the case of a submit, calling <tt>Event.stop()</tt> should be sufficient as there are no other listeners on the button.
 * however, if a normal button has a handler that calls <tt>save()</tt>, and another handler using client code and calling stop,
 * it will not affect stopping of the event, since <tt>Event.stop</tt> only stops propagation, not other handlers!
 *
 * note that this function will always execute the specified handler before any other defined events.
 *
 * @param {Element} element the element to use for this click event
 * @param {Function} handler the handler to use for this stopping click event, if this handler returns true,
 * all other actions for the click event will be prevented
 * @returns {Element} the element that was supplied as argument
 */
function stoppingClickEvent(element, handler) {
    if (!element) throw 'cannot use method, if element is undefined';

    // assign default handler if none was supplied
    handler = handler || Prototype.emptyFunction;

    if (element.type && element.type === 'submit') {
        element.on('click', function(submitEvent) {
            // call the supplied handler with the current element as context and the event as argument
            // if it returns true, we should stop the event
            var stopEvent = handler.call(element, submitEvent);

            if (stopEvent) {
                // since the element's default action will be to submit a form, we prevent it
                submitEvent.stop();
            }
        });
    } else {
        // prototype 1.7.3 removed support for 'prototype_event_registry', so we need to do multiple hacks here
        // first get the window of the element so we can access the prototype
        // event cache from the correct context (frames)
        var elementDoc = element.ownerDocument;
        var elementWindow = elementDoc.defaultView || elementDoc.parentWindow;

        if (!elementWindow) {
            throw 'cannot access the window object for element ' + element.id;
        }

        // we are dealing with a normal element's click event, so we don't know how many click events have been set up.
        // capture them all so we can decide to call them or not.
        // FIXME: need a better way of doing this
        var registry = elementWindow['Event'].cache[element._prototypeUID || element.uniqueID] || {},
            events = registry['click'] || [];

        // now that we have a copy of the events, we can stop them all and add our new handler
        element.stopObserving('click').on('click', function(clickEvent) {
            // call the supplied handler with the current element as context and the event as argument
            // if it returns true, we should stop the event
            var stopEvent = handler.call(element, clickEvent);

            if (!stopEvent) {
                // the event should not be stopped, run all the original click events
                events.each(function(wrapper) {
                    wrapper.handler.call(element, clickEvent);
                });
            }
        });
    }

    return element;
}

Solution

  • After running with the above code for 3-4 months, I finally decided to revert it. There seems to be a lot of issues, especially when dealing with multiple frames and event handlers on a single page.

    The most prevalent one being, Event.cache for a specific element being undefined.

    This may be due to incorrect handling above, but I highly suspect the new Event framework to be incorrect somehow, since a revert to 1.7.0 completely fixes all the issues I experienced.

    Just for reference, this is the code I'm now using with 1.7.0:

    /**
     * creates a toggling handler for click events taking previous click events into account.
     *
     * w.r.t stopping of a click event, handles cases where the button is a submit or a normal button.
     * in the case of a submit, calling <tt>Event.stop()</tt> should be sufficient as there are no other listeners on the button.
     * however, if a normal button has a handler that calls <tt>save()</tt>, and another handler using client code and calling stop,
     * it will not affect stopping of the event, since <tt>Event.stop</tt> only stops propagation, not other handlers!
     *
     * note that this function will always execute the specified handler before any other defined events.
     *
     * @param {Element} element the element to use for this click event
     * @param {Function} handler the handler to use for this stopping click event, if this handler returns true,
     * all other actions for the click event will be prevented
     * @returns {Element} the element that was supplied as argument
     */
    function stoppingClickEvent(element, handler) {
        if (!element) throw 'cannot use method, if element is undefined';
    
        // assign default handler if none was supplied
        handler = handler || Prototype.emptyFunction;
    
        if (element.type && element.type === 'submit') {
            element.on('click', function(submitEvent) {
                // call the supplied handler with the current element as context and the event as argument
                // if it returns true, we should stop the event
                var stopEvent = handler.call(element, submitEvent);
    
                if (stopEvent) {
                    // since the element's default action will be to submit a form, we prevent it
                    submitEvent.stop();
                }
            });
        } else {
            // we are dealing with a normal element's click event, so we don't know how many click events have been set up.
            // capture them all so we can decide to call them or not.
            var registry = element.getStorage().get('prototype_event_registry') || $H(),
                events = registry.get('click') || [];
    
            // now that we have a copy of the events, we can stop them all and add our new handler
            element.stopObserving('click').on('click', function(clickEvent) {
                // call the supplied handler with the current element as context and the event as argument
                // if it returns true, we should stop the event
                var stopEvent = handler.call(element, clickEvent);
    
                if (!stopEvent) {
                    // the event should not be stopped, run all the original click events
                    events.each(function(func) {
                        func.call(element, clickEvent);
                    });
                }
            });
        }
    
        return element;
    }