root/trunk/common/Component.js @ 212

Revision 212, 18.4 kB (checked in by ddavis, 3 years ago)

Fix js errors due to events being delivered during destruction. BugzID: 50121

  • Property svn:keywords set to Id
Line 
1/*
2DOM Component Library - Copyright 2005 Six Apart
3$Id$
4
5Copyright (c) 2005, Six Apart, Ltd.
6All rights reserved.
7
8Redistribution and use in source and binary forms, with or without
9modification, are permitted provided that the following conditions are
10met:
11
12    * Redistributions of source code must retain the above copyright
13notice, this list of conditions and the following disclaimer.
14
15    * Redistributions in binary form must reproduce the above
16copyright notice, this list of conditions and the following disclaimer
17in the documentation and/or other materials provided with the
18distribution.
19
20    * Neither the name of "Six Apart" nor the names of its
21contributors may be used to endorse or promote products derived from
22this software without specific prior written permission.
23
24THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
36 * <br/><br/>
37 * <hr><br/><br/>
38 * Class <code>Component</code> class.<br/><br/>
39 * @object-prop <code>sentinels</code> <code>Array</code>  A collection of
40 *              hidden html input fields that bestow tabbability and modality upon components.<br/>
41 * @object-prop <code>ativatable</code> <code>boolean</code> Whether or not this component could
42 *              be made active -- see below.<br/>
43 * @object-prop <code>ative</code> <code>boolean</code> Whether or not this component is active -- active
44 *              components are managed by <code>App</code>.<br/>
45 * @object-prop <code>modal</code> <code>boolean</code> Whether or not this component is modal
46 *              (is the only part of the application that may be currently interacted with by the user)
47 *               -- modal components are managed by <code>App</code>.<br/>
48 * @object-prop <code>focusableTagNames</code> <code>Object</code>A dictionary of names of html tags that are
49 *              allowed focus under the 'active' state and under modality.  Used for controlling focus.<br/>
50 * <br/><hr/>
51 * Class <code>Modal</code>  The modal pop-up superclass.<br/>
52 * @inherits-from <code>Component</code>.<br/>
53 * <hr/>
54 * Class <code>Transient</code>  A transient modal pop-up, such as a list or color picker.<br/>
55 * @inherits-from <code>Modal</code>.<br/>
56 * @listens-to <code>onkeypress</code> <br/>
57 * <hr/>
58 * Class <code>Modal-Message</code>  A  modal pop-up message, such as a confirm dialog.<br/>
59 * @inherits-from <code>Modal</code>.<br/>
60 * @listens-to <code>onkeypress</code> <br/>
61 * <hr/>
62 * Class <code>ModalMask</code>  The event-blocking mask that preserves the modality of modals
63 *        from clicking outside their boundaries.<br/>
64 * <br/><br/>
65 */
66
67Component = new Class( Observable, Autolayout, {
68    useClosures: false,
69   
70   
71    init: function() {
72        arguments.callee.applySuper( this, arguments );
73        this.initObject.apply( this, arguments );
74        this.initSentinels();
75        this.initEventListeners();
76        this.initComponents();
77    },
78   
79   
80    destroy: function() {
81        arguments.callee.applySuper( this, arguments );
82        this.destroyComponents();
83        this.destroyEventListeners();
84        this.destroyObject();
85    },
86   
87   
88    initObject: function( element ) {
89        this.element = DOM.getElement( element );
90        if( !this.element )
91            throw typeof element == "string"
92                ? "no element: " + element
93                : "no element";
94        this.name = element;
95        this.parent = null;
96        this.components = [];
97        this.sentinels = null;
98        this.active = false;
99    },
100   
101   
102    destroyObject: function( element ) {
103        this.components = null;
104        this.sentinels = null;
105        this.parent = null;
106        this.element = null;
107        this.name = null;
108    },
109   
110   
111    /* events */
112   
113    initEventListeners: function() {
114        this.addEventListener( this.element, "mouseover", "eventMouseOver" );
115        this.addEventListener( this.element, "mouseout", "eventMouseOut" );
116        this.addEventListener( this.element, "mousemove", "eventMouseMove" );
117        this.addEventListener( this.element, "mousedown", "eventMouseDown" );
118        this.addEventListener( this.element, "mouseup", "eventMouseUp" );
119        this.addEventListener( this.element, "click", "eventClick" );
120        this.addEventListener( this.element, "dblclick", "eventDoubleClick" );
121        this.addEventListener( this.element, "contextmenu", "eventContextMenu" );
122        this.addEventListener( this.element, "keydown", "eventKeyDown" );
123        this.addEventListener( this.element, "keyup", "eventKeyUp" );
124        this.addEventListener( this.element, "keypress", "eventKeyPress" );
125        this.addEventListener( this.element, "focus", "eventFocus", true ); // FIXME: why are these capturing?
126        this.addEventListener( this.element, "blur", "eventBlur", true ); // FIXME: why are these capturing?
127        this.addEventListener( this.element, "focusin", "eventFocusIn" );
128        this.addEventListener( this.element, "focusout", "eventFocusOut" );
129    },
130   
131
132    destroyEventListeners: function() {},
133   
134   
135    addEventListener: function( object, eventName, methodName, useCapture ) {
136        if ( !this[ methodName ] || this[ methodName ] === Function.stub ) /* XXX - purge instances of Function.stub */
137            return;
138        DOM.addEventListener( object, eventName, 
139            (this.useClosures
140                ? this.getEventListener( methodName )
141                : this.getIndirectEventListener( methodName ) ),
142            useCapture );
143    },
144
145   
146    removeEventListener: function( object, eventName, methodName, useCapture ) {
147        var listener = this.useClosures
148            ? this.getEventListener( methodName )
149            : this.getIndirectEventListener( methodName );
150        DOM.removeEventListener( object, eventName, listener, useCapture );
151    },
152   
153
154    matchCommand: /(?:^|\s)command-(\S+)(?:\s|$)/,
155
156
157    getMouseEventCommand: function( event, rootElement ) {
158        var ancestors = DOM.getAncestors( event.target, true );
159        var cmdattr = app.NAMESPACE + ":command";
160        for( var i = 0; i < ancestors.length; i++ ) {
161            try {
162                /* check the new NAMESPACE:command attribute first */
163                var result = ancestors[ i ].getAttribute( cmdattr );
164                if ( !result ) {
165                    result = this.matchCommand.exec( ancestors[ i ].className );
166                    if ( result[ 1 ] )
167                        result = result[ 1 ];
168                }
169                if ( result ) {
170                    event.commandElement = ancestors[ i ];
171                    /* foo-bar -> fooBar */
172                    event.command = result.cssToJS(); 
173                    return event.command;
174                }
175                if ( ancestors[ i ] == rootElement )
176                    break;
177            } catch( e ) {}
178        }
179    },
180
181
182    /* event listeners */
183
184    eventMouseDown : function( event ) {
185        if( this.activatable ) 
186            this.activate( event );
187    },
188
189   
190
191    eventFocus: function( event ) {
192        if( this.activatable ) 
193            this.activate( event );
194    },
195
196
197    eventFocusIn: function( event ) {
198        if( this.activatable ) 
199            this.activate( event );
200    },
201   
202   
203    /* layout */
204       
205    reflow: function( event ) {
206        this.applyAutolayouts( this.element );
207        this.reflowComponents( event );
208    },
209   
210   
211    reflowComponents: function( event ) {
212        this.components.forEach( function( component ) { component.reflow( event ); } );
213    },
214   
215   
216    /* components */
217   
218    initComponents: function() {},
219   
220   
221    destroyComponents: function() {
222        this.components.forEach( function( component ) { component.destroy(); } );
223        this.components.length = 0;
224    },
225   
226   
227    addComponent: function( component ) {
228        this.components.add( component );
229        component.parent = this;
230        component.reflow();
231        return component;
232    },
233   
234   
235    removeComponent: function( component ) {
236        this.components.remove( component );
237        component.parent = null;
238        return component;
239    },
240   
241
242    /*  active component functionality (including modality) */
243 
244    activatable: false,
245    modal: false,
246   
247   
248    /**
249     * class <code>Component</code>
250     * Creates the invisible text input elements used as focus sinks for tabbability and modality
251     * (modal <code>div</code> "windows", etc).  These text input elements are arranged in a
252     * specific way so that, for modal components, they may trap tabbing events to keep tab focus
253     * within the modal component.  When it receives focus, the last sentinal 'punts' focus
254     * back to the first sentinel.
255     */   
256    initSentinels: function() {
257        if( !this.activatable || this.sentinels )
258            return;
259       
260        this.sentinels = {
261            captureStart: DOM.createInvisibleInput()
262        };
263
264        this.element.insertBefore( this.sentinels.captureStart, this.element.firstChild );
265       
266        if( !this.modal )
267            return;
268       
269        extend( this.sentinels, {
270            puntStart: DOM.createInvisibleInput(), 
271            captureEnd: DOM.createInvisibleInput(),
272            puntEnd: DOM.createInvisibleInput()
273        } );
274
275        this.element.insertBefore( this.sentinels.puntStart, this.element.firstChild );
276        this.element.appendChild( this.sentinels.captureEnd );
277        this.element.appendChild( this.sentinels.puntEnd );
278    },
279
280
281    /**
282     * class <code>Component</code>
283     * Create new sentinels.  This is necessary in case their html must be destroyed.
284     */
285    refreshSentinels: function() {
286        this.sentinels = null;
287        this.initSentinels();
288    },
289
290
291    /**
292     * class <code>Component</code>     
293     * Activate this component (can be either modal or 'plain' active).
294     */
295    activate: function( event ) {
296        if( !this.activatable ) 
297            return; 
298        if( !window.app.setActiveComponent( this ) ) 
299           return;
300         
301        this.active = true;
302        this.captureFocus( event );
303        DOM.addClassName( this.element, "active-component" );
304        this.broadcastToObserversNB( "componentActivated", this );
305    },
306
307
308    /**
309     * class <code>Component</code>
310     * De-activate this component (can be either modal or 'plain' active).
311     */
312    deactivate: function() {
313        this.active = false;
314        DOM.removeClassName( this.element, "active-component" );
315        this.broadcastToObserversNB( "componentDeactivated", this );
316    },
317   
318
319    focusableTagNames: { // Note: All elements with 'mouse event commands' are focusable (see 'captureFocus').
320        taginput: defined,
321        tagtextarea: defined,
322        tagbutton: defined,
323        tagselect: defined,
324        tagdiv: defined, // FF 1.5+ puts divs in the tabbing order. Added 2006-05-03: See case 27751 and case 34362.
325        taga: defined, // 'anchor' tag.  Added 2006-05-03: See Case 27751.
326        tagoption: defined,
327        tagp: defined // case 36542
328    },
329
330
331    /**
332     * class <code>Component</code>
333     * Capture and manage the focus on an activatable (optionally including modal) component.
334     * The behavior differs for the various types of activatable component.
335     * @param event <code>Event</code> The event that leads to focus (i.e., a tab or a click).
336     */       
337    captureFocus: function( event ) {
338        if ( this.active && !this.modal )
339            return;
340        var tagName = ( defined( event ) && event.target.tagName )
341            ? "tag" + event.target.tagName.toLowerCase()
342            : null;
343        var command;
344        if ( tagName && event ) 
345           command = this.getMouseEventCommand( event ); // Avoid scrolling out from over command elements.
346        // Set the tabbing focus on the sentinel, unless the user wants it on another form or command element:
347        if( !tagName || ( this.focusableTagNames[ tagName ] !== defined ) && !command ) {
348            try {
349                this.sentinels.captureStart.focus();
350                event.stop(); // Force the action to stop at the focus set above.
351                if( defined( this.sentinels.captureStart.focusIn ) )
352                    this.sentinels.captureStart.focusIn();
353            } catch ( e ) {}
354        }
355
356        if( !defined( event ) || !this.modal )
357            return;
358        /*-
359         * As of Firefox 1.5.0.6 at least, bug where key events could not be heard from divs has been fixed.
360         * Workaround code for this put in on 2006-05-03 and removed 2006-08-24.  See case 27751 and case 34362.
361         */
362        try {
363            if( event.target === this.sentinels.puntEnd )
364                this.sentinels.captureStart.focus();
365            else if( event.target === this.sentinels.puntStart )
366                this.sentinels.captureEnd.focus();
367        } catch( e ) {}
368    },
369   
370
371    /* misc */
372   
373    hide: function() {
374        DOM.removeClassName( this.element, "visible" );
375        DOM.addClassName( this.element, "hidden" );
376    },
377   
378   
379    show: function() {
380        DOM.removeClassName( this.element, "hidden" );
381        DOM.addClassName( this.element, "visible" );
382        this.reflow();
383    }
384} );
385
386
387/* modal subclass */
388
389Modal = new Class( Component, {
390    activatable: true,
391    modal: true,
392    isOpen: false,
393   
394    /* execution */
395   
396    open: function( data, callback ) {
397        if ( !this.transitory ) 
398            window.scrollTo( 0, 0 );
399        this.data = defined( data ) ? data : {};
400        this.callback = callback;
401        this.active = false;
402        this.isOpen = true;
403        window.app.addModal( this );       
404    },
405   
406   
407    close: function( data ) {
408        window.app.removeModal( this );
409        this.isOpen = false;
410        if( this.callback )
411            this.callback( data, this );
412        this.callback = null;
413        this.data = null;
414    },
415   
416
417    eventClick: function( event ) {
418        if ( event.shiftKey ) 
419            event.stop();
420    },
421
422   
423    eventKeyPress: function( event ) {
424        switch( event.keyCode ) {
425            case 27:
426                this.close( false );
427        }
428    }
429} );
430
431
432/* transient subclass */
433
434Transient = new Class( Modal, {
435    transitory: true,
436   
437    /* events */
438   
439    /**
440     * Class: <code>Transient</code><br>
441     * This method allows a <ocde>Transient</code> to disappear on keypress of 'esc'..
442     * @param event <code>Event</code>  A prepared (processed by the custom js framework) <code>Event</code> object.
443     */
444    eventKeyPress: function( event ) {
445        switch( event.keyCode ) {
446            case 27:
447                this.close( false ); 
448        }
449    },
450   
451
452    /**
453     * Returns the command from the event, via <code>getMouseEventCommand</code>.
454     * @param event  <code>Event</code> A prepared event object.
455     */
456    eventClick: function( event ) {
457        this.close( this.getMouseEventCommand( event ) );
458        return event.stop();
459    },
460   
461   
462    eventContextMenu: function( event ) {
463        return event.stop();
464    },
465
466   
467    open: function( data, callback, targetElement ) {
468        this.targetElement = targetElement;
469        return arguments.callee.applySuper( this, arguments );
470    }
471} );
472
473
474Component.Delegator = {
475   
476    DEFAULT_NAMESPACE: "core",
477
478    setupDelegates: function( object ) {
479        /* this needs more testing before enabling
480        if ( object && !object.delegateParent )
481            object.delegateParent = this;
482        */
483       
484        if ( !this.delegateListeners )
485            this.delegateListeners = {};
486           
487        if ( !this.delegates )
488            this.delegates = {};
489
490        if ( !defined( this.NAMESPACE ) )
491            this.NAMESPACE = ( window.app && app.NAMESPACE )
492                ? app.NAMESPACE : this.DEFAULT_NAMESPACE;
493    },
494   
495
496    addEventListener: function( object, eventName, methodName, useCapture ) {
497        DOM.addEventListener( object, eventName, 
498            (this.useClosures
499                ? this.getEventListener( methodName )
500                : this.getIndirectEventListener( methodName ) ),
501            useCapture );
502    },
503
504
505    /* delegate functions */
506    setDelegate: function( name, object ) {
507        this.setupDelegates( object );
508        this.delegates[ name ] = object;
509        return object;
510    },
511   
512   
513    setDelegateListener: function( eventName, delegateName ) {
514        this.setupDelegates();
515        this.delegateListeners[ eventName ] = delegateName;
516    },
517   
518   
519    delegateEvent: function( event, eventName ) {
520        var delegate = DOM.getMouseEventAttribute( event, this.NAMESPACE + ":delegate" );
521           
522        if ( !delegate ) {
523            if ( this.delegateListeners && this.delegateListeners.hasOwnProperty( eventName ) )
524                delegate = this.delegateListeners[ eventName ];
525            else
526                return undefined;
527        } else
528            delegate = delegate.cssToJS();
529       
530        if ( this.delegates && this.delegates.hasOwnProperty( delegate ) && this.delegates[ delegate ][ eventName ] )
531            return this.delegates[ delegate ][ eventName ]( event, this );
532    },
533   
534   
535    getIndirectEventListener: function( methodName ) {
536        if( !this.indirectEventListeners )
537            this.indirectEventListeners = {};
538        var method = this[ methodName ];
539        var indirectIndex = this.getIndirectIndex();
540        if( !this.indirectEventListeners[ methodName ] ) {
541            return this.indirectEventListeners[ methodName ] = new Function( "event",
542                "if ( window.indirectObjects === undefined ) return;" +
543                "try { event = Event.prep( event ); } catch( e ) {}" +
544                "var o = window.indirectObjects[" + indirectIndex + "];" +
545                "if ( !o ) return;" +
546                "var r = o.delegateEvent( event, '" + methodName +
547                "' ); if ( r ) return r; if ( o[ '" + methodName +
548                "' ] ) return o['" + methodName + "'].call( o, event );" );
549        }
550       
551        return this.indirectEventListeners[ methodName ];
552    }
553};
Note: See TracBrowser for help on using the browser.