From 204d57a4a58f7d27b11abaa2b61643919229bdda Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Mon, 26 Jan 2026 16:34:26 +1100 Subject: [PATCH] feature: display pdf attachments inline --- .../Areas/API/Controllers/DeviceController.cs | 4 +- .../Areas/API/Controllers/JobController.cs | 4 +- .../Areas/API/Controllers/UserController.cs | 4 +- .../ClientSource/Scripts/Modules/Shadowbox.js | 2970 ++++++++++++++++- .../Scripts/Modules/Shadowbox.min.js | 9 +- .../Scripts/Modules/Shadowbox/shadowbox.js | 2970 ++++++++++++++++- .../T4MVC/API.DeviceController.generated.cs | 8 +- .../T4MVC/API.JobController.generated.cs | 8 +- .../T4MVC/API.UserController.generated.cs | 8 +- .../Device/DeviceParts/_Resources.cshtml | 20 +- .../DeviceParts/_Resources.generated.cs | 192 +- Disco.Web/Views/Job/JobParts/Resources.cshtml | 20 +- .../Views/Job/JobParts/Resources.generated.cs | 125 +- .../Views/User/UserParts/_Resources.cshtml | 20 +- .../User/UserParts/_Resources.generated.cs | 197 +- 15 files changed, 6285 insertions(+), 274 deletions(-) diff --git a/Disco.Web/Areas/API/Controllers/DeviceController.cs b/Disco.Web/Areas/API/Controllers/DeviceController.cs index f6c1a067..351b4320 100644 --- a/Disco.Web/Areas/API/Controllers/DeviceController.cs +++ b/Disco.Web/Areas/API/Controllers/DeviceController.cs @@ -547,7 +547,7 @@ namespace Disco.Web.Areas.API.Controllers #region Device Attachments [DiscoAuthorize(Claims.Device.ShowAttachments), OutputCache(Location = System.Web.UI.OutputCacheLocation.Client, Duration = 172800)] - public virtual ActionResult AttachmentDownload(int id) + public virtual ActionResult AttachmentDownload(int id, bool? inline = null) { var da = Database.DeviceAttachments.Find(id); if (da != null) @@ -555,7 +555,7 @@ namespace Disco.Web.Areas.API.Controllers var filePath = da.RepositoryFilename(Database); if (System.IO.File.Exists(filePath)) { - return File(filePath, da.MimeType, da.Filename); + return File(filePath, da.MimeType, (inline ?? false) ? null : da.Filename); } else { diff --git a/Disco.Web/Areas/API/Controllers/JobController.cs b/Disco.Web/Areas/API/Controllers/JobController.cs index 440811e5..3c8c9dec 100644 --- a/Disco.Web/Areas/API/Controllers/JobController.cs +++ b/Disco.Web/Areas/API/Controllers/JobController.cs @@ -1927,7 +1927,7 @@ namespace Disco.Web.Areas.API.Controllers #region Job Attachments [DiscoAuthorize(Claims.Job.ShowAttachments), OutputCache(Location = System.Web.UI.OutputCacheLocation.Client, Duration = 172800)] - public virtual ActionResult AttachmentDownload(int id) + public virtual ActionResult AttachmentDownload(int id, bool? inline = null) { var ja = Database.JobAttachments.Find(id); if (ja != null) @@ -1935,7 +1935,7 @@ namespace Disco.Web.Areas.API.Controllers var filePath = ja.RepositoryFilename(Database); if (System.IO.File.Exists(filePath)) { - return File(filePath, ja.MimeType, ja.Filename); + return File(filePath, ja.MimeType, (inline ?? false) ? null : ja.Filename); } else { diff --git a/Disco.Web/Areas/API/Controllers/UserController.cs b/Disco.Web/Areas/API/Controllers/UserController.cs index 09b5443b..3e591973 100644 --- a/Disco.Web/Areas/API/Controllers/UserController.cs +++ b/Disco.Web/Areas/API/Controllers/UserController.cs @@ -108,7 +108,7 @@ namespace Disco.Web.Areas.API.Controllers [DiscoAuthorize(Claims.User.ShowAttachments)] [OutputCache(Location = System.Web.UI.OutputCacheLocation.Client, Duration = 172800)] - public virtual ActionResult AttachmentDownload(int id) + public virtual ActionResult AttachmentDownload(int id, bool? inline = null) { var ua = Database.UserAttachments.Find(id); if (ua != null) @@ -116,7 +116,7 @@ namespace Disco.Web.Areas.API.Controllers var filePath = ua.RepositoryFilename(Database); if (System.IO.File.Exists(filePath)) { - return File(filePath, ua.MimeType, ua.Filename); + return File(filePath, ua.MimeType, (inline ?? false) ? null : ua.Filename); } else { diff --git a/Disco.Web/ClientSource/Scripts/Modules/Shadowbox.js b/Disco.Web/ClientSource/Scripts/Modules/Shadowbox.js index e7f8c4f0..999b44d8 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/Shadowbox.js +++ b/Disco.Web/ClientSource/Scripts/Modules/Shadowbox.js @@ -1,8 +1,2972 @@ -/* +/*! * Shadowbox.js, version 3.0.3 * http://shadowbox-js.com/ * * Copyright 2007-2010, Michael J. I. Jackson - * Date: 2011-05-22 05:52:23 +0000 + * Date: 2026-01-26 15:30:20.2317822 +1100 */ -(function(au,k){var Q={version:"3.0.3"};var J=navigator.userAgent.toLowerCase();if(J.indexOf("windows")>-1||J.indexOf("win32")>-1){Q.isWindows=true}else{if(J.indexOf("macintosh")>-1||J.indexOf("mac os x")>-1){Q.isMac=true}else{if(J.indexOf("linux")>-1){Q.isLinux=true}}}Q.isIE=J.indexOf("msie")>-1;Q.isIE6=J.indexOf("msie 6")>-1;Q.isIE7=J.indexOf("msie 7")>-1;Q.isGecko=J.indexOf("gecko")>-1&&J.indexOf("safari")==-1;Q.isWebKit=J.indexOf("applewebkit/")>-1;var ab=/#(.+)$/,af=/^(light|shadow)box\[(.*?)\]/i,az=/\s*([a-z_]*?)\s*=\s*(.+)\s*/,f=/[0-9a-z]+$/i,aD=/(.+\/)shadowbox\.js/i;var A=false,a=false,l={},z=0,R,ap;Q.current=-1;Q.dimensions=null;Q.ease=function(K){return 1+Math.pow(K-1,3)};Q.errorInfo={fla:{name:"Flash",url:"http://www.adobe.com/products/flashplayer/"},qt:{name:"QuickTime",url:"http://www.apple.com/quicktime/download/"},wmp:{name:"Windows Media Player",url:"http://www.microsoft.com/windows/windowsmedia/"},f4m:{name:"Flip4Mac",url:"http://www.flip4mac.com/wmv_download.htm"}};Q.gallery=[];Q.onReady=aj;Q.path=null;Q.player=null;Q.playerId="sb-player";Q.options={animate:true,animateFade:true,autoplayMovies:true,continuous:false,enableKeys:true,flashParams:{bgcolor:"#000000",allowfullscreen:true},flashVars:{},flashVersion:"9.0.115",handleOversize:"resize",handleUnsupported:"link",onChange:aj,onClose:aj,onFinish:aj,onOpen:aj,showMovieControls:true,skipSetup:false,slideshowDelay:0,viewportPadding:20};Q.getCurrent=function(){return Q.current>-1?Q.gallery[Q.current]:null};Q.hasNext=function(){return Q.gallery.length>1&&(Q.current!=Q.gallery.length-1||Q.options.continuous)};Q.isOpen=function(){return A};Q.isPaused=function(){return ap=="pause"};Q.applyOptions=function(K){l=aC({},Q.options);aC(Q.options,K)};Q.revertOptions=function(){aC(Q.options,l)};Q.init=function(aG,aJ){if(a){return}a=true;if(Q.skin.options){aC(Q.options,Q.skin.options)}if(aG){aC(Q.options,aG)}if(!Q.path){var aI,S=document.getElementsByTagName("script");for(var aH=0,K=S.length;aHaQ){aS=aQ-aM}var aG=2*aO+K;if(aJ+aG>aR){aJ=aR-aG}var S=(aN-aS)/aN,aP=(aH-aJ)/aH,aK=(S>0||aP>0);if(aL&&aK){if(S>aP){aJ=Math.round((aH/aN)*aS)}else{if(aP>S){aS=Math.round((aN/aH)*aJ)}}}Q.dimensions={height:aS+aI,width:aJ+K,innerHeight:aS,innerWidth:aJ,top:Math.floor((aQ-(aS+aM))/2+aO),left:Math.floor((aR-(aJ+aG))/2+aO),oversized:aK};return Q.dimensions};Q.makeGallery=function(aI){var K=[],aH=-1;if(typeof aI=="string"){aI=[aI]}if(typeof aI.length=="number"){aF(aI,function(aK,aL){if(aL.content){K[aK]=aL}else{K[aK]={content:aL}}});aH=0}else{if(aI.tagName){var S=Q.getCache(aI);aI=S?S:Q.makeObject(aI)}if(aI.gallery){K=[];var aJ;for(var aG in Q.cache){aJ=Q.cache[aG];if(aJ.gallery&&aJ.gallery==aI.gallery){if(aH==-1&&aJ.content==aI.content){aH=K.length}K.push(aJ)}}if(aH==-1){K.unshift(aI);aH=0}}else{K=[aI];aH=0}}aF(K,function(aK,aL){K[aK]=aC({},aL)});return[K,aH]};Q.makeObject=function(aH,aG){var aI={content:aH.href,title:aH.getAttribute("title")||"",link:aH};if(aG){aG=aC({},aG);aF(["player","title","height","width","gallery"],function(aJ,aK){if(typeof aG[aK]!="undefined"){aI[aK]=aG[aK];delete aG[aK]}});aI.options=aG}else{aI.options={}}if(!aI.player){aI.player=Q.getPlayer(aI.content)}var K=aH.getAttribute("rel");if(K){var S=K.match(af);if(S){aI.gallery=escape(S[2])}aF(K.split(";"),function(aJ,aK){S=aK.match(az);if(S){aI[S[1]]=S[2]}})}return aI};Q.getPlayer=function(aG){if(aG.indexOf("#")>-1&&aG.indexOf(document.location.href)==0){return"inline"}var aH=aG.indexOf("?");if(aH>-1){aG=aG.substring(0,aH)}var S,K=aG.match(f);if(K){S=K[0].toLowerCase()}if(S){if(Q.img&&Q.img.ext.indexOf(S)>-1){return"img"}if(Q.swf&&Q.swf.ext.indexOf(S)>-1){return"swf"}if(Q.flv&&Q.flv.ext.indexOf(S)>-1){return"flv"}if(Q.qt&&Q.qt.ext.indexOf(S)>-1){if(Q.wmp&&Q.wmp.ext.indexOf(S)>-1){return"qtwmp"}else{return"qt"}}if(Q.wmp&&Q.wmp.ext.indexOf(S)>-1){return"wmp"}}return"iframe"};function G(){var aH=Q.errorInfo,aI=Q.plugins,aK,aL,aO,aG,aN,S,aM,K;for(var aJ=0;aJ'+s(Q.lang.errors[aN],S)+""}else{aL=true}}else{if(aK.player=="inline"){aG=ab.exec(aK.content);if(aG){aM=ad(aG[1]);if(aM){aK.content=aM.innerHTML}else{aL=true}}else{aL=true}}else{if(aK.player=="swf"||aK.player=="flv"){K=(aK.options&&aK.options.flashVersion)||Q.options.flashVersion;if(Q.flash&&!Q.flash.hasFlashPlayerVersion(K)){aK.width=310;aK.height=177}}}}if(aL){Q.gallery.splice(aJ,1);if(aJ0?aJ-1:aJ}}--aJ}}}function aq(K){if(!Q.options.enableKeys){return}(K?F:M)(document,"keydown",an)}function an(aG){if(aG.metaKey||aG.shiftKey||aG.altKey||aG.ctrlKey){return}var S=v(aG),K;switch(S){case 81:case 88:case 27:K=Q.close;break;case 37:K=Q.previous;break;case 39:K=Q.next;break;case 32:K=typeof ap=="number"?Q.pause:Q.play;break}if(K){n(aG);K()}}function c(aK){aq(false);var aJ=Q.getCurrent();var aG=(aJ.player=="inline"?"html":aJ.player);if(typeof Q[aG]!="function"){throw"unknown player "+aG}if(aK){Q.player.remove();Q.revertOptions();Q.applyOptions(aJ.options||{})}Q.player=new Q[aG](aJ,Q.playerId);if(Q.gallery.length>1){var aH=Q.gallery[Q.current+1]||Q.gallery[0];if(aH.player=="img"){var S=new Image();S.src=aH.content}var aI=Q.gallery[Q.current-1]||Q.gallery[Q.gallery.length-1];if(aI.player=="img"){var K=new Image();K.src=aI.content}}Q.skin.onLoad(aK,W)}function W(){if(!A){return}if(typeof Q.player.ready!="undefined"){var K=setInterval(function(){if(A){if(Q.player.ready){clearInterval(K);K=null;Q.skin.onReady(e)}}else{clearInterval(K);K=null}},10)}else{Q.skin.onReady(e)}}function e(){if(!A){return}Q.player.append(Q.skin.body,Q.dimensions);Q.skin.onShow(I)}function I(){if(!A){return}if(Q.player.onLoad){Q.player.onLoad()}Q.options.onFinish(Q.getCurrent());if(!Q.isPaused()){Q.play()}aq(true)}if(!Array.prototype.indexOf){Array.prototype.indexOf=function(S,aG){var K=this.length>>>0;aG=aG||0;if(aG<0){aG+=K}for(;aG-1;Q.plugins={fla:w.indexOf("Shockwave Flash")>-1,qt:w.indexOf("QuickTime")>-1,wmp:!ai&&w.indexOf("Windows Media")>-1,f4m:ai}}else{var p=function(K){var S;try{S=new ActiveXObject(K)}catch(aG){}return !!S};Q.plugins={fla:p("ShockwaveFlash.ShockwaveFlash"),qt:p("QuickTime.QuickTime"),wmp:p("wmplayer.ocx"),f4m:false}}var X=/^(light|shadow)box/i,am="shadowboxCacheKey",b=1;Q.cache={};Q.select=function(S){var aG=[];if(!S){var K;aF(document.getElementsByTagName("a"),function(aJ,aK){K=aK.getAttribute("rel");if(K&&X.test(K)){aG.push(aK)}})}else{var aI=S.length;if(aI){if(typeof S=="string"){if(Q.find){aG=Q.find(S)}}else{if(aI==2&&typeof S[0]=="string"&&S[1].nodeType){if(Q.find){aG=Q.find(S[0],S[1])}}else{for(var aH=0;aH{1} browser plugin to view this content.',shared:'You must install both the {1} and {3} browser plugins to view this content.',either:'You must install either the {1} or the {3} browser plugin to view this content.'}};var D,at="sb-drag-proxy",E,j,ag;function ax(){E={x:0,y:0,startX:null,startY:null}}function aA(){var K=Q.dimensions;aC(j.style,{height:K.innerHeight+"px",width:K.innerWidth+"px"})}function O(){ax();var K=["position:absolute","cursor:"+(Q.isGecko?"-moz-grab":"move"),"background-color:"+(Q.isIE?"#fff;filter:alpha(opacity=0)":"transparent")].join(";");Q.appendHTML(Q.skin.body,'
');j=ad(at);aA();F(j,"mousedown",L)}function B(){if(j){M(j,"mousedown",L);C(j);j=null}ag=null}function L(S){n(S);var K=V(S);E.startX=K[0];E.startY=K[1];ag=ad(Q.player.id);F(document,"mousemove",H);F(document,"mouseup",i);if(Q.isGecko){j.style.cursor="-moz-grabbing"}}function H(aI){var K=Q.player,aJ=Q.dimensions,aH=V(aI);var aG=aH[0]-E.startX;E.startX+=aG;E.x=Math.max(Math.min(0,E.x+aG),aJ.innerWidth-K.width);var S=aH[1]-E.startY;E.startY+=S;E.y=Math.max(Math.min(0,E.y+S),aJ.innerHeight-K.height);aC(ag.style,{left:E.x+"px",top:E.y+"px"})}function i(){M(document,"mousemove",H);M(document,"mouseup",i);if(Q.isGecko){j.style.cursor="-moz-grab"}}Q.img=function(S,aG){this.obj=S;this.id=aG;this.ready=false;var K=this;D=new Image();D.onload=function(){K.height=S.height?parseInt(S.height,10):D.height;K.width=S.width?parseInt(S.width,10):D.width;K.ready=true;D.onload=null;D=null};D.src=S.content};Q.img.ext=["bmp","gif","jpg","jpeg","png"];Q.img.prototype={append:function(S,aI){var aG=document.createElement("img");aG.id=this.id;aG.src=this.obj.content;aG.style.position="absolute";var K,aH;if(aI.oversized&&Q.options.handleOversize=="resize"){K=aI.innerHeight;aH=aI.innerWidth}else{K=this.height;aH=this.width}aG.setAttribute("height",K);aG.setAttribute("width",aH);S.appendChild(aG)},remove:function(){var K=ad(this.id);if(K){C(K)}B();if(D){D.onload=null;D=null}},onLoad:function(){var K=Q.dimensions;if(K.oversized&&Q.options.handleOversize=="drag"){O()}},onWindowResize:function(){var aH=Q.dimensions;switch(Q.options.handleOversize){case"resize":var K=ad(this.id);K.height=aH.innerHeight;K.width=aH.innerWidth;break;case"drag":if(ag){var aG=parseInt(Q.getStyle(ag,"top")),S=parseInt(Q.getStyle(ag,"left"));if(aG+this.height=aJ){clearInterval(S);S=null;aM(aG,aN);if(aR){aR()}}else{aM(aG,aO+aK((aI-aH)/aL)*aP)}},10)}function aB(){aa.style.height=Q.getWindowSize("Height")+"px";aa.style.width=Q.getWindowSize("Width")+"px"}function aE(){aa.style.top=document.documentElement.scrollTop+"px";aa.style.left=document.documentElement.scrollLeft+"px"}function ay(K){if(K){aF(Y,function(S,aG){aG[0].style.visibility=aG[1]||""})}else{Y=[];aF(Q.options.troubleElements,function(aG,S){aF(document.getElementsByTagName(S),function(aH,aI){Y.push([aI,aI.style.visibility]);aI.style.visibility="hidden"})})}}function r(aG,K){var S=ad("sb-nav-"+aG);if(S){S.style.display=K?"":"none"}}function ah(K,aJ){var aI=ad("sb-loading"),aG=Q.getCurrent().player,aH=(aG=="img"||aG=="html");if(K){Q.setOpacity(aI,0);aI.style.display="block";var S=function(){Q.clearOpacity(aI);if(aJ){aJ()}};if(aH){N(aI,"opacity",1,Q.options.fadeDuration,S)}else{S()}}else{var S=function(){aI.style.display="none";Q.clearOpacity(aI);if(aJ){aJ()}};if(aH){N(aI,"opacity",0,Q.options.fadeDuration,S)}else{S()}}}function t(aO){var aJ=Q.getCurrent();ad("sb-title-inner").innerHTML=aJ.title||"";var aP,aL,S,aQ,aM;if(Q.options.displayNav){aP=true;var aN=Q.gallery.length;if(aN>1){if(Q.options.continuous){aL=aM=true}else{aL=(aN-1)>Q.current;aM=Q.current>0}}if(Q.options.slideshowDelay>0&&Q.hasNext()){aQ=!Q.isPaused();S=!aQ}}else{aP=aL=S=aQ=aM=false}r("close",aP);r("next",aL);r("play",S);r("pause",aQ);r("previous",aM);var K="";if(Q.options.displayCounter&&Q.gallery.length>1){var aN=Q.gallery.length;if(Q.options.counterType=="skip"){var aI=0,aH=aN,aG=parseInt(Q.options.counterLimit)||0;if(aG2){var aK=Math.floor(aG/2);aI=Q.current-aK;if(aI<0){aI+=aN}aH=Q.current+(aG-aK);if(aH>aN){aH-=aN}}while(aI!=aH){if(aI==aN){aI=0}K+='"}}else{K=[Q.current+1,Q.lang.of,aN].join(" ")}}ad("sb-counter").innerHTML=K;aO()}function U(aH){var K=ad("sb-title-inner"),aG=ad("sb-info-inner"),S=0.35;K.style.visibility=aG.style.visibility="";if(K.innerHTML!=""){N(K,"marginTop",0,S)}N(aG,"marginTop",0,S,aH)}function av(aG,aM){var aK=ad("sb-title"),K=ad("sb-info"),aH=aK.offsetHeight,aI=K.offsetHeight,aJ=ad("sb-title-inner"),aL=ad("sb-info-inner"),S=(aG?0.35:0);N(aJ,"marginTop",aH,S);N(aL,"marginTop",aI*-1,S,function(){aJ.style.visibility=aL.style.visibility="hidden";aM()})}function ac(K,aH,S,aJ){var aI=ad("sb-wrapper-inner"),aG=(S?Q.options.resizeDuration:0);N(Z,"top",aH,aG);N(aI,"height",K,aG,aJ)}function ar(K,aH,S,aI){var aG=(S?Q.options.resizeDuration:0);N(Z,"left",aH,aG);N(Z,"width",K,aG,aI)}function ak(aM,aG){var aI=ad("sb-body-inner"),aM=parseInt(aM),aG=parseInt(aG),S=Z.offsetHeight-aI.offsetHeight,K=Z.offsetWidth-aI.offsetWidth,aK=ae.offsetHeight,aL=ae.offsetWidth,aJ=parseInt(Q.options.viewportPadding)||20,aH=(Q.player&&Q.options.handleOversize!="drag");return Q.setDimensions(aM,aG,aK,aL,S,K,aJ,aH)}var T={};T.markup='';T.options={animSequence:"sync",counterLimit:10,counterType:"default",displayCounter:true,displayNav:true,fadeDuration:0.35,initialHeight:160,initialWidth:320,modal:false,overlayColor:"#000",overlayOpacity:0.5,resizeDuration:0.35,showOverlay:true,troubleElements:["select","object","embed","canvas"]};T.init=function(){Q.appendHTML(document.body,s(T.markup,Q.lang));T.body=ad("sb-body-inner");aa=ad("sb-container");ae=ad("sb-overlay");Z=ad("sb-wrapper");if(!x){aa.style.position="absolute"}if(!h){var aG,K,S=/url\("(.*\.png)"\)/;aF(q,function(aI,aJ){aG=ad(aJ);if(aG){K=Q.getStyle(aG,"backgroundImage").match(S);if(K){aG.style.backgroundImage="none";aG.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,src="+K[1]+",sizingMethod=scale);"}}})}var aH;F(au,"resize",function(){if(aH){clearTimeout(aH);aH=null}if(A){aH=setTimeout(T.onWindowResize,10)}})};T.onOpen=function(K,aG){m=false;aa.style.display="block";aB();var S=ak(Q.options.initialHeight,Q.options.initialWidth);ac(S.innerHeight,S.top);ar(S.width,S.left);if(Q.options.showOverlay){ae.style.backgroundColor=Q.options.overlayColor;Q.setOpacity(ae,0);if(!Q.options.modal){F(ae,"click",Q.close)}ao=true}if(!x){aE();F(au,"scroll",aE)}ay();aa.style.visibility="visible";if(ao){N(ae,"opacity",Q.options.overlayOpacity,Q.options.fadeDuration,aG)}else{aG()}};T.onLoad=function(S,K){ah(true);while(T.body.firstChild){C(T.body.firstChild)}av(S,function(){if(!A){return}if(!S){Z.style.visibility="visible"}t(K)})};T.onReady=function(aH){if(!A){return}var S=Q.player,aG=ak(S.height,S.width);var K=function(){U(aH)};switch(Q.options.animSequence){case"hw":ac(aG.innerHeight,aG.top,true,function(){ar(aG.width,aG.left,true,K)});break;case"wh":ar(aG.width,aG.left,true,function(){ac(aG.innerHeight,aG.top,true,K)});break;default:ar(aG.width,aG.left,true);ac(aG.innerHeight,aG.top,true,K)}};T.onShow=function(K){ah(false,K);m=true};T.onClose=function(){if(!x){M(au,"scroll",aE)}M(ae,"click",Q.close);Z.style.visibility="hidden";var K=function(){aa.style.visibility="hidden";aa.style.display="none";ay(true)};if(ao){N(ae,"opacity",0,Q.options.fadeDuration,K)}else{K()}};T.onPlay=function(){r("play",false);r("pause",true)};T.onPause=function(){r("pause",false);r("play",true)};T.onWindowResize=function(){if(!m){return}aB();var K=Q.player,S=ak(K.height,K.width);ar(S.width,S.left);ac(S.innerHeight,S.top);if(K.onWindowResize){K.onWindowResize()}};Q.skin=T;au.Shadowbox=Q})(window); \ No newline at end of file +(function(window, undefined) { +/** + * The Shadowbox object. + * + * @type {Object} + * @public + */ +var S = { + + /** + * The current version of Shadowbox. + * + * @type {String} + * @public + */ + version: "3.0.3" + +} + +var ua = navigator.userAgent.toLowerCase(); + +// operating system detection +if (ua.indexOf('windows') > -1 || ua.indexOf('win32') > -1) { + S.isWindows = true; +} else if (ua.indexOf('macintosh') > -1 || ua.indexOf('mac os x') > -1) { + S.isMac = true; +} else if (ua.indexOf('linux') > -1) { + S.isLinux = true; +} + +// browser detection -- deprecated. the goal is to use object detection +// instead of the user agent string +S.isIE = ua.indexOf('msie') > -1; +S.isIE6 = ua.indexOf('msie 6') > -1; +S.isIE7 = ua.indexOf('msie 7') > -1; +S.isGecko = ua.indexOf('gecko') > -1 && ua.indexOf('safari') == -1; +S.isWebKit = ua.indexOf('applewebkit/') > -1; + +var inlineId = /#(.+)$/, + galleryName = /^(light|shadow)box\[(.*?)\]/i, + inlineParam = /\s*([a-z_]*?)\s*=\s*(.+)\s*/, + fileExtension = /[0-9a-z]+$/i, + scriptPath = /(.+\/)shadowbox\.js/i; + +/** + * True if Shadowbox is currently open, false otherwise. + * + * @type {Boolean} + * @private + */ +var open = false, + +/** + * True if Shadowbox has been initialized, false otherwise. + * + * @type {Boolean} + * @private + */ +initialized = false, + +/** + * The previous set of options that were used before Shadowbox.applyOptions was + * called. + * + * @type {Object} + * @private + */ +lastOptions = {}, + +/** + * The delay in milliseconds that the current gallery uses. + * + * @type {Number} + * @private + */ +slideDelay = 0, + +/** + * The time at which the current slideshow frame appeared. + * + * @type {Number} + * @private + */ +slideStart, + +/** + * The timeout id for the slideshow transition function. + * + * @type {Number} + * @private + */ +slideTimer; + +/** + * The index of the current object in the gallery array. + * + * @type {Number} + * @public + */ +S.current = -1; + +/** + * The current dimensions of Shadowbox. + * + * @type {Object} + * @public + */ +S.dimensions = null; + +/** + * Easing function used for animations. Based on a cubic polynomial. + * + * @param {Number} state The state of the animation (% complete) + * @return {Number} The adjusted easing value + * @public + */ +S.ease = function(state) { + return 1 + Math.pow(state - 1, 3); +} + +/** + * An object containing names of plugins and links to their respective download pages. + * + * @type {Object} + * @public + */ +S.errorInfo = { + fla: { + name: "Flash", + url: "http://www.adobe.com/products/flashplayer/" + }, + qt: { + name: "QuickTime", + url: "http://www.apple.com/quicktime/download/" + }, + wmp: { + name: "Windows Media Player", + url: "http://www.microsoft.com/windows/windowsmedia/" + }, + f4m: { + name: "Flip4Mac", + url: "http://www.flip4mac.com/wmv_download.htm" + } +}; + +/** + * The content objects in the current set. + * + * @type {Array} + * @public + */ +S.gallery = []; + +/** + * A function that will be called as soon as the DOM is ready. + * + * @type {Function} + * @public + */ +S.onReady = noop; + +/** + * The URL path to the Shadowbox script. + * + * @type {String} + * @public + */ +S.path = null; + +/** + * The current player object. + * + * @type {Object} + * @public + */ +S.player = null; + +/** + * The id to use for the Shadowbox player element. + * + * @type {String} + * @public + */ +S.playerId = "sb-player"; + +/** + * Various options that control Shadowbox' behavior. + * + * @type {Object} + * @public + */ +S.options = { + + /** + * True to enable animations. + * + * @type {Boolean} + */ + animate: true, + + /** + * True to enable opacity animations. + * + * @type {Boolean} + */ + animateFade: true, + + /** + * True to automatically play movies when the load. + * + * @type {Boolean} + */ + autoplayMovies: true, + + /** + * True to enable the user to skip to the first item in a gallery from the last using + * next. + * + * @type {Boolean} + */ + continuous: false, + + /** + * True to enable keyboard navigation. + * + * @type {Boolean} + */ + enableKeys: true, + + /** + * Parameters to pass to flash 's. + * + * @type {Object} + */ + flashParams: { + bgcolor: "#000000", + allowfullscreen: true + }, + + /** + * Variables to pass to flash 's. + * + * @type {Object} + */ + flashVars: {}, + + /** + * The minimum required Flash version. + * + * Note: The default is 9.0.115. This is the minimum version suggested by + * the JW FLV player. + * + * @type {String} + */ + flashVersion: "9.0.115", + + /** + * Determines how oversized content is handled. If set to "resize" the + * content will be resized while preserving aspect ratio. If "drag" will display + * the image at its original resolution but it will be draggable. If "none" will + * display the content at its original resolution but it may be cropped. + * + * @type {String} + */ + handleOversize: "resize", + + /** + * Determines how unsupported content is handled. If set to "remove" will + * remove the content from the gallery. If "link" will display a helpful + * link to a page where the necessary browser plugin can be installed. + * + * @type {String} + */ + handleUnsupported: "link", + + /** + * A hook function to be fired when changing from one gallery item to the + * next. Is passed the item that is about to be displayed as its only argument. + * + * @type {Function} + */ + onChange: noop, + + /** + * A hook function to be fired when closing. Is passed the most recent item + * as its only argument. + * + * @type {Function} + */ + onClose: noop, + + /** + * A hook funciton to be fires when content is finished loading. Is passed the + * current gallery item as its only argument. + * + * @type {Function} + */ + onFinish: noop, + + /** + * A hook function to be fired when opening. Is passed the current gallery item + * as its only argument. + * + * @type {Function} + */ + onOpen: noop, + + /** + * True to enable movie controls on movie players. + * + * @type {Boolean} + */ + showMovieControls: true, + + /** + * True to skip calling setup during init. + * + * @type {Boolean} + */ + skipSetup: false, + + /** + * The delay (in seconds) to use when displaying a gallery in slideshow mode. Setting + * this option to any value other than 0 will trigger slideshow mode. + * + * @type {Number} + */ + slideshowDelay: 0, + + /** + * The ammount of padding (in pixels) to maintain around the edge of the viewport at all + * times. + * + * @type {Number} + */ + viewportPadding: 20 + +}; + +/** + * Gets the object that is currently being displayed. + * + * @return {Object} + * @public + */ +S.getCurrent = function() { + return S.current > -1 ? S.gallery[S.current] : null; +} + +/** + * Returns true if there is another object to display after the current. + * + * @return {Boolean} + * @public + */ +S.hasNext = function() { + return S.gallery.length > 1 && (S.current != S.gallery.length - 1 || S.options.continuous); +} + +/** + * Returns true if Shadowbox is currently open. + * + * @return {Boolean} + * @public + */ +S.isOpen = function() { + return open; +} + +/** + * Returns true if Shadowbox is currently paused. + * + * @return {Boolean} + * @public + */ +S.isPaused = function() { + return slideTimer == "pause"; +} + +/** + * Applies the given set of options to Shadowbox' options. May be undone with revertOptions(). + * + * @param {Object} options + * @public + */ +S.applyOptions = function(options) { + lastOptions = apply({}, S.options); + apply(S.options, options); +} + +/** + * Reverts to whatever the options were before applyOptions() was called. + * + * @public + */ +S.revertOptions = function() { + apply(S.options, lastOptions); +} + +/** + * Initializes the Shadowbox environment. If options are given here, they + * will override the defaults. A callback may be provided that will be called + * when the document is ready. This function can be used for setting up links + * using Shadowbox.setup. + * + * @param {Object} options + * @param {Function} callback + * @public + */ +S.init = function(options, callback) { + if (initialized) + return; + + initialized = true; + + if (S.skin.options) + apply(S.options, S.skin.options); + + if (options) + apply(S.options, options); + + if (!S.path) { + // determine script path automatically + var path, scripts = document.getElementsByTagName("script"); + for (var i = 0, len = scripts.length; i < len; ++i) { + path = scriptPath.exec(scripts[i].src); + if (path) { + S.path = path[1]; + break; + } + } + } + + if (callback) + S.onReady = callback; + + bindLoad(); +} + +/** + * Opens the given object in Shadowbox. This object may be any of the following: + * + * - A URL specifying the location of some content to display + * - An HTML link object (A or AREA tag) that links to some content + * - A custom object similar to one produced by Shadowbox.makeObject + * - An array of any of the above + * + * Note: When a single link object is given, Shadowbox will automatically search + * for other cached link objects that have been set up in the same gallery and + * display them all together. + * + * @param {mixed} obj + * @public + */ +S.open = function(obj) { + if (open) + return; + + var gc = S.makeGallery(obj); + S.gallery = gc[0]; + S.current = gc[1]; + + obj = S.getCurrent(); + + if (obj == null) + return; + + S.applyOptions(obj.options || {}); + + filterGallery(); + + // anything left to display? + if (S.gallery.length) { + obj = S.getCurrent(); + + if (S.options.onOpen(obj) === false) + return; + + open = true; + + S.skin.onOpen(obj, load); + } +} + +/** + * Closes Shadowbox. + * + * @public + */ +S.close = function() { + if (!open) + return; + + open = false; + + if (S.player) { + S.player.remove(); + S.player = null; + } + + if (typeof slideTimer == "number") { + clearTimeout(slideTimer); + slideTimer = null; + } + slideDelay = 0; + + listenKeys(false); + + S.options.onClose(S.getCurrent()); + + S.skin.onClose(); + + S.revertOptions(); +} + +/** + * Starts a slideshow when a gallery is being displayed. Is called automatically + * when the slideshowDelay option is set to anything other than 0. + * + * @public + */ +S.play = function() { + if (!S.hasNext()) + return; + + if (!slideDelay) + slideDelay = S.options.slideshowDelay * 1000; + + if (slideDelay) { + slideStart = now(); + slideTimer = setTimeout(function(){ + slideDelay = slideStart = 0; // reset slideshow + S.next(); + }, slideDelay); + + if(S.skin.onPlay) + S.skin.onPlay(); + } +} + +/** + * Pauses a slideshow on the current object. + * + * @public + */ +S.pause = function() { + if (typeof slideTimer != "number") + return; + + slideDelay = Math.max(0, slideDelay - (now() - slideStart)); + + // if there's any time left on current slide, pause the timer + if (slideDelay) { + clearTimeout(slideTimer); + slideTimer = "pause"; + + if(S.skin.onPause) + S.skin.onPause(); + } +} + +/** + * Changes Shadowbox to display the item in the gallery specified by index. + * + * @param {Number} index + * @public + */ +S.change = function(index) { + if (!(index in S.gallery)) { + if (S.options.continuous) { + index = (index < 0 ? S.gallery.length + index : 0); // loop + if (!(index in S.gallery)) + return; + } else { + return; + } + } + + S.current = index; + + if (typeof slideTimer == "number") { + clearTimeout(slideTimer); + slideTimer = null; + slideDelay = slideStart = 0; + } + + S.options.onChange(S.getCurrent()); + + load(true); +} + +/** + * Advances to the next item in the gallery. + * + * @public + */ +S.next = function() { + S.change(S.current + 1); +} + +/** + * Rewinds to the previous gallery item. + * + * @public + */ +S.previous = function() { + S.change(S.current - 1); +} + +/** + * Calculates the dimensions for Shadowbox. + * + * @param {Number} height The height of the object + * @param {Number} width The width of the object + * @param {Number} maxHeight The maximum available height + * @param {Number} maxWidth The maximum available width + * @param {Number} topBottom The extra top/bottom required for borders/toolbars + * @param {Number} leftRight The extra left/right required for borders/toolbars + * @param {Number} padding The amount of padding (in pixels) to maintain around + * the edge of the viewport + * @param {Boolean} preserveAspect True to preserve the original aspect ratio when the + * given dimensions are too large + * @return {Object} The new dimensions object + * @public + */ +S.setDimensions = function(height, width, maxHeight, maxWidth, topBottom, leftRight, padding, preserveAspect) { + var originalHeight = height, + originalWidth = width; + + // constrain height/width to max + var extraHeight = 2 * padding + topBottom; + if (height + extraHeight > maxHeight) + height = maxHeight - extraHeight; + var extraWidth = 2 * padding + leftRight; + if (width + extraWidth > maxWidth) + width = maxWidth - extraWidth; + + // determine if object is oversized + var changeHeight = (originalHeight - height) / originalHeight, + changeWidth = (originalWidth - width) / originalWidth, + oversized = (changeHeight > 0 || changeWidth > 0); + + // adjust height/width if too large + if (preserveAspect && oversized) { + // preserve aspect ratio according to greatest change + if (changeHeight > changeWidth) { + width = Math.round((originalWidth / originalHeight) * height); + } else if (changeWidth > changeHeight) { + height = Math.round((originalHeight / originalWidth) * width); + } + } + + S.dimensions = { + height: height + topBottom, + width: width + leftRight, + innerHeight: height, + innerWidth: width, + top: Math.floor((maxHeight - (height + extraHeight)) / 2 + padding), + left: Math.floor((maxWidth - (width + extraWidth)) / 2 + padding), + oversized: oversized + }; + + return S.dimensions; +} + +/** + * Returns an array with two elements. The first is an array of objects that + * constitutes the gallery, and the second is the index of the given object in + * that array. + * + * @param {mixed} obj + * @return {Array} An array containing the gallery and current index + * @public + */ +S.makeGallery = function(obj) { + var gallery = [], current = -1; + + if (typeof obj == "string") + obj = [obj]; + + if (typeof obj.length == "number") { + each(obj, function(i, o) { + if (o.content) { + gallery[i] = o; + } else { + gallery[i] = {content: o}; + } + }); + current = 0; + } else { + if (obj.tagName) { + // check the cache for this object before building one on the fly + var cacheObj = S.getCache(obj); + obj = cacheObj ? cacheObj : S.makeObject(obj); + } + + if (obj.gallery) { + // gallery object, build gallery from cached gallery objects + gallery = []; + + var o; + for (var key in S.cache) { + o = S.cache[key]; + if (o.gallery && o.gallery == obj.gallery) { + if (current == -1 && o.content == obj.content) + current = gallery.length; + gallery.push(o); + } + } + + if (current == -1) { + gallery.unshift(obj); + current = 0; + } + } else { + // single object, no gallery + gallery = [obj]; + current = 0; + } + } + + // use apply to break references to each gallery object here because + // the code may modify certain properties of these objects from here + // on out and we want to preserve the original in case the same object + // is used again in a future call + each(gallery, function(i, o) { + gallery[i] = apply({}, o); + }); + + return [gallery, current]; +} + +/** + * Extracts parameters from a link element and returns an object containing + * (most of) the following keys: + * + * - content: The URL of the linked to content + * - player: The abbreviated name of the player to use for the object (can automatically + * be determined in most cases) + * - title: The title to use for the object (optional) + * - gallery: The name of the gallery the object belongs to (optional) + * - height: The height of the object (in pixels, only required for movies and Flash) + * - width: The width of the object (in pixels, only required for movies and Flash) + * - options: A set of options to use for this object (optional) + * - link: A reference to the original link element + * + * A custom set of options may be passed in here that will be applied when + * this object is displayed. However, any options that are specified in + * the link's HTML markup will trump options given here. + * + * @param {HTMLElement} link + * @param {Object} options + * @return {Object} An object representing the link + * @public + */ +S.makeObject = function(link, options) { + var obj = { + // accessing the href attribute directly here (instead of using + // getAttribute) should give a full URL instead of a relative one + content: link.href, + title: link.getAttribute("title") || "", + link: link + }; + + // remove link-level options from top-level options + if (options) { + options = apply({}, options); + each(["player", "title", "height", "width", "gallery"], function(i, o) { + if (typeof options[o] != "undefined") { + obj[o] = options[o]; + delete options[o]; + } + }); + obj.options = options; + } else { + obj.options = {}; + } + + if (!obj.player) + obj.player = S.getPlayer(obj.content); + + // HTML options always trump JavaScript options, so do these last + var rel = link.getAttribute("rel"); + if (rel) { + // extract gallery name from shadowbox[name] format + var match = rel.match(galleryName); + if (match) + obj.gallery = escape(match[2]); + + // extract any other parameters + each(rel.split(';'), function(i, p) { + match = p.match(inlineParam); + if (match) + obj[match[1]] = match[2]; + }); + } + + return obj; +} + +/** + * Attempts to automatically determine the correct player to use for an object based + * on its content attribute. Defaults to "iframe" when the content type cannot + * automatically be determined. + * + * @param {String} content The content attribute of the object + * @return {String} The name of the player to use + * @public + */ +S.getPlayer = function(content) { + if (content.indexOf("#") > -1 && content.indexOf(document.location.href) == 0) + return "inline"; + + // strip query string for player detection purposes + var q = content.indexOf("?"); + if (q > -1) + content = content.substring(0, q); + + // get file extension + var ext, m = content.match(fileExtension); + if (m) + ext = m[0].toLowerCase(); + + if (ext) { + if (S.img && S.img.ext.indexOf(ext) > -1) + return "img"; + if (S.swf && S.swf.ext.indexOf(ext) > -1) + return "swf"; + if (S.flv && S.flv.ext.indexOf(ext) > -1) + return "flv"; + if (S.qt && S.qt.ext.indexOf(ext) > -1) { + if (S.wmp && S.wmp.ext.indexOf(ext) > -1) { + return "qtwmp"; // can be played by either QuickTime or Windows Media Player + } else { + return "qt"; + } + } + if (S.wmp && S.wmp.ext.indexOf(ext) > -1) + return "wmp"; + } + + return "iframe"; +} + +/** + * Filters the current gallery for unsupported objects. + * + * @private + */ +function filterGallery() { + var err = S.errorInfo, plugins = S.plugins, obj, remove, needed, + m, format, replace, inlineEl, flashVersion; + + for (var i = 0; i < S.gallery.length; ++i) { + obj = S.gallery[i] + + remove = false; // remove the object? + needed = null; // what plugins are needed? + + switch (obj.player) { + case "flv": + case "swf": + if (!plugins.fla) + needed = "fla"; + break; + case "qt": + if (!plugins.qt) + needed = "qt"; + break; + case "wmp": + if (S.isMac) { + if (plugins.qt && plugins.f4m) { + obj.player = "qt"; + } else { + needed = "qtf4m"; + } + } else if (!plugins.wmp) { + needed = "wmp"; + } + break; + case "qtwmp": + if (plugins.qt) { + obj.player = "qt"; + } else if (plugins.wmp) { + obj.player = "wmp"; + } else { + needed = "qtwmp"; + } + break; + } + + // handle unsupported elements + if (needed) { + if (S.options.handleUnsupported == "link") { + // generate a link to the appropriate plugin download page(s) + switch (needed) { + case "qtf4m": + format = "shared"; + replace = [err.qt.url, err.qt.name, err.f4m.url, err.f4m.name]; + break; + case "qtwmp": + format = "either"; + replace = [err.qt.url, err.qt.name, err.wmp.url, err.wmp.name]; + break; + default: + format = "single"; + replace = [err[needed].url, err[needed].name]; + } + + obj.player = "html"; + obj.content = '
' + sprintf(S.lang.errors[format], replace) + '
'; + } else { + remove = true; + } + } else if (obj.player == "inline") { + // inline element, retrieve innerHTML + m = inlineId.exec(obj.content); + if (m) { + inlineEl = get(m[1]); + if (inlineEl) { + obj.content = inlineEl.innerHTML; + } else { + // cannot find element with id + remove = true; + } + } else { + // cannot determine element id from content string + remove = true; + } + } else if (obj.player == "swf" || obj.player == "flv") { + flashVersion = (obj.options && obj.options.flashVersion) || S.options.flashVersion; + + if (S.flash && !S.flash.hasFlashPlayerVersion(flashVersion)) { + // express install will be triggered because the client does not meet the + // minimum required version of Flash. set height and width to those of expressInstall.swf + obj.width = 310; + // minimum height is 127, but +20 pixels on top and bottom looks better + obj.height = 177; + } + } + + if (remove) { + S.gallery.splice(i, 1); + + if (i < S.current) { + --S.current; // maintain integrity of S.current + } else if (i == S.current) { + S.current = i > 0 ? i - 1 : i; // look for supported neighbor + } + + // decrement index for next loop + --i; + } + } +} + +/** + * Sets up a listener on the document for keydown events. + * + * @param {Boolean} on True to enable the listener, false to disable + * @private + */ +function listenKeys(on) { + if (!S.options.enableKeys) + return; + + (on ? addEvent : removeEvent)(document, "keydown", handleKey); +} + +/** + * A listener function that is fired when a key is pressed. + * + * @param {Event} e The keydown event + * @private + */ +function handleKey(e) { + // don't handle events with modifier keys + if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey) + return; + + var code = keyCode(e), handler; + + switch (code) { + case 81: // q + case 88: // x + case 27: // esc + handler = S.close; + break; + case 37: // left + handler = S.previous; + break; + case 39: // right + handler = S.next; + break; + case 32: // space + handler = typeof slideTimer == "number" ? S.pause : S.play; + break; + } + + if (handler) { + preventDefault(e); + handler(); + } +} + +/** + * Loads the current object. + * + * @param {Boolean} True if changing from a previous object + * @private + */ +function load(changing) { + listenKeys(false); + + var obj = S.getCurrent(); + + // determine player, inline is really just html + var player = (obj.player == "inline" ? "html" : obj.player); + + if (typeof S[player] != "function") + throw "unknown player " + player; + + if (changing) { + S.player.remove(); + S.revertOptions(); + S.applyOptions(obj.options || {}); + } + + S.player = new S[player](obj, S.playerId); + + // preload neighboring gallery images + if (S.gallery.length > 1) { + var next = S.gallery[S.current + 1] || S.gallery[0]; + if (next.player == "img") { + var a = new Image(); + a.src = next.content; + } + var prev = S.gallery[S.current - 1] || S.gallery[S.gallery.length - 1]; + if (prev.player == "img") { + var b = new Image(); + b.src = prev.content; + } + } + + S.skin.onLoad(changing, waitReady); +} + +/** + * Waits until the current object is ready to be displayed. + * + * @private + */ +function waitReady() { + if (!open) + return; + + if (typeof S.player.ready != "undefined") { + // wait for content to be ready before loading + var timer = setInterval(function() { + if (open) { + if (S.player.ready) { + clearInterval(timer); + timer = null; + S.skin.onReady(show); + } + } else { + clearInterval(timer); + timer = null; + } + }, 10); + } else { + S.skin.onReady(show); + } +} + +/** + * Displays the current object. + * + * @private + */ +function show() { + if (!open) + return; + + S.player.append(S.skin.body, S.dimensions); + + S.skin.onShow(finish); +} + +/** + * Finishes up any remaining tasks after the object is displayed. + * + * @private + */ +function finish() { + if (!open) + return; + + if (S.player.onLoad) + S.player.onLoad(); + + S.options.onFinish(S.getCurrent()); + + if (!S.isPaused()) + S.play(); // kick off next slide + + listenKeys(true); +} +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(obj, from) { + var len = this.length >>> 0; + + from = from || 0; + if (from < 0) + from += len; + + for (; from < len; ++from) { + if (from in this && this[from] === obj) + return from; + } + + return -1; + } +} + +/** + * Gets the current time in milliseconds. + * + * @return {Number} + * @private + */ +function now() { + return (new Date).getTime(); +} + +/** + * Applies all properties of extension to original. + * + * @param {Object} original + * @param {Object} extension + * @return {Object} The original object + * @private + */ +function apply(original, extension) { + for (var property in extension) + original[property] = extension[property]; + return original; +} + +/** + * Calls the given callback function for each element in obj. Note: obj must be an array-like + * object. + * + * @param {Array|mixed} obj + * @param {Function} callback + * @private + */ +function each(obj, callback) { + var i = 0, len = obj.length; + for (var value = obj[0]; i < len && callback.call(value, i, value) !== false; value = obj[++i]) {} +} + +/** + * Formats a string with the elements in the replacement array. The string should contain + * tokens in the format {n} where n corresponds to the index of property name of the replacement + * in the replace object. + * + * Example: + * + * format('Hello {0}', ['World']); // "Hello World" + * format('Hello {world}', {world: "World"}); // "Hello World" + * + * @param {String} str The format spec string + * @param {Array|Object} replace The array/object of replacement values + * @return {String} The formatted string + * @private + */ +function sprintf(str, replace) { + return str.replace(/\{(\w+?)\}/g, function(match, i) { + return replace[i]; + }); +} + +/** + * A no-op function. + * + * @private + */ +function noop() {} + +/** + * Gets the element with the given id. + * + * @param {String} id + * @return {HTMLElement} + * @private + */ +function get(id) { + return document.getElementById(id); +} + +/** + * Removes an element from the DOM. + * + * @param {HTMLElement} el The element to remove + * @private + */ +function remove(el) { + el.parentNode.removeChild(el); +} + +/** + * True if this browser supports opacity. + * + * @type {Boolean} + * @private + */ +var supportsOpacity = true, + +/** + * True if the browser supports fixed positioning. + * + * @type {Boolean} + * @private + */ +supportsFixed = true; + +/** + * Checks the level of support the browser provides. Should be called when + * the DOM is ready to be manipulated. + * + * @private + */ +function checkSupport() { + var body = document.body, + div = document.createElement("div"); + + // detect opacity support + supportsOpacity = typeof div.style.opacity === "string"; + + // detect support for fixed positioning + div.style.position = "fixed"; + div.style.margin = 0; + div.style.top = "20px"; + body.appendChild(div, body.firstChild); + supportsFixed = div.offsetTop == 20; + body.removeChild(div); +} + +/** + * Gets the computed value of the style on the given element. + * + * Note: This function is not safe for retrieving float values or non-pixel values + * in IE. + * + * @param {HTMLElement} el The element + * @param {String} style The camel-cased name of the style + * @return {mixed} The computed value of the given style + * @public + */ +S.getStyle = (function() { + var opacity = /opacity=([^)]*)/, + getComputedStyle = document.defaultView && document.defaultView.getComputedStyle; + + return function(el, style) { + var ret; + + if (!supportsOpacity && style == "opacity" && el.currentStyle) { + ret = opacity.test(el.currentStyle.filter || "") ? (parseFloat(RegExp.$1) / 100) + "" : ""; + return ret === "" ? "1" : ret; + } + + if (getComputedStyle) { + var computedStyle = getComputedStyle(el, null); + + if (computedStyle) + ret = computedStyle[style]; + + if (style == "opacity" && ret == "") + ret = "1"; + } else { + ret = el.currentStyle[style]; + } + + return ret; + } +})(); + +/** + * Appends an HTML fragment to the given element. + * + * @param {HTMLElement} el + * @param {String} html The HTML fragment to use + * @public + */ +S.appendHTML = function(el, html) { + if (el.insertAdjacentHTML) { + el.insertAdjacentHTML("BeforeEnd", html); + } else if (el.lastChild) { + var range = el.ownerDocument.createRange(); + range.setStartAfter(el.lastChild); + var frag = range.createContextualFragment(html); + el.appendChild(frag); + } else { + el.innerHTML = html; + } +} + +/** + * Gets the window size. The dimension may be either "Height" or "Width". + * + * @param {String} dimension + * @return {Number} + * @public + */ +S.getWindowSize = function(dimension) { + if (document.compatMode === "CSS1Compat") + return document.documentElement["client" + dimension]; + + return document.body["client" + dimension]; +} + +/** + * Sets an element's opacity. + * + * @param {HTMLElement} el + * @param {Number} opacity + * @public + */ +S.setOpacity = function(el, opacity) { + var style = el.style; + if (supportsOpacity) { + style.opacity = (opacity == 1 ? "" : opacity); + } else { + style.zoom = 1; // trigger hasLayout + if (opacity == 1) { + if (typeof style.filter == "string" && (/alpha/i).test(style.filter)) + style.filter = style.filter.replace(/\s*[\w\.]*alpha\([^\)]*\);?/gi, ""); + } else { + style.filter = (style.filter || "").replace(/\s*[\w\.]*alpha\([^\)]*\)/gi, "") + + " alpha(opacity=" + (opacity * 100) + ")"; + } + } +} + +/** + * Clears the opacity setting on the given element. Needed for some cases in IE. + * + * @param {HTMLElement} el + * @public + */ +S.clearOpacity = function(el) { + S.setOpacity(el, 1); +} +/** + * The jQuery adapter for Shadowbox. + */ + +/** + * Gets the target of the given event. The event object passed will be + * the same object that is passed to listeners registered with + * addEvent(). + * + * @param {Event} e The event object + * @return {HTMLElement} The event's target element + * @private + */ +function getTarget(e) { + return e.target; +} + +/** + * Gets the page X/Y coordinates of the mouse event in an [x, y] array. + * The page coordinates should be relative to the document, and not the + * viewport. The event object provided here will be the same object that + * is passed to listeners registered with addEvent(). + * + * @param {Event} e The event object + * @return {Array} The page X/Y coordinates + * @private + */ +function getPageXY(e) { + return [e.pageX, e.pageY]; +} + +/** + * Prevents the event's default behavior. The event object passed will + * be the same object that is passed to listeners registered with + * addEvent(). + * + * @param {Event} e The event object + * @private + */ +function preventDefault(e) { + e.preventDefault(); +} + +/** + * Gets the key code of the given event object (keydown). The event + * object here will be the same object that is passed to listeners + * registered with addEvent(). + * + * @param {Event} e The event object + * @return {Number} The key code of the event + * @private + */ +function keyCode(e) { + return e.keyCode; +} + +/** + * Adds an event handler to the given element. The handler should be called + * in the scope of the element with the event object as its only argument. + * + * @param {HTMLElement} el The element to listen to + * @param {String} type The type of the event to add + * @param {Function} handler The event handler function + * @private + */ +function addEvent(el, type, handler) { + jQuery(el).bind(type, handler); +} + +/** + * Removes an event handler from the given element. + * + * @param {HTMLElement} el The DOM element to stop listening to + * @param {String} type The type of the event to remove + * @param {Function} handler The event handler function + * @private + */ +function removeEvent(el, type, handler) { + jQuery(el).unbind(type, handler); +} + +/** + * Passes the selected elements to the Shadowbox.setup() function. Supports + * embedded height and width attributes within the class attribute. + * + * @param {Object} options The options to pass to setup() for all selected elements + * @return {Object} The jQuery object + * @public + * @author Mike Alsup + * @author Roger Barrett + */ +jQuery.fn.shadowbox = function(options) { + return this.each(function() { + var el = jQuery(this); + // support jQuery metadata plugin + var opts = jQuery.extend({}, options || {}, jQuery.metadata ? el.metadata() : jQuery.meta ? el.data() : {}); + // support embedded opts (for w/h) within the class attr + var cls = this.className || ''; + opts.width = parseInt((cls.match(/w:(\d+)/)||[])[1]) || opts.width; + opts.height = parseInt((cls.match(/h:(\d+)/)||[])[1]) || opts.height; + Shadowbox.setup(el, opts); + }); +} +// The code in this file is adapted for Shadowbox from the jQuery JavaScript library + +/** + * True if Shadowbox has been loaded into the DOM, false otherwise. + * + * @type {Boolean} + * @private + */ +var loaded = false, + +/** + * The callback function for the DOMContentLoaded browser event. + * + * @type {Function} + * @private + */ +DOMContentLoaded; + +if (document.addEventListener) { + DOMContentLoaded = function() { + document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false); + S.load(); + } +} else if (document.attachEvent) { + DOMContentLoaded = function() { + if (document.readyState === "complete") { + document.detachEvent("onreadystatechange", DOMContentLoaded); + S.load(); + } + } +} + +/** + * A DOM ready check for IE. + * + * @private + */ +function doScrollCheck() { + if (loaded) + return; + + try { + document.documentElement.doScroll("left"); + } catch (e) { + setTimeout(doScrollCheck, 1); + return; + } + + S.load(); +} + +/** + * Waits for the DOM to be ready before firing the given callback function. + * + * @param {Function} callback + * @private + */ +function bindLoad() { + if (document.readyState === "complete") + return S.load(); + + if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", DOMContentLoaded, false); + window.addEventListener("load", S.load, false); + } else if (document.attachEvent) { + document.attachEvent("onreadystatechange", DOMContentLoaded); + window.attachEvent("onload", S.load); + + var topLevel = false; + try { + topLevel = window.frameElement === null; + } catch (e) {} + + if (document.documentElement.doScroll && topLevel) + doScrollCheck(); + } +} + +/** + * Loads the Shadowbox code into the DOM. Is called automatically when the document + * is ready. + * + * @public + */ +S.load = function() { + if (loaded) + return; + + if (!document.body) + return setTimeout(S.load, 13); + + loaded = true; + + checkSupport(); + + S.onReady(); + + if (!S.options.skipSetup) + S.setup(); + + S.skin.init(); +} +/** + * Contains plugin support information. Each property of this object is a + * boolean indicating whether that plugin is supported. Keys are: + * + * - fla: Flash player + * - qt: QuickTime player + * - wmp: Windows Media player + * - f4m: Flip4Mac plugin + * + * @type {Object} + * @public + */ +S.plugins = {}; + +if (navigator.plugins && navigator.plugins.length) { + var names = []; + each(navigator.plugins, function(i, p) { + names.push(p.name); + }); + names = names.join(','); + + var f4m = names.indexOf('Flip4Mac') > -1; + + S.plugins = { + fla: names.indexOf('Shockwave Flash') > -1, + qt: names.indexOf('QuickTime') > -1, + wmp: !f4m && names.indexOf('Windows Media') > -1, // if it's Flip4Mac, it's not really WMP + f4m: f4m + }; +} else { + var detectPlugin = function(name) { + var axo; + try { + axo = new ActiveXObject(name); + } catch(e) {} + return !!axo; + } + + S.plugins = { + fla: detectPlugin('ShockwaveFlash.ShockwaveFlash'), + qt: detectPlugin('QuickTime.QuickTime'), + wmp: detectPlugin('wmplayer.ocx'), + f4m: false + }; +} +// used to match the rel attribute of links +var relAttr = /^(light|shadow)box/i, + +/** + * The name of the expando property that Shadowbox uses on HTML elements + * to store the cache index of that element. + * + * @type {String} + * @private + */ +expando = "shadowboxCacheKey", + +/** + * A unique id counter. + * + * @type {Number} + * @private + */ +cacheKey = 1; + +/** + * Contains all link objects that have been cached. + * + * @type {Object} + * @public + */ +S.cache = {}; + +/** + * Resolves a link selector. The selector may be omitted to select all anchor elements + * on the page with rel="shadowbox" or, if Shadowbox.find is used, it may be a single CSS + * selector or an array of [selector, [context]]. + * + * @param {mixed} selector + * @return {Array} An array of matching link elements + * @public + */ +S.select = function(selector) { + var links = []; + + if (!selector) { + var rel; + each(document.getElementsByTagName("a"), function(i, el) { + rel = el.getAttribute("rel"); + if (rel && relAttr.test(rel)) + links.push(el); + }); + } else { + var length = selector.length; + if (length) { + if (typeof selector == "string") { + if (S.find) + links = S.find(selector); // css selector + } else if (length == 2 && typeof selector[0] == "string" && selector[1].nodeType) { + if (S.find) + links = S.find(selector[0], selector[1]); // css selector + context + } else { + // array of links (or node list) + for (var i = 0; i < length; ++i) + links[i] = selector[i]; + } + } else { + links.push(selector); // single link + } + } + + return links; +} + +/** + * Adds all links specified by the given selector to the cache. If no selector + * is provided, will select every anchor element on the page with rel="shadowbox". + * + * Note: Options given here apply only to links selected by the given selector. + * Also, because elements do not support the rel attribute, they must be + * explicitly passed to this method. + * + * @param {mixed} selector + * @param {Object} options Some options to use for the given links + * @public + */ +S.setup = function(selector, options) { + each(S.select(selector), function(i, link) { + S.addCache(link, options); + }); +} + +/** + * Removes all links specified by the given selector from the cache. + * + * @param {mixed} selector + * @public + */ +S.teardown = function(selector) { + each(S.select(selector), function(i, link) { + S.removeCache(link); + }); +} + +/** + * Adds the given link element to the cache with the given options. + * + * @param {HTMLElement} link + * @param {Object} options + * @public + */ +S.addCache = function(link, options) { + var key = link[expando]; + + if (key == undefined) { + key = cacheKey++; + // assign cache key expando, use integer primitive to avoid memory leak in IE + link[expando] = key; + // add onclick listener + addEvent(link, "click", handleClick); + } + + S.cache[key] = S.makeObject(link, options); +} + +/** + * Removes the given link element from the cache. + * + * @param {HTMLElement} link + * @public + */ +S.removeCache = function(link) { + removeEvent(link, "click", handleClick); + delete S.cache[link[expando]]; + link[expando] = null; +} + +/** + * Gets the object from cache representative of the given link element (if there is one). + * + * @param {HTMLElement} link + * @return {Object} + * @public + */ +S.getCache = function(link) { + var key = link[expando]; + return (key in S.cache && S.cache[key]); +} + +/** + * Removes all onclick listeners from elements that have previously been setup with + * Shadowbox and clears all objects from cache. + * + * @public + */ +S.clearCache = function() { + for (var key in S.cache) + S.removeCache(S.cache[key].link); + + S.cache = {}; +} + +/** + * Handles all clicks on links that have been set up to work with Shadowbox + * and cancels the default event behavior when appropriate. + * + * @param {Event} e The click event + * @private + */ +function handleClick(e) { + //preventDefault(e); // good for debugging + + S.open(this); + + if (S.gallery.length) + preventDefault(e); +} +/** + * The English language translation for Shadowbox. + */ + +S.lang = { + code: 'en', + of: 'of', + loading: 'loading', + cancel: 'Cancel', + next: 'Next', + previous: 'Previous', + play: 'Play', + pause: 'Pause', + close: 'Close', + errors: { + single: 'You must install the {1} browser plugin to view this content.', + shared: 'You must install both the {1} and {3} browser plugins to view this content.', + either: 'You must install either the {1} or the {3} browser plugin to view this content.' + } +} +/** + * The iframe player for Shadowbox. + */ + +/** + * Constructor. The iframe player class for Shadowbox. + * + * @constructor + * @param {Object} obj The content object + * @param {String} id The player id + * @public + */ +S.iframe = function(obj, id) { + this.obj = obj; + this.id = id; + + // height/width default to full viewport height/width + var overlay = get("sb-overlay"); + this.height = obj.height ? parseInt(obj.height, 10) : overlay.offsetHeight; + this.width = obj.width ? parseInt(obj.width, 10) : overlay.offsetWidth; +} + +S.iframe.prototype = { + + /** + * Appends this iframe to the DOM. + * + * @param {HTMLElement} body The body element + * @param {Object} dims The current Shadowbox dimensions + * @public + */ + append: function(body, dims) { + var html = ''; + + // use innerHTML method of insertion here instead of appendChild + // because IE renders frameborder otherwise + body.innerHTML = html; + }, + + /** + * Removes this iframe from the DOM. + * + * @public + */ + remove: function() { + var el = get(this.id); + if (el) { + remove(el); + if (S.isGecko) + delete window.frames[this.id]; // needed for Firefox + } + }, + + /** + * An optional callback function to process after this content has been loaded. + * + * @public + */ + onLoad: function() { + var win = S.isIE ? get(this.id).contentWindow : window.frames[this.id]; + win.location.href = this.obj.content; + } + +} +/** + * The image player for Shadowbox. + */ + +/** + * Resource used to preload images. It's class-level so that when a new image is requested, + * the same resource can be reassigned, cancelling the original's callback. + * + * @type {Image} + * @private + */ +var pre, + +/** + * The id to use for the drag proxy element. + * + * @type {String} + * @private + */ +proxyId = "sb-drag-proxy", + +/** + * Keeps track of 4 floating values (x, y, startx, & starty) that are used in the drag calculations. + * + * @type {Object} + * @private + */ +dragData, + +/** + * The transparent element that is used to listen for drag events. + * + * @type {HTMLElement} + * @private + */ +dragProxy, + +/** + * The draggable element. + * + * @type {HTMLElement} + * @private + */ +dragTarget; + +/** + * Resets the class drag variable. + * + * @private + */ +function resetDrag() { + dragData = { + x: 0, + y: 0, + startX: null, + startY: null + }; +} + +/** + * Updates the drag proxy dimensions. + * + * @private + */ +function updateProxy() { + var dims = S.dimensions; + apply(dragProxy.style, { + height: dims.innerHeight + "px", + width: dims.innerWidth + "px" + }); +} + +/** + * Enables a transparent drag layer on top of images. + * + * @private + */ +function enableDrag() { + resetDrag(); + + // add transparent proxy layer to prevent browser dragging of actual image + var style = [ + "position:absolute", + "cursor:" + (S.isGecko ? "-moz-grab" : "move"), + "background-color:" + (S.isIE ? "#fff;filter:alpha(opacity=0)" : "transparent") + ].join(";"); + S.appendHTML(S.skin.body, '
'); + + dragProxy = get(proxyId); + updateProxy(); + + addEvent(dragProxy, "mousedown", startDrag); +} + +/** + * Disables the drag layer. + * + * @private + */ +function disableDrag() { + if (dragProxy) { + removeEvent(dragProxy, "mousedown", startDrag); + remove(dragProxy); + dragProxy = null; + } + + dragTarget = null; +} + +/** + * Sets up a drag listener on the document. + * + * @param {Event} e The mousedown event + * @private + */ +function startDrag(e) { + // prevent browser dragging + preventDefault(e); + + var xy = getPageXY(e); + dragData.startX = xy[0]; + dragData.startY = xy[1]; + + dragTarget = get(S.player.id); + + addEvent(document, "mousemove", positionDrag); + addEvent(document, "mouseup", endDrag); + + if (S.isGecko) + dragProxy.style.cursor = "-moz-grabbing"; +} + +/** + * Positions an oversized image on drag. + * + * @param {Event} e The mousemove event + * @private + */ +function positionDrag(e) { + var player = S.player, + dims = S.dimensions, + xy = getPageXY(e); + + var moveX = xy[0] - dragData.startX; + dragData.startX += moveX; + dragData.x = Math.max(Math.min(0, dragData.x + moveX), dims.innerWidth - player.width); + + var moveY = xy[1] - dragData.startY; + dragData.startY += moveY; + dragData.y = Math.max(Math.min(0, dragData.y + moveY), dims.innerHeight - player.height); + + apply(dragTarget.style, { + left: dragData.x + "px", + top: dragData.y + "px" + }); +} + +/** + * Removes the drag listener from the document. + * + * @private + */ +function endDrag() { + removeEvent(document, "mousemove", positionDrag); + removeEvent(document, "mouseup", endDrag); + + if (S.isGecko) + dragProxy.style.cursor = "-moz-grab"; +} + +/** + * Constructor. The image player class for Shadowbox. + * + * @constructor + * @param {Object} obj The content object + * @param {String} id The player id + * @public + */ +S.img = function(obj, id) { + this.obj = obj; + this.id = id; + + // preload the image + this.ready = false; + var self = this; + pre = new Image(); + pre.onload = function() { + // height/width defaults to image height/width + self.height = obj.height ? parseInt(obj.height, 10) : pre.height; + self.width = obj.width ? parseInt(obj.width, 10) : pre.width; + + // ready to go + self.ready = true; + + // clean up to prevent memory leak in IE + pre.onload = null; + pre = null; + } + pre.src = obj.content; +} + +S.img.ext = ["bmp", "gif", "jpg", "jpeg", "png"]; + +S.img.prototype = { + + /** + * Appends this image to the document. + * + * @param {HTMLElement} body The body element + * @param {Object} dims The current Shadowbox dimensions + * @public + */ + append: function(body, dims) { + var img = document.createElement("img"); + img.id = this.id; + img.src = this.obj.content; + img.style.position = "absolute"; + + var height, width; + if (dims.oversized && S.options.handleOversize == "resize") { + height = dims.innerHeight; + width = dims.innerWidth; + } else { + height = this.height; + width = this.width; + } + + // need to use setAttribute here for IE's sake + img.setAttribute("height", height); + img.setAttribute("width", width); + + body.appendChild(img); + }, + + /** + * Removes this image from the document. + * + * @public + */ + remove: function() { + var el = get(this.id); + if (el) + remove(el); + + disableDrag(); + + // prevent old image requests from loading + if (pre) { + pre.onload = null; + pre = null; + } + }, + + /** + * An optional callback function to process after this content has been + * loaded. + * + * @public + */ + onLoad: function() { + var dims = S.dimensions; + + // listen for drag when image is oversized + if (dims.oversized && S.options.handleOversize == "drag") + enableDrag(); + }, + + /** + * Called when the window is resized. + * + * @public + */ + onWindowResize: function() { + var dims = S.dimensions; + + switch (S.options.handleOversize) { + case "resize": + var el = get(this.id); + el.height = dims.innerHeight; + el.width = dims.innerWidth; + break; + case "drag": + if (dragTarget) { + var top = parseInt(S.getStyle(dragTarget, "top")), + left = parseInt(S.getStyle(dragTarget, "left")); + + // fix positioning when viewport is enlarged + if (top + this.height < dims.innerHeight) + dragTarget.style.top = dims.innerHeight - this.height + "px"; + if (left + this.width < dims.innerWidth) + dragTarget.style.left = dims.innerWidth - this.width + "px"; + + updateProxy(); + } + break; + } + } + +} +/** + * Keeps track of whether or not the overlay is activated. + * + * @type {Boolean} + * @private + */ +var overlayOn = false, + +/** + * A cache of elements that are troublesome for modal overlays. + * + * @type {Array} + * @private + */ +visibilityCache = [], + +/** + * Id's of elements that need transparent PNG support. + * + * @type {Array} + * @private + */ +pngIds = [ + "sb-nav-close", + "sb-nav-next", + "sb-nav-play", + "sb-nav-pause", + "sb-nav-previous" +], + +/** + * The container element. + * + * @type {HTMLElement} + * @private + */ +container, + +/** + * The overlay element. + * + * @type {HTMLElement} + * @private + */ +overlay, + +/** + * The wrapper element. + * + * @type {HTMLElement} + * @private + */ +wrapper, + +/** + * True if the window resize event is allowed to fire. + * + * @type {Boolean} + * @private + */ +doWindowResize = true; + +/** + * Animates the given property of el to the given value over a specified duration. If a + * callback is provided, it will be called when the animation is finished. + * + * @param {HTMLElement} el + * @param {String} property + * @param {mixed} to + * @param {Number} duration + * @param {Function} callback + * @private + */ +function animate(el, property, to, duration, callback) { + var isOpacity = (property == "opacity"), + anim = isOpacity ? S.setOpacity : function(el, value) { + // default unit is px for properties other than opacity + el.style[property] = "" + + value + "px"; + }; + + if (duration == 0 || (!isOpacity && !S.options.animate) || (isOpacity && !S.options.animateFade)) { + anim(el, to); + if (callback) + callback(); + return; + } + + var from = parseFloat(S.getStyle(el, property)) || 0; + var delta = to - from; + if (delta == 0) { + if (callback) + callback(); + return; // nothing to animate + } + + duration *= 1000; // convert to milliseconds + + var begin = now(), + ease = S.ease, + end = begin + duration, + time; + + var interval = setInterval(function() { + time = now(); + if (time >= end) { + clearInterval(interval); + interval = null; + anim(el, to); + if (callback) + callback(); + } else { + anim(el, from + ease((time - begin) / duration) * delta); + } + }, 10); // 10 ms interval is minimum on WebKit +} + +/** + * Sets the size of the container element. + * + * @private + */ +function setSize() { + container.style.height = S.getWindowSize("Height") + "px"; + container.style.width = S.getWindowSize("Width") + "px"; +} + +/** + * Sets the top of the container element. This is only necessary in browsers that + * don't support fixed positioning, such as IE6. + * + * @private + */ +function setPosition() { + container.style.top = document.documentElement.scrollTop + "px"; + container.style.left = document.documentElement.scrollLeft + "px"; +} + +/** + * Toggles the visibility of elements that are troublesome for overlays. + * + * @param {Boolean} on True to make visible, false to hide + * @private + */ +function toggleTroubleElements(on) { + if (on) { + each(visibilityCache, function(i, el){ + el[0].style.visibility = el[1] || ''; + }); + } else { + visibilityCache = []; + each(S.options.troubleElements, function(i, tag) { + each(document.getElementsByTagName(tag), function(j, el) { + visibilityCache.push([el, el.style.visibility]); + el.style.visibility = "hidden"; + }); + }); + } +} + +/** + * Toggles the display of the nav control with the given id. + * + * @param {String} id The id of the navigation control + * @param {Boolean} on True to toggle on, false to toggle off + * @private + */ +function toggleNav(id, on) { + var el = get("sb-nav-" + id); + if (el) + el.style.display = on ? "" : "none"; +} + +/** + * Toggles the visibility of the loading layer. + * + * @param {Boolean} on True to toggle on, false to toggle off + * @param {Function} callback The callback to use when finished + * @private + */ +function toggleLoading(on, callback) { + var loading = get("sb-loading"), + playerName = S.getCurrent().player, + anim = (playerName == "img" || playerName == "html"); // fade on images & html + + if (on) { + S.setOpacity(loading, 0); + loading.style.display = "block"; + + var wrapped = function() { + S.clearOpacity(loading); + if (callback) + callback(); + } + + if (anim) { + animate(loading, "opacity", 1, S.options.fadeDuration, wrapped); + } else { + wrapped(); + } + } else { + var wrapped = function() { + loading.style.display = "none"; + S.clearOpacity(loading); + if (callback) + callback(); + } + + if (anim) { + animate(loading, "opacity", 0, S.options.fadeDuration, wrapped); + } else { + wrapped(); + } + } +} + +/** + * Builds the content for the title and information bars. + * + * @param {Function} callback The callback to use when finished + * @private + */ +function buildBars(callback) { + var obj = S.getCurrent(); + + get("sb-title-inner").innerHTML = obj.title || ""; + + // build the nav + var close, next, play, pause, previous; + if (S.options.displayNav) { + close = true; + var len = S.gallery.length; + if (len > 1) { + if (S.options.continuous) { + next = previous = true; + } else { + next = (len - 1) > S.current; // not last in gallery, show next + previous = S.current > 0; // not first in gallery, show previous + } + } + // in a slideshow? + if (S.options.slideshowDelay > 0 && S.hasNext()) { + pause = !S.isPaused(); + play = !pause; + } + } else { + close = next = play = pause = previous = false; + } + toggleNav("close", close); + toggleNav("next", next); + toggleNav("play", play); + toggleNav("pause", pause); + toggleNav("previous", previous); + + // build the counter + var counter = ""; + if (S.options.displayCounter && S.gallery.length > 1) { + var len = S.gallery.length; + if (S.options.counterType == "skip") { + // limit the counter? + var i = 0, + end = len, + limit = parseInt(S.options.counterLimit) || 0; + + if (limit < len && limit > 2) { // support large galleries + var h = Math.floor(limit / 2); + i = S.current - h; + if (i < 0) + i += len; + end = S.current + (limit - h); + if (end > len) + end -= len; + } + + while (i != end) { + if (i == len) + i = 0; + counter += '"; + } + } else { + counter = [S.current + 1, S.lang.of, len].join(' '); + } + } + + get("sb-counter").innerHTML = counter; + + callback(); +} + +/** + * Shows the title and info bars. + * + * @param {Function} callback The callback to use when finished + * @private + */ +function showBars(callback) { + var titleInner = get("sb-title-inner"), + infoInner = get("sb-info-inner"), + duration = 0.35; + + // clear visibility before animating into view + titleInner.style.visibility = infoInner.style.visibility = ""; + + if (titleInner.innerHTML != "") + animate(titleInner, "marginTop", 0, duration); + animate(infoInner, "marginTop", 0, duration, callback); +} + +/** + * Hides the title and info bars. + * + * @param {Boolean} anim True to animate the transition + * @param {Function} callback The callback to use when finished + * @private + */ +function hideBars(anim, callback) { + var title = get("sb-title"), + info = get("sb-info"), + titleHeight = title.offsetHeight, + infoHeight = info.offsetHeight, + titleInner = get("sb-title-inner"), + infoInner = get("sb-info-inner"), + duration = (anim ? 0.35 : 0); + + animate(titleInner, "marginTop", titleHeight, duration); + animate(infoInner, "marginTop", infoHeight * -1, duration, function() { + titleInner.style.visibility = infoInner.style.visibility = "hidden"; + callback(); + }); +} + +/** + * Adjusts the height of #sb-wrapper-inner and centers #sb-wrapper vertically + * in the viewport. + * + * @param {Number} height The height (in pixels) + * @param {Number} top The top (in pixels) + * @param {Boolean} anim True to animate the transition + * @param {Function} callback The callback to use when finished + * @private + */ +function adjustHeight(height, top, anim, callback) { + var wrapperInner = get("sb-wrapper-inner"), + duration = (anim ? S.options.resizeDuration : 0); + + animate(wrapper, "top", top, duration); + animate(wrapperInner, "height", height, duration, callback); +} + +/** + * Adjusts the width and left position of #sb-wrapper. + * + * @param {Number} width The width (in pixels) + * @param {Number} left The left (in pixels) + * @param {Boolean} anim True to animate the transition + * @param {Function} callback The callback to use when finished + * @private + */ +function adjustWidth(width, left, anim, callback) { + var duration = (anim ? S.options.resizeDuration : 0); + + animate(wrapper, "left", left, duration); + animate(wrapper, "width", width, duration, callback); +} + +/** + * Calculates the dimensions for Shadowbox. + * + * @param {Number} height The content height + * @param {Number} width The content width + * @return {Object} The new dimensions object + * @private + */ +function setDimensions(height, width) { + var bodyInner = get("sb-body-inner"), + height = parseInt(height), + width = parseInt(width), + topBottom = wrapper.offsetHeight - bodyInner.offsetHeight, + leftRight = wrapper.offsetWidth - bodyInner.offsetWidth, + + // overlay should provide proper window dimensions here + maxHeight = overlay.offsetHeight, + maxWidth = overlay.offsetWidth, + + // default to the default viewport padding + padding = parseInt(S.options.viewportPadding) || 20, + + // only preserve aspect ratio if there is something to display and + // it's not draggable + preserveAspect = (S.player && S.options.handleOversize != "drag"); + + return S.setDimensions(height, width, maxHeight, maxWidth, topBottom, leftRight, padding, preserveAspect); +} + +/** + * The Shadowbox.skin object. + * + * @type {Object} + * @public + */ +var K = {}; + +/** + * The HTML markup to use. + * + * @type {String} + * @public + */ +K.markup = "" + +''; + +/** + * Various options that control the behavior of Shadowbox' skin. + * + * @type {Object} + * @public + */ +K.options = { + + /** + * The sequence of the resizing animations. "hw" will resize height, then width. "wh" resizes + * width, then height. "sync" resizes both simultaneously. + * + * @type {String} + */ + animSequence: "sync", + + /** + * The limit to the number of counter links that are displayed in a "skip"-style counter. + * + * @type {Number} + */ + counterLimit: 10, + + /** + * The counter type to use. May be either "default" or "skip". A skip counter displays a + * link for each object in the gallery. + * + * @type {String} + */ + counterType: "default", + + /** + * True to display the gallery counter. + * + * @type {Boolean} + */ + displayCounter: true, + + /** + * True to show the navigation controls. + * + * @type {Boolean} + */ + displayNav: true, + + /** + * The duration (in seconds) of opacity animations. + * + * @type {Number} + */ + fadeDuration: 0.35, + + /** + * The initial height (in pixels). + * + * @type {Number} + */ + initialHeight: 160, + + /** + * The initial width (in pixels). + * + * @type {Number} + */ + initialWidth: 320, + + /** + * True to trigger Shadowbox.close when the overlay is clicked. + * + * @type {Boolean} + */ + modal: false, + + /** + * The color (in hex) to use for the overlay. + * + * @type {String} + */ + overlayColor: "#000", + + /** + * The opacity to use for the overlay. + * + * @type {Number} + */ + overlayOpacity: 0.5, + + /** + * The duration (in seconds) to use for resizing animations. + * + * @type {Number} + */ + resizeDuration: 0.35, + + /** + * True to show the overlay, false to hide it. + * + * @type {Boolean} + */ + showOverlay: true, + + /** + * Names of elements that should be hidden when the overlay is enabled. + * + * @type {String} + */ + troubleElements: ["select", "object", "embed", "canvas"] + +}; + +/** + * Initialization function. Called immediately after this skin's markup has been + * appended to the document with all of the necessary language replacements done. + * + * @public + */ +K.init = function() { + S.appendHTML(document.body, sprintf(K.markup, S.lang)); + + K.body = get("sb-body-inner"); + + // cache oft-used elements + container = get("sb-container"); + overlay = get("sb-overlay"); + wrapper = get("sb-wrapper"); + + // use absolute positioning in browsers that don't support fixed + if (!supportsFixed) + container.style.position = "absolute"; + + if (!supportsOpacity) { + // support transparent PNG's via AlphaImageLoader + var el, m, re = /url\("(.*\.png)"\)/; + each(pngIds, function(i, id) { + el = get(id); + if (el) { + m = S.getStyle(el, "backgroundImage").match(re); + if (m) { + el.style.backgroundImage = "none"; + el.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,src=" + + m[1] + ",sizingMethod=scale);"; + } + } + }); + } + + // add window resize event handler, use 10 ms buffer to prevent jerky resizing + var timer; + addEvent(window, "resize", function() { + if (timer) { + clearTimeout(timer); + timer = null; + } + + if (open) + timer = setTimeout(K.onWindowResize, 10); + }); +} + +/** + * Called when Shadowbox opens. + * + * @param {Object} obj The object to open + * @param {Function} callback The callback to use when finished + * @public + */ +K.onOpen = function(obj, callback) { + // prevent window resize events from firing until we're finished + doWindowResize = false; + + container.style.display = "block"; + + setSize(); + + var dims = setDimensions(S.options.initialHeight, S.options.initialWidth); + adjustHeight(dims.innerHeight, dims.top); + adjustWidth(dims.width, dims.left); + + if (S.options.showOverlay) { + overlay.style.backgroundColor = S.options.overlayColor; + S.setOpacity(overlay, 0); + + if (!S.options.modal) + addEvent(overlay, "click", S.close); + + overlayOn = true; + } + + if (!supportsFixed) { + setPosition(); + addEvent(window, "scroll", setPosition); + } + + toggleTroubleElements(); + container.style.visibility = "visible"; + + if (overlayOn) { + animate(overlay, "opacity", S.options.overlayOpacity, S.options.fadeDuration, callback); + } else { + callback(); + } +} + +/** + * Called when a new object is being loaded. + * + * @param {Boolean} changing True if the content is changing from some + * previous object + * @param {Function} callback The callback to use when finished + * @public + */ +K.onLoad = function(changing, callback) { + toggleLoading(true); + + // make sure the body doesn't have any children + while (K.body.firstChild) + remove(K.body.firstChild); + + hideBars(changing, function() { + if (!open) + return; + + if (!changing) + wrapper.style.visibility = "visible"; + + buildBars(callback); + }); +} + +/** + * Called when the content is ready to be loaded (e.g. when the image has finished + * loading). Should resize the content box and make any other necessary adjustments. + * + * @param {Function} callback The callback to use when finished + * @public + */ +K.onReady = function(callback) { + if (!open) + return; + + var player = S.player, + dims = setDimensions(player.height, player.width); + + var wrapped = function() { + showBars(callback); + } + + switch (S.options.animSequence) { + case "hw": + adjustHeight(dims.innerHeight, dims.top, true, function() { + adjustWidth(dims.width, dims.left, true, wrapped); + }); + break; + case "wh": + adjustWidth(dims.width, dims.left, true, function() { + adjustHeight(dims.innerHeight, dims.top, true, wrapped); + }); + break; + default: // sync + adjustWidth(dims.width, dims.left, true); + adjustHeight(dims.innerHeight, dims.top, true, wrapped); + } +} + +/** + * Called when the content is loaded into the box and is ready to be displayed. + * + * @param {Function} callback The callback to use when finished + * @public + */ +K.onShow = function(callback) { + toggleLoading(false, callback); + + // re-enable window resize events + doWindowResize = true; +} + +/** + * Called in Shadowbox.close. + * + * @public + */ +K.onClose = function() { + if (!supportsFixed) + removeEvent(window, "scroll", setPosition); + + removeEvent(overlay, "click", S.close); + + wrapper.style.visibility = "hidden"; + + var callback = function() { + container.style.visibility = "hidden"; + container.style.display = "none"; + toggleTroubleElements(true); + } + + if (overlayOn) { + animate(overlay, "opacity", 0, S.options.fadeDuration, callback); + } else { + callback(); + } +} + +/** + * Called in Shadowbox.play. + * + * @public + */ +K.onPlay = function() { + toggleNav("play", false); + toggleNav("pause", true); +} + +/** + * Called in Shadowbox.pause. + * + * @public + */ +K.onPause = function() { + toggleNav("pause", false); + toggleNav("play", true); +} + +/** + * Called when the window is resized. + * + * @public + */ +K.onWindowResize = function() { + if (!doWindowResize) + return; + + setSize(); + + var player = S.player, + dims = setDimensions(player.height, player.width); + + // adjust width first to eliminate horizontal scroll bar + adjustWidth(dims.width, dims.left); + adjustHeight(dims.innerHeight, dims.top); + + if (player.onWindowResize) + player.onWindowResize(); +} + +S.skin = K; +// expose +window['Shadowbox'] = S; + +})(window); diff --git a/Disco.Web/Extensions/T4MVC/API.DeviceController.generated.cs b/Disco.Web/Extensions/T4MVC/API.DeviceController.generated.cs index 41b82366..586e162e 100644 --- a/Disco.Web/Extensions/T4MVC/API.DeviceController.generated.cs +++ b/Disco.Web/Extensions/T4MVC/API.DeviceController.generated.cs @@ -544,6 +544,7 @@ namespace Disco.Web.Areas.API.Controllers public class ActionParamsClass_AttachmentDownload { public readonly string id = "id"; + public readonly string inline = "inline"; } static readonly ActionParamsClass_AttachmentDownloadAll s_params_AttachmentDownloadAll = new ActionParamsClass_AttachmentDownloadAll(); [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] @@ -945,14 +946,15 @@ namespace Disco.Web.Areas.API.Controllers } [NonAction] - partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id); + partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id, bool? inline); [NonAction] - public override System.Web.Mvc.ActionResult AttachmentDownload(int id) + public override System.Web.Mvc.ActionResult AttachmentDownload(int id, bool? inline) { var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.AttachmentDownload); ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); - AttachmentDownloadOverride(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "inline", inline); + AttachmentDownloadOverride(callInfo, id, inline); return callInfo; } diff --git a/Disco.Web/Extensions/T4MVC/API.JobController.generated.cs b/Disco.Web/Extensions/T4MVC/API.JobController.generated.cs index 442e7d9c..aadfda62 100644 --- a/Disco.Web/Extensions/T4MVC/API.JobController.generated.cs +++ b/Disco.Web/Extensions/T4MVC/API.JobController.generated.cs @@ -1184,6 +1184,7 @@ namespace Disco.Web.Areas.API.Controllers public class ActionParamsClass_AttachmentDownload { public readonly string id = "id"; + public readonly string inline = "inline"; } static readonly ActionParamsClass_AttachmentDownloadAll s_params_AttachmentDownloadAll = new ActionParamsClass_AttachmentDownloadAll(); [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] @@ -2071,14 +2072,15 @@ namespace Disco.Web.Areas.API.Controllers } [NonAction] - partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id); + partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id, bool? inline); [NonAction] - public override System.Web.Mvc.ActionResult AttachmentDownload(int id) + public override System.Web.Mvc.ActionResult AttachmentDownload(int id, bool? inline) { var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.AttachmentDownload); ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); - AttachmentDownloadOverride(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "inline", inline); + AttachmentDownloadOverride(callInfo, id, inline); return callInfo; } diff --git a/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs b/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs index fe01492f..a67578ef 100644 --- a/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs +++ b/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs @@ -230,6 +230,7 @@ namespace Disco.Web.Areas.API.Controllers public class ActionParamsClass_AttachmentDownload { public readonly string id = "id"; + public readonly string inline = "inline"; } static readonly ActionParamsClass_AttachmentDownloadAll s_params_AttachmentDownloadAll = new ActionParamsClass_AttachmentDownloadAll(); [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] @@ -370,14 +371,15 @@ namespace Disco.Web.Areas.API.Controllers } [NonAction] - partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id); + partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id, bool? inline); [NonAction] - public override System.Web.Mvc.ActionResult AttachmentDownload(int id) + public override System.Web.Mvc.ActionResult AttachmentDownload(int id, bool? inline) { var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.AttachmentDownload); ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); - AttachmentDownloadOverride(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "inline", inline); + AttachmentDownloadOverride(callInfo, id, inline); return callInfo; } diff --git a/Disco.Web/Views/Device/DeviceParts/_Resources.cshtml b/Disco.Web/Views/Device/DeviceParts/_Resources.cshtml index 4e5f441d..816cbf46 100644 --- a/Disco.Web/Views/Device/DeviceParts/_Resources.cshtml +++ b/Disco.Web/Views/Device/DeviceParts/_Resources.cshtml @@ -56,7 +56,19 @@ \r\n"); +" if (this[\'shadowboxCacheKey\'])\r\n Shadowbox.removeCac" + +"he(this);\r\n $element.remove();\r\n d" + +"ocument.DiscoFunctions.liveAfterUpdate();\r\n });\r\n " + +" }\r\n }\r\n\r\n $attachmentOutput.children(\'a\').each(functio" + +"n () {\r\n $this = $(this);\r\n if ($this.attr(\'data-m" + +"imetype\').toLowerCase().indexOf(\'image/\') == 0)\r\n $this.shado" + +"wbox({ gallery: \'attachments\', player: \'img\', title: $this.find(\'.comments\').tex" + +"t() });\r\n else if ($this.attr(\'data-mimetype\').toLowerCase().inde" + +"xOf(\'application/pdf\') == 0 && navigator.pdfViewerEnabled)\r\n " + +"$this.shadowbox({ gallery: \'attachments\', player: \'iframe\', title: $this.find(\'." + +"comments\').text() });\r\n else\r\n $this.click(onD" + +"ownload);\r\n });\r\n\r\n $Attachments\r\n .find(\'." + +"attachmentInput span.download-all\')\r\n .on(\'click\', function (even" + +"t) {\r\n const downloadAllUrl = $Attachments.attr(\'data-downloa" + +"dallurl\');\r\n const $this = $(this);\r\n\r\n if" + +" ($this.hasClass(\'fa-spinner\'))\r\n return;\r\n\r\n " + +" $this\r\n .removeClass(\'fa-download\')\r\n " + +" .addClass(\'fa-spinner fa-spin\');\r\n\r\n if (!$attach" + +"mentDownloadHost) {\r\n $attachmentDownloadHost = $(\'\')\r\n .attr({ \'id\': \'AttachmentsDownloadHost\', \'name" + +"\': \'AttachmentsDownloadHost\', \'title\': \'Attachment Download Host\' })\r\n " + +" .addClass(\'hidden\')\r\n .appendTo(\'bo" + +"dy\')\r\n .contents();\r\n }\r\n " + +" const $form = $(\'
\')\r\n .attr({\r\n " + +" method: \'POST\',\r\n action: download" + +"AllUrl,\r\n target: \'AttachmentsDownloadHost\'\r\n " + +" })\r\n .append($(\'\')\r\n " + +" .attr({ type: \'hidden\', name: \'__RequestVerificationToken\' })\r\n " + +" .val(document.body.dataset.antiforgery))\r\n " + +" .appendTo(document.body)\r\n .trigger(\'submit\');" + +"\r\n\r\n window.setTimeout(function () {\r\n " + +" $this\r\n .removeClass(\'fa-spinner fa-spin\')\r\n " + +" .addClass(\'fa-download\');\r\n $form.rem" + +"ove();\r\n }, 2000);\r\n });\r\n\r\n // Add" + +" Globally Available Functions\r\n document.DiscoFunctions.liveAddAttach" + +"ment = addAttachment;\r\n document.DiscoFunctions.liveRemoveAttachment " + +"= removeAttachment;\r\n\r\n //#endregion\r\n });\r\n\r\n\r\n \r" + +"\n"); - #line 588 "..\..\Views\Job\JobParts\Resources.cshtml" + #line 604 "..\..\Views\Job\JobParts\Resources.cshtml" } #line default #line hidden - #line 589 "..\..\Views\Job\JobParts\Resources.cshtml" + #line 605 "..\..\Views\Job\JobParts\Resources.cshtml" if (canShowLogs || canShowAttachments) { @@ -1309,7 +1326,7 @@ WriteLiteral("/\' + a.Id + \'?v=\' + retryCount);\r\n };\ WriteLiteral(" \r\n\r\n"); - #line 354 "..\..\Views\User\UserParts\_Resources.cshtml" + #line 370 "..\..\Views\User\UserParts\_Resources.cshtml" if (canRemoveAnyAttachments || canRemoveOwnAttachments) { @@ -826,7 +829,7 @@ WriteLiteral(" class=\"fa fa-exclamation-triangle fa-lg\""); WriteLiteral("> Are you sure?\r\n

\r\n \r\n"); - #line 361 "..\..\Views\User\UserParts\_Resources.cshtml" + #line 377 "..\..\Views\User\UserParts\_Resources.cshtml" } #line default