root/trunk/common/App.js

Revision 256, 13.9 kB (checked in by ddavis, 2 years ago)

potentially undef golbal values must be checked against the window

  • Property svn:keywords set to Id
Line 
1/**
2 * App Library - Copyright (c) 2006 Six Apart
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 *     * Redistributions in binary form must reproduce the above
13 * copyright notice, this list of conditions and the following disclaimer
14 * in the documentation and/or other materials provided with the
15 * distribution.
16 *
17 *     * Neither the name of "Six Apart" nor the names of its
18 * contributors may be used to endorse or promote products derived from
19 * this software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 *
33 *
34 * This file holds the <code>App</code> class.<br>
35 * @object-prop <code>activeComponent</code> <code>Component</code> The currently active component in the
36 *              application.  This can be a panel in the application, a modal dialog, or
37 *              some other component.<br>
38 * @object-prop <code>modalStack</code> <code>Array</code> An array of modal dialog components.<br>
39 * @object-prop <code>modalMask</code> <code>Component</code> A div that is used when a modal dialog is displayed
40 *              to block events from reaching non-modal components.<br>
41 * <br>
42 * $Id$
43 */
44
45
46/* modal mask class */
47
48ModalMask = new Class( Component, {
49    eventMouseDown: function( event ) {
50        window.app.dismissTransients();
51    }
52} );
53
54
55/* app class */
56
57App = new Class( Component, Component.Delegator, {
58    NAMESPACE: "core",
59       
60   
61    initObject: function() {
62        arguments.callee.callSuper( this, document.documentElement );
63        this.window = window;
64        this.document = document;
65        this.displayState = {};
66        this.dialogs = {};
67        this.flyouts = {};
68        this.modalStack = [];
69        this.activeComponent = null;
70        this.monitorTimer = new Timer( this.getIndirectMethod( "monitor" ), 100 );
71    },
72   
73   
74    destroyObject: function() {
75        this.modalStack = null;
76        this.dialogs = null;
77        this.flyouts = null;
78        this.displayState = null;
79        this.activeComponent = null;
80        this.document = null;
81        this.window = null;
82        arguments.callee.applySuper( this, arguments );
83    },
84
85
86    /* events */
87
88    initEventListeners: function() {
89        arguments.callee.applySuper( this, arguments );
90        this.addEventListener( window, "resize", "eventResize" );
91        this.addEventListener( window, "unload", "eventUnload" );
92    },
93   
94   
95    eventClick: function( event ) {
96        var command = this.getMouseEventCommand( event );
97        switch( command ) {
98            case "goToLocation":
99                event.stop();
100                this.gotoLocation( event.commandElement.getAttribute( "href" ) );
101                break;
102        }
103    },
104   
105
106    /* event listeners */
107   
108    eventResize: function( event ) {
109        return this.reflow( event );
110    },
111   
112   
113    eventUnload: function( event ) {
114        this.destroy();
115    },
116   
117
118    /* components */
119   
120    initComponents: function() {
121        arguments.callee.applySuper( this, arguments );
122        if ( DOM.getElement( "modal-mask" ) )
123            this.modalMask = this.addComponent( new ModalMask( "modal-mask" ) );
124    },
125   
126   
127    /* timers */
128   
129    monitor: function( timer ) {
130        this.monitorDisplayState();
131        this.monitorLocation();
132    },
133   
134   
135    /* display state */
136   
137    monitorDisplayState: function() {
138        var changed = 0;
139        var style = DOM.getComputedStyle( this.element );
140        changed += this.displayChanged( style, "fontSize" );
141        if( changed )
142            this.reflow();
143    },
144   
145   
146    displayChanged: function( object, property ) {
147        try {
148            if( this.displayState[ property ] != object[ property ] ) {
149                this.displayState[ property ] = object[ property ];
150                return 1;
151            }
152        } catch( e ) {}
153        return 0;
154    },
155   
156     
157    /*  - - - - Begin activatables section - - - -  */
158   
159    /**
160     * Get the currently active (shaded, having meta-focus, etc) component on the page.
161     * @return <code>Component</code>
162     */   
163    getActiveComponent: function() {
164        return this.activeComponent; 
165    },
166
167
168    /**
169     * Event handler <b>not</b> based on <code>currentTarget</code>.
170     * @param component <code>Component</code> The component that should be activated.
171     * @return <code>boolean</code> Whether or not the <code>app</code> allowed the activation.
172     *         Currently, this always returns <code>true</code>. <br><br>TODO: Code the check for the following:
173     *         While the modal mask prevents the user from messing with the currently active
174     *         component when a modal dialog is up, we want to keep code from doing so as well just in case.
175     */
176    setActiveComponent: function( component ) {   
177        var modal = this.modalStack[ this.modalStack.length - 1 ];
178        var ancestors = DOM.getAncestors( component.element, true );
179       
180        if( modal && ancestors.indexOf( modal.element ) > 0 && modal.active )       
181            return false; // Since we 'includeSelf' in the 'getAncestors' (above), *must* use '>', not '>='.
182
183        if( this.activeComponent && this.activeComponent !== component )
184            this.activeComponent.deactivate();
185        this.activeComponent = component; 
186        return true; 
187    },
188
189   
190    /*  - Begin modal subsection -  */
191
192    /**
193     * Add a modal component to the collection of modal components managed by this <code>App</code>.
194     * @param modal <code>Component</code> The modal component (i.e., a dialog) to be added.
195     */
196    addModal: function( modal ) {
197        this.modalStack.add( modal );
198        this.modalMask.show();
199        modal.show();         
200        this.stackModals();
201    },
202   
203   
204    /**
205     * Remove a modal component from the collection of modal components managed by this <code>App</code>.
206     * @param modal <code>Component</code> The modal component (i.e., a dialog) to be added.
207     */
208    removeModal: function( modal ) {
209        modal.active = false;
210        modal.hide();
211        this.modalStack.remove( modal );
212        this.stackModals();
213    },
214       
215
216    /**
217     * Order the modals visuallly (in the z-index) according to their order in the managed collection
218     * of modal components.  Switch <code>modalMask</code> appropriately according to the nature of
219     * the top-most modal element in the z-index.
220     */
221   
222    STACK_RADIX: 1000,
223   
224    stackModals: function() {           
225        for( var i = this.modalStack.length; i > 0; i-- )
226            DOM.setZIndex( this.modalStack[ i - 1 ].element, i * this.STACK_RADIX );
227       
228        DOM.setZIndex( this.modalMask.element, this.modalStack.length * this.STACK_RADIX - 10 );
229        if( this.modalStack.length ) {
230            this.modalMask.show();
231            this.modalStack[ this.modalStack.length - 1 ].activate(); // Keep the top one active.
232        } else
233            this.modalMask.hide();
234    },
235   
236
237    dismissTransients: function() {
238        for ( var i = this.modalStack.length - 1; i >= 0; i-- )
239            if ( this.modalStack[ i ].transitory )
240                this.modalStack[ i ].close();
241    },
242
243
244    /**
245     * Remove all modal components, so that they do not show on the screen and
246     * and so that they do not exist in the managed collection (<code>modalStack</code>).
247     */
248    dismissAll: function() {
249        while ( this.modalStack.length )
250            this.removeModal( this.modalStack[ 0 ] );
251    },
252
253    /*  - End modal subsection -  */
254
255
256
257    /*  - - - - End activatables section - - - -  */
258
259   
260    /* location monitoring, back/forward/bookmark in AJAX pages */
261   
262    gotoLocation: function( locationBase, locationArg ) {
263        var location = ("" + this.window.location);
264        var parts = location.split( "#" );
265        if( locationBase ) {
266            parts[ 0 ] = locationBase;
267            this.monitorTimer.stop();
268        }
269        if( locationArg )
270            parts[ 1 ] = encodeURI( this.encodeLocation( locationArg ) );
271        else
272            parts.length = 1;
273           
274        location = parts.join( "#" );
275       
276        if( this.location == location )
277             return;
278        this.location = location;
279       
280        // IE uses a hidden iframe
281        if( !locationBase && defined( this.window.clipboardData ) ) {
282            var iframe = this.document.getElementById( "__location" );
283            iframe.contentWindow.document.open( "text/html" );
284            iframe.contentWindow.document.write(
285                "<script type='text/javascript'>" +
286                "if( window.parent && window.parent.app )" +
287                "window.parent.app.replaceLocation( '" + this.location + "' );" +
288                "</script>" +
289                "<body style='background: rgb(" +
290                    Math.floor( Math.random() * 256 ) + "," +
291                    Math.floor( Math.random() * 256 ) + "," +
292                    Math.floor( Math.random() * 256 ) + ")'>" +
293                this.location + "</body>"
294            );
295        }
296       
297        // otherwise just set the location
298        else {
299            this.window.location = this.location;
300            /* safari back/forward is busted
301             * since window.location isn't set immediately, monitorLocation
302             * catches it later and fires exec again.  ALSO, safari never
303             * changes window.location on back/forward, so monitorLocation
304             * is useless.
305             */
306            if ( ( "" + this.window.location ) != this.location )
307                this.monitorTimer.stop();
308        }
309       
310        this.parseLocation();
311        if ( !locationBase ) {
312            this.exec();
313            this.broadcastToObservers( "exec" );
314        }
315    },
316   
317   
318    /* used by hidden iframe on IE */
319    replaceLocation: function( location ) {
320        this.window.location.replace( location );
321        if( this.location == location )
322            return;
323        this.location = location;
324        this.parseLocation();
325        this.exec();
326        this.broadcastToObservers( "exec" );
327    },
328   
329   
330    monitorLocation: function() {
331        var location = ("" + window.location);
332        if( this.location == location )
333            return;
334        this.location = location;
335        this.parseLocation();
336        this.exec();
337        this.broadcastToObservers( "exec" );
338    },
339   
340   
341    parseLocation: function() {
342        if( !defined( this.locationArg ) )
343            this.locationArg = null;
344        try {
345            var parts = this.location.split( "#" );
346            var arg = decodeURI( parts[ 1 ] || "" );
347            this.locationArg = this.decodeLocation( arg );
348        } catch( e ) {}
349        return this.locationArg;
350    },
351
352
353    /* basic #/type:Music/page:2 encoding and decoding */
354   
355    encodeLocation: function( obj ) {
356        var loc = [];
357        for ( key in obj )
358            if ( obj.hasOwnProperty( key ) && typeof obj[ key ] != "function" )
359                loc.push( key + ":" + ( ( obj[ key ] instanceof Array ) ? obj[ key ].join( "," ) : obj[ key ] ) );
360        return loc.join( "/" );
361    },
362
363
364    decodeLocation: function( arg ) {
365        var obj = {};
366        var vals = arg.split( "/" );
367        var kv;
368        for ( var i = 0; i < vals.length; i++ ) {
369            if ( !defined( vals[ i ] ) )
370                continue;
371            kv = vals[ i ].split( ":" );
372            /* xxx we don't decode comma seperated lists to arrays here */
373            if ( kv && kv.length > 1 )
374               obj[ kv[ 0 ] ] = kv[ 1 ];
375            else
376                obj[ vals[ i ] ] = true;
377        }
378        return obj;
379    },
380
381   
382    /* execution */
383   
384    exec: function() {} 
385
386});
387
388   
389/* bootstrap methods */
390
391App.bootstrap = function() {
392    window.app = App.initSingleton();
393};
394
395
396App.bootstrapInline = function( defer ) {
397    /* deferred bootstrap onload by default */
398    DOM.addEventListener( window, "load", this.bootstrap );
399    this.deferBootstrap = defer;
400    this.bootstrapApp();
401};
402
403
404App.bootstrapIframe = function() {
405    this.deferBootstrap = false;
406    this.bootstrapApp();
407};
408
409
410App.bootstrapCheck = function() {
411    var e = DOM.getElement( "bootstrapper" );
412    if ( !e )
413        return log.warn('bootstrap checking...');
414   
415    if ( this.bootstrapTimer )
416        this.bootstrapTimer.stop();
417   
418    this.bootstrapTimer = null;
419
420    this.bootstrap();
421}
422
423
424App.bootstrapApp = function() {
425    if ( this.deferBootstrap )
426        return;
427    this.bootstrapTimer = new Timer( this.bootstrapCheck.bind( this ), 20 );
428};
429
430
431/* creating the iframe during load is the only reliable way to preserve AJAX history
432   across different page views. DOM created iframes do not work */
433
434if( defined( window.clipboardData ) ) {
435    /* fix IE background image flash */
436    try {
437        document.execCommand( "BackgroundImageCache", false, true );
438    } catch( e ) { };
439    var blankURI = window.__blankURI__ || "about:blank";
440    document.write( "<iframe id='__location' src='" + blankURI + "' width='0' height='0' frameborder='0'" +
441        "style='visibility:hidden;position:absolute;left:0;top:0;'></iframe>" );
442}
443
Note: See TracBrowser for help on using the browser.