views:

196

answers:

1

I would like to be able to delay the default action of an event until some other action has been taken.

What it's for: I'm trying to build a reusable, unobtrusive way to confirm actions with a modal-type dialogue. The key wishlist item is that any javascript handlers are attached by a script, and not written directly inline.

To make this truly reusable, I want to use it on different types of items: html links, checkboxes, and even other javascript-driven actions. And for purely HTML elements like links or checkboxes, I want them to degrade gracefully so they're usable without javascript turned on.

Here's how I would envision the implementation:

<a href="/somelink/" class="confirm">Some Link</a>
_________

<script>
    attachEvent('a.confirm','click', confirmAction.fire)

    var confirmAction = (function(){
     var public = {
      fire: function(e){
       e.default.suspend();
       this.modal();
      },
      modal: function(){
       showmodal();
       yesbutton.onclick = this.confirmed;
       nobutton.onclick = this.canceled;
      },
      confirmed: function(){
       hidemodal();
       e.default.resume();
      },
      canceled: function(){
       hidemodal();
       e.default.prevent();
      }
     }
     return public;
    })()

</script>

I know about the e.preventDefault function, but that will kill the default action without giving me the ability to resume it. Obviously, the default object with the suspend, resume and prevent methods is made up to illustrate my desired end.

By the way, I'm building this using the Ext.Core library, if that helps. The library provides a good deal of normalization for handling events. But I'm really very interested in learning the general principles of this in Javascript.

Thanks!

A: 

To resume, you could try saving the event and re-fire it, setting a flag that can be used to skip the handlers that call suspend() ('confirmAction.fire', in your example).

<a href="/somelink/" class="confirm">Some Link</a>
_________

<script>
function bindMethod(self, method) {
    return function() {
        method.apply(self, arguments);
    }
}
var confirmAction = (function(){
    var public = {
            delayEvent: function(e) {
                if (e.suspend()) {
                    this.rememberEvent(e);
                    return true;
                } else {
                    return false;
                }
            },
            fire: function(e){
                if (this.delayEvent(e)) {
                    this.modal();
                }
            },
            modal: function(){
                    showmodal();
                    yesbutton.onclick = bindMethod(this, this.confirmed);
                    nobutton.onclick = bindMethod(this, this.canceled);
            },
            confirmed: function(){
                    hidemodal();
                    this.rememberEvent().resume();
            },
            canceled: function(){
                    hidemodal();
                    this.forgetEvent();
            },
            rememberEvent: function (e) {
                if (e) {
                    this.currEvent=e;
                } else {
                    return this.currEvent;
                }
            },
            forgetEvent: function () {
                delete this.currEvent;
            }
    }
    return public;
})()

// delayableEvent should be mixed in to the event class.
var delayableEvent = (function (){
     return {
        suspend: function() {
            if (this.suspended) {
                return false;
            } else {
                this.suspended = true;
                this.preventDefault();
                return true;
            }
        },
        resume: function () {
            /* rest of 'resume' is highly dependent on event implementation, 
               but might look like */
            this.target.fireEvent(this);
        }
     };
})();

/* 'this' in event handlers will generally be the element the listener is registered 
 * on, so you need to make sure 'this' has the confirmAction methods.
 */
mixin('a.confirm', confirmAction);
attachEvent('a.confirm','click', confirmAction.fire);
</script>

This still has potential bugs, such as how it interacts with other event listeners.

outis
Thanks for the great response. I had not know it was possible to transfer around the default action via the `e` object.I'm going to build a test case for this as soon as possible and let you know the result.
DanielMason
Note that I accidentally left in a reference to an Event.default property, which doesn't exist in the DOM and I never defined, nor can I think of a way of defining. The code doesn't so much use the default handler as it just re-fires the event and includes some code to prevent the 'fire' listener from performing the modal action.
outis
There was also a bug with how the `confirmed` and `canceled` listeners were registered that caused 'this' to be bound incorrectly. Fixed with the addition of the `bindMethod` function. Ext.Core might have its own version of `bindMethod`.
outis
You could also split the variadic `rememberEvent(...)` into `rememberEvent(e)` and `recallEvent()`. I used a single method only because the word "remember" is ambiguous.
outis
I may have actually misunderstood the intent of some of what you wrote, but it appears that assigning the event object to another variable and then calling it later as you would a function doesn't work. It (correctly) points to an object (the event object). As far as I can tell there's no direct pointer to the actual action anywhere.I'm gonna have to try a different approach, probably some kind of select case to determine what the default action should be, and an additional handler to reproduce that action.
DanielMason
The key is the implementation of `delayableEvent.resume`, in particular the `this.target.fireEvent(this)` line. Note that this doesn't treat the event as a callable, it refires the event. On reflection, `currentTarget` is more appropriate than `target`.
outis