1 
  2 /**
  3  * Creates a new Formats object.
  4  *  Do not create an indeterminent number of these components or it may result in
  5  *  memory leaks
  6  *
  7  * @constructor
  8  */
  9 madrona.panel = function(options){
 10     
 11     var defaults = {
 12         hideOnly: false,
 13         showCloseButton: true,
 14         content: false,
 15         appendTo: window.document.body,
 16         scrollable: true
 17     }
 18     
 19     var that = {
 20         options: $.extend({}, defaults, options),
 21         shown: false
 22     };
 23     
 24     if(madrona && madrona.addPanel){
 25         madrona.addPanel(that);
 26     }
 27     
 28     var s = '';
 29     var loader;
 30     
 31     if(!that.options.showCloseButton){
 32         s = 'display:none';
 33     }
 34     
 35     var close = '<a style="'+s+'" class="close" href="#"><img src="'+madrona.options.media_url+'common/images/close.png" width="17" height="16" /></a>';
 36     
 37     var other_classes = that.options.scrollable ? '' : 'madrona-panel-noscroll';
 38     var el = $('<div style="display:none;" class="madrona-panel '+other_classes+'"><div class="loadingMask"><span>Loading</span></div><div class="panelMask"></div>'+close+'<div class="content container_12"><div class="panel"></div></div></div>');
 39     el.data('panelComponent', that);
 40     var anotherel = el;
 41         
 42     el.find('a.close').click(function(){
 43         that.close();
 44     });
 45     
 46     var content = el.find('.content');
 47     
 48     $(that.options.appendTo).append(el);
 49     
 50     if(that.options.content && $(that.options.content).length){
 51         var c = $(that.options.content);
 52         c.remove();
 53         content.append(c);
 54     }
 55 
 56     that.showContent = function(elements, opts){
 57         that.addContent(elements);
 58         if(opts && opts.showClose){
 59             el.find('a.close').show();
 60         }
 61         that.show();
 62     }
 63     
 64     that.addContent = function(elements){
 65         if(!that.options.content){
 66             content.html('');
 67             content.append(elements);            
 68         }
 69     }
 70     
 71     that.show = function(animate){
 72         $(el[0]).show();
 73         $(el[0]).find('.madrona-table').each(function(){
 74             madrona.ui.table(this);
 75         });
 76         $(el[0]).scrollTop(0);
 77         that.shown = true;
 78         $(that).trigger('panelshow', that);       
 79     }
 80     
 81     that.close = function(){
 82         if(loader && loader.destroy){
 83             loader.destroy();
 84         }
 85         $(el[0]).scrollTop(1).scrollTop(0);
 86         el.find('a.close').hide();
 87         if(!that.options.hideOnly){
 88             el.hide();
 89             that.shown = false;
 90             el.find('div.content').html('');
 91             $(that).trigger('panelclose', that);
 92         }
 93     }
 94     
 95     that.spin = function(message){
 96         if(el.is(':visible')){
 97             el.find('.loadingMask span').text(message || "Loading")
 98             el.find('.loadingMask').show();
 99         }else{
100             madrona.showLoadingMask(message);
101         }
102     };
103     
104     that.stopSpinning = function(){
105         madrona.hideLoadingMask();
106         el.find('.loadingMask').hide();
107     };
108     
109     that.showError = function(title, message){
110         
111     };
112     
113     // Returns the names of all open tabs as a list, ie: root->parent->child
114     var getActiveTabs = function(element, list){
115         var list = list || [];
116         var selected = element.find('.ui-tabs:first .ui-tabs-selected:first > a');
117         var href = selected.attr('href');
118         var name = selected.text()
119         list.push(name);
120         var panel = $(href);
121         if(panel.find('.ui-tabs').length){
122             getActiveTabs(panel, list);
123         }
124         return list;
125     };
126     
127     that.showUrl = function(url, options){
128         that.spin(options.load_msg || "Loading");
129         $(that).trigger('panelloading');        
130         loader = madrona.contentLoader($.extend({}, {
131             url: url,
132             activeTabs: options.syncTabs ? getActiveTabs(el) : false,
133             error: function(e){ 
134                 alert('error loading content in panel');
135                 that.stopSpinning();
136             },
137             behaviors: applyBehaviors,
138             target: el.find('.content'),
139             beforeCallbacks: function(){
140                 that.stopSpinning();
141                 el.find('a.close').show();                    
142                 that.show();
143             }
144         }, options));
145         loader.load();
146     };
147     
148     that.showText = function(text, options){
149         loader = madrona.contentLoader($.extend({}, {
150             text: text,
151             activeTabs: options.syncTabs ? getActiveTabs(el) : false,
152             error: options.error,
153             behaviors: applyBehaviors,
154             target: el.find('.content'),
155             beforeCallbacks: function(){
156                 el.find('a.close').show();                    
157                 that.show();
158             }
159         }, options));
160         loader.load();
161     }
162     
163     // Applies default behaviors for sidebar content that are defined by css
164     // classes and html tags such as datagrids, links that open in the same 
165     // sidebar, tooltips, etc.
166     // 
167     // New functions will be added to this section over time, and 
168     // documentation should be added to the sidebar-content author guide.
169     var applyBehaviors = function(el){
170         
171         // Any link with a 'panel_link' class is overridden to open within the 
172         // same panel.
173         // WARNING: the link needs to be in a block-level container 
174         // (p, div, span, etc). Also, since it uses ajax calls, the host must 
175         // be the same
176         el.find('a.panel_link').click( function(e) {
177             that.showUrl( $(this).attr('href') ,options);
178             e.preventDefault();
179         });
180     }    
181     
182     // Methods needed for test management
183     that.destroy = function(){
184         that.getEl().remove();
185         if(that.shown){
186             that.close();
187         }
188     }
189     
190     that.getEl = function(){
191         return el;
192     }
193     
194     that.hide = function(){
195         $(el[0]).scrollTop(0);
196         el.hide();
197         that.shown = false;
198         $(that).trigger('panelhide', that);
199         // }
200         $(that).trigger('panelhide');
201     }
202                 
203     return that;
204 };
205 
206 
207 // Takes an html fragment and extracts and stores and style or script tags
208 // from it. The resulting html can then be added to the page. This class also
209 // exposes a method for adding stylesheets to the page and extracting 
210 // callbacks within the content and providing them with references to the tabs
211 // they are to be associated with.
212 madrona.layout.SanitizedContent = function(html){
213     
214     // these instance variables will be available, but are not 
215     // particularly useful. Use instance methods instead.
216     this.js = [];
217     this.styles = [];
218     this.html;
219     
220     var rscript = /<script(.|\s)*?\/script>/ig;
221     var rstyle = /<style(.|\s)*?\/style>/ig;
222     
223     var that = this;
224     
225     // extract script tags. NOT intended to work for src="..."-type tags
226     html = html.replace(rscript, function(m){
227         if(m.indexOf('text/javascript+protovis') !== -1){
228             return m;
229         }else if(m.indexOf('application/vnd.google-earth.kml+xml') !== -1){
230             return m;
231         }else{
232             that.js.push(m.replace(
233                 /<script(.|\s)*?>/i, '').replace(/<\/script>/i,''));
234             return '';
235         }
236     });
237     
238     
239     // extract style tags
240     html = html.replace(rstyle, function(m){
241         that.styles.push({
242             id: $(m).attr('id'),
243             style: m.replace(/<style(.|\s)*?>/i, '').replace(/<\/style>/i, '')
244         });
245         return '';
246     });
247     
248     if($.browser.msie && $.browser.version === "8.0" && html.match('<FORM') && html.match('.errorlist')){
249         // html coming from the iframe with validation errors is going to be 
250         // mangled. This took forever to figure out, and is an ugly ugly hack.
251         // This can be removed once we stop using iframes to submit forms
252         var dom = $('<div>' + html + '</div>');
253         if(dom.find('form div.json').length === 0){
254             // IE jumbled where items should be in the dom
255             var attrs = $(dom.children()[0].childNodes[1].childNodes[2]);
256             var form = attrs.find('form');
257             form.append(attrs.children().slice(1, -1));
258             // remove trailing form end tag
259             $(attrs.children()[1]).detach();
260         }
261         var el = $('<div>');
262         el.append(dom);
263         html = el.html();
264     }
265     
266     this.html = jQuery.trim(html);
267     return this;
268 };
269 
270 // Adds new style tags to the head of the document that were found in the
271 // sanitized fragment. If a style tag has an ID attribute, and has already
272 // been added to the document, it won't be added again.
273 madrona.layout.SanitizedContent.prototype.addStylesToDocument = function(){
274     for(var i = 0; i < this.styles.length; i++){
275         var style = this.styles[i];
276         if(!style.id || $('#'+style.id).length < 1){
277             var ss1 = document.createElement('style');
278             if(style.id){
279                 $(ss1).attr('id', style.id);
280             }
281             ss1.setAttribute("type", "text/css");
282             if (ss1.styleSheet) {   // IE
283                 ss1.styleSheet.cssText = style.style;
284             } else {                // the world
285                 var tt1 = document.createTextNode(style.style);
286                 ss1.appendChild(tt1);
287             }
288             var hh1 = document.getElementsByTagName('head')[0];
289             hh1.appendChild(ss1);
290         }
291     }
292 };
293 
294 // Extracts all callbacks defined in the html fragment by onPanelShow and
295 // onTabShow calls and returns them as an object with keys 'tabs'
296 // and 'panel'. 'tabs' is an object keyed 
297 madrona.layout.SanitizedContent.prototype.extractCallbacks = function(){
298     var returnObj = {
299         panel: {},
300         tabs: {}
301     };
302     
303     // Create functions for the eval'd code to call
304     
305     function addCallback(type, target, callback){
306         if(target && !callback){
307             callback = target;
308             target = false;
309         }
310         if(target){
311             if(!returnObj.tabs[target]){
312                 returnObj.tabs[target] = {};
313             }
314             if(!returnObj.tabs[target][type]){
315                 returnObj.tabs[target][type] = [];
316             }
317             returnObj.tabs[target][type].push(callback);            
318         }else{
319             if(!returnObj.panel[type]){
320                 returnObj.panel[type] = [];
321             }
322             returnObj.panel[type].push(callback);
323         }        
324     }
325     
326     madrona.onShow = function(target, callback){
327         addCallback('show', target, callback);
328     };
329     
330     madrona.onHide = function(target, callback){
331         addCallback('hide', target, callback);
332     };
333     
334     madrona.onUnhide = function(target, callback){
335         addCallback('unhide', target, callback);
336     };
337     
338     madrona.beforeDestroy = function(target, callback){
339         if(!callback){
340             callback = target;
341         }
342         addCallback('destroy', false, callback);
343     };
344     
345     // will create a script tag, append it so it runs, then remove the tag
346     jQuery.globalEval(this.js.join(';\n'));
347     
348     // remove the event registration functions to ensure no overlap
349     // madrona.onShow = madrona.onHide = lingod.onUnhide = madronae.beforeDestroy = false;
350     
351     return returnObj;
352 }
353 
354 madrona.layout.SanitizedContent.prototype.cleanHtml = function(){
355     return this.html;
356 }
357 
358 // contentLoader loads the specified url in a hidden staging area. The 
359 // callback function provides a reference to the loaded content, which should 
360 // be moved into a space for interaction with the user.
361 // This function can be called with just a url and callback, or with an 
362 // optional opentabs argument. This argument should be an array of tab names
363 // (in order) that should be opened before firing the callback.
364 // 
365 // Examples
366 // madrona.contentLoader('/my/url.html', ['firstTab', 'childTab'], function(domRef){
367 //      $(domRef).detach();
368 //      $('#sidebar .content').append(domRef);
369 // });
370 madrona.contentLoader = (function(){
371     
372     return function(options){
373         
374         if(!options.target &&(!options.url || !options.text)){
375             throw('madrona.contentLoader: must specify a target, and a url or text option.');
376         }
377         
378         options.error = options.error || function(){ 
379             alert('error loading content from '+options.url);
380         };
381         
382         options.beforeCallbacks = options.beforeCallbacks || function(){};
383         options.afterCallbacks = options.afterCallbacks || function(){};
384         options.success = options.success || function(){};
385         
386         var that = {};
387         var callbacks = false;
388         var still_staging = true;
389         var staging = $('<div class="madrona-panel-staging"></div>');
390         $(document.body).prepend(staging);
391         
392         // Finds any onShow callbacks via the mm:onshow data attribute and fires
393         // them unless instance.still_staging is true
394         function fireCallbacks(types, el){
395             if(typeof types === 'string'){
396                 types = [types];
397             }
398             el.each(function(){
399                 var a = $(this);
400                 var callbacks = a.data('mm:callbacks');
401                 if(callbacks){
402                     for(var j = 0; j < types.length; j++){
403                         var type = types[j];
404                         if(!still_staging && callbacks[type] && callbacks[type].length){
405                             for(var i = 0; i < callbacks[type].length; i++){
406                                 callbacks[type][i]();
407                             }
408                             if(type === 'show' || type === 'destroy'){
409                                 delete callbacks[type];
410                             }
411                         }
412                     }
413                     a.data('mm:callbacks', callbacks);                    
414                 }
415             });
416         };
417                 
418         // removes script and style tags from html and returns. Also Adds 
419         // found style tags to the document and assigns callbacks defined in 
420         // those script tags to 'callbacks'. Meant to be assigned to 
421         // dataFilter option of a jQuery.ajax call.
422         var dataFilter = function(html, type){
423             if(callbacks){
424                 throw('callbacks not handled before fetching new content!');
425             }
426             var content = new madrona.layout.SanitizedContent(html);
427             content.addStylesToDocument();
428             callbacks = content.extractCallbacks();
429             return content.cleanHtml();
430         };
431         
432         // Finds any div with a class of .tabs and applies the jqueryui tabs
433         // widget to it with suitable options.
434         var enableTabs = function(el){
435             var tabs = el.find('div.tabs');
436             if(tabs.length){
437                 var t = tabs.tabs({
438                     'spinner': '<img id="loadingTab" src="'+madrona.options.media_url+'common/images/small-loader.gif" />loading...', 
439                     ajaxOptions: {
440                         error: function(e){
441                             if (e.statusText == 'error') {
442                                 $('#loadingTab').parent().parent().remove();
443                                 alert('An error occured attempting to load this tab. ' +
444                                       '\nError code ' + e.status +
445                                       '\nIf the problem persists, please contact ' +
446                                       'help@madrona.org for assistance.');
447                             }
448                         },
449                         dataFilter: dataFilter
450                     },
451                     load: function(event, ui){
452                         var p = $(ui.panel);
453                         enableTabs(p);
454                         attachCallbacks($(ui.tab));
455                         // Fire the newly loaded tab's callbacks
456                         fireCallbacks(['show', 'unhide'], $(ui.tab));
457                         // Fire events of any subtabs of the loaded tab that 
458                         // are selected
459                         fireCallbacks(['show', 'unhide'], p.find('.ui-tabs-selected a'));
460                         var after = $(ui.tab).data('mm:aftershow');
461                         if(after && !$(ui.tab).parent().hasClass('ui-state-processing')){
462                             $(ui.tab).removeData('mm:aftershow')
463                             after();
464                         }
465                     },
466                     show: function(event, ui){
467                         fireCallbacks(['show', 'unhide'], $(ui.tab));
468                         // first find disabled tabs at this level and fire callbacks
469                         $(this).find('ul.ui-tabs-nav:first a').each(function(){
470                             if(!$(this).parent().hasClass('ui-tabs-selected')){
471                                 fireCallbacks('hide', $(this));
472                             }
473                         });
474                         // find now-hidden selected subtabs and fire callbacks
475                         var selected_subtab = $(this).find('.ui-tabs-hide li.ui-tabs-selected a');
476                         if(selected_subtab.length){
477                             fireCallbacks('hide', selected_subtab);
478                         }
479                         fireCallbacks(['show', 'unhide'], 
480                             $(ui.panel).find('li.ui-tabs-selected a'));
481                         var after = $(ui.tab).data('mm:aftershow');
482                         if(after && !$(ui.tab).parent().hasClass('ui-state-processing')){
483                             $(ui.tab).removeData('mm:aftershow')
484                             after();
485                         }
486                     },
487                     cache: true
488                 });
489             }
490         };
491         
492         // Used to sync open tabs with another panel if options.activeTabs is 
493         // populated. Recursively calls itself while opening async tabs.
494         var followTabs = function(element, callback){
495             if(options.activeTabs && options.activeTabs.length > 0){
496                 var t = options.activeTabs.shift();
497                 var link = element.find('div.tabs > ul li a').filter(function(){
498                     return $(this).text() === t;
499                 });
500                 if(link.length === 0){
501                     followTabs(null, callback);
502                     return;
503                 }
504                 var tabs = link.parent().parent().parent();
505                 // enableTabs(tabs.parent());
506                 var cback = function(){
507                     attachCallbacks(tabs);
508                     followTabs(tabs, callback);
509                 };
510                 if(link.parent().hasClass('ui-tabs-selected')){
511                     cback();
512                 }else{
513                     link.data('mm:aftershow', cback);
514                     link.click();                        
515                 }
516             }else{
517                 callback();
518             }
519         };
520         
521         // Given an element, will assign onPanelShow callbacks to that 
522         // element. It will also find any tabs within the element associated
523         // with callbacks['tabs'] callbacks and assign them. Uses jQuery.data
524         // function and adds callbacks using a key of "mm:onshow"
525         var attachCallbacks = function(el){
526             el.data('mm:callbacks', callbacks['panel']);
527             if(callbacks.tabs){
528                 if(el.is('a')){
529                     var el = el.parent().parent().parent()
530                         .find(el.attr('href'));
531                 }
532                 for(var key in callbacks['tabs']){
533                     var t = el.find('.ui-tabs-nav li a[href='+key+']');
534                     t.data('mm:callbacks', callbacks.tabs[key]);
535                 }
536             }
537             callbacks = false; // must do this or error will be thrown            
538         };
539 
540         that.destroy = function(){
541             fireCallbacks('destroy', $(jQuery.merge(options.target.toArray(), options.target.find('.tabs, .ui-tabs-nav li a').toArray())));            
542         };
543 
544 
545         var processText = function(data){
546             staging.html(data);
547             if(options.behaviors){
548                 options.behaviors(staging);
549             }
550             enableTabs(staging);
551             attachCallbacks(staging);
552             followTabs(staging, function(){
553                 // move staged content to target
554                 options.target.html('');
555                 var contents = staging.children();
556                 contents.detach();
557                 options.target.append(contents);
558                 // fire callbacks
559                 options.beforeCallbacks();
560                 still_staging = false;
561                 fireCallbacks(['show', 'unhide'], staging);
562                 fireCallbacks(['show', 'unhide'], contents.find('.ui-tabs-selected a'));
563                 options.target.data('mm:callbacks', staging.data('mm:callbacks'));
564                 options.afterCallbacks();
565                 options.success();
566                 staging.remove();
567             });
568         }
569 
570         that.load = function(){
571             if(options.text){
572                 processText(dataFilter(options.text));
573             }else{
574                 $.ajax({
575                     url: options.url,
576                     dataFilter: dataFilter,
577                     error: options.error,
578                     success: function(data, status, xhr){
579                         processText(data);
580                     }
581                 });                
582             }
583         };
584         
585         return that;
586     };
587     
588 })();
589