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