1 2Steam = { 3 sm_bInitialized: false, 4 sm_bUserInClient: false, 5 sm_bUserInGameOverlay: false, 6 sm_bUserInTenfootBrowser: false, 7 sm_bUserInMobileChat: false, 8 sm_bUserInMobileApp: false, 9 10 BIsUserInSteamClient: function() 11 { 12 if ( !Steam.sm_bInitialized ) 13 Steam.Init(); 14 15 return Steam.sm_bUserInClient; 16 }, 17 18 BIsUserInGameOverlay: function() 19 { 20 if ( !Steam.sm_bInitialized ) 21 Steam.Init(); 22 23 return Steam.sm_bUserInGameOverlay 24 }, 25 26 BIsUserInSteamTenfootBrowser: function() 27 { 28 if ( !Steam.sm_bInitialized ) 29 Steam.Init(); 30 31 return Steam.sm_bUserInTenfootBrowser; 32 }, 33 34 BIsUserInClientOrOverlay: function() 35 { 36 if ( !Steam.sm_bInitialized ) 37 Steam.Init(); 38 39 return Steam.sm_bUserInClient || Steam.sm_bUserInGameOverlay; 40 }, 41 42 BIsUserInSteamMobileChat: function() 43 { 44 if ( !Steam.sm_bInitialized ) 45 Steam.Init(); 46 47 return Steam.sm_bUserInMobileChat; 48 }, 49 50 BIsUserInSteamMobileApp: function() 51 { 52 if ( !Steam.sm_bInitialized ) 53 Steam.Init(); 54 55 return Steam.sm_bUserInMobileApp; 56 }, 57 58 GetClientPackageVersion: function() 59 { 60 if ( !Steam.BIsUserInClientOrOverlay() ) 61 return 0; 62 63 if ( typeof navigator != 'undefined' && navigator.userAgent ) 64 { 65 var matches = navigator.userAgent.match( /Valve Steam [^\/]*\/([0-9]*)/ ); 66 if ( matches && matches.length == 2 ) 67 return matches[1]; 68 } 69 70 return 0; 71 }, 72 73 Init: function() 74 { 75 var fnCheckAgent = function( strUAMatch, strURLParam ) 76 { 77 if ( window.location.href.match( '[?&]' + strURLParam + '=' ) ) 78 return true; 79 80 if ( typeof navigator != 'undefined' && navigator.userAgent && navigator.userAgent.indexOf( strUAMatch ) != -1 ) 81 return true; 82 83 return false; 84 }; 85 86 Steam.sm_bUserInTenfootBrowser = fnCheckAgent( 'Valve Steam Tenfoot', 'force_tenfoot_client_view' ); 87 Steam.sm_bUserInGameOverlay = fnCheckAgent( 'Valve Steam GameOverlay', 'force_overlay_view' ); 88 Steam.sm_bUserInClient = Steam.sm_bUserInTenfootBrowser || fnCheckAgent( 'Valve Steam Client', 'force_client_view' ); 89 Steam.sm_bUserInMobileChat = fnCheckAgent( 'Valve Steam Mobile Chat', 'force_mobile_chat_view' ); 90 Steam.sm_bUserInMobileApp = fnCheckAgent( 'Valve Steam App', 'force_mobile_app_view' ); 91 92 Steam.sm_bInitialized = true; 93 }, 94 95 LinkInNewWindow: function( $A ) 96 { 97 if ( Steam.BIsUserInSteamClient() && !Steam.BIsUserInSteamTenfootBrowser() ) 98 $A.attr( 'href', 'steam://openurl_external/' + $A.attr('href') ); 99 else 100 $A.attr( 'target', '_blank' ); 101 } 102}; 103 104function OpenFriendChat( steamid, accountid ) 105{ 106 if ( Steam.BIsUserInClientOrOverlay() ) 107 { 108 window.location = 'steam://friends/message/' + steamid; 109 } 110 else if ( Steam.BIsUserInSteamMobileChat() ) 111 { 112 window.location = 'steamchatmobile://friend/' + steamid; 113 } 114 else if ( Steam.BIsUserInSteamMobileApp() ) 115 { 116 window.location = 'https://steamcommunity.com/chat/friend/' + steamid; 117 } 118 else if ( typeof ClientConnectionAPI !== 'undefined' ) 119 { 120 ClientConnectionAPI.OpenFriendChatDialog( steamid ).then( function( result ) { 121 if ( !result.success ) 122 { 123 PromptContinueToWebChat( result, function() { 124 OpenFriendChatInWebChat( steamid, accountid ); 125 }, 'steam://friends/message/' + steamid ); 126 } 127 }); 128 } 129 else 130 { 131 OpenFriendChatInWebChat( steamid, accountid ); 132 } 133} 134 135function OpenFriendChatInWebChat( steamid, accountid ) 136{ 137 LaunchWebChat( { friend: accountid }, {command: 'ShowFriendChatDialog', steamid: steamid} ); 138} 139 140function OpenGroupChat( steamid ) 141{ 142 if ( Steam.BIsUserInSteamMobileChat() ) 143 { 144 window.location = 'steamchatmobile://group/' + steamid; 145 } 146 else if ( Steam.BIsUserInSteamMobileApp() ) 147 { 148 window.location = 'https://steamcommunity.com/chat/group/' + steamid; 149 } 150 else if ( !Steam.BIsUserInClientOrOverlay() && typeof ClientConnectionAPI !== 'undefined' ) 151 { 152 ClientConnectionAPI.OpenFriendChatDialog( steamid ).then( function( result ) { 153 if ( !result.success ) 154 { 155 PromptContinueToWebChat( result, function() { 156 LaunchWebChat( null, {command: 'ShowFriendChatDialog', steamid: steamid} ); 157 }, 'steam://friends/joinchat/' + steamid ); 158 } 159 }); 160 } 161 else 162 { 163 window.location = 'steam://friends/joinchat/' + steamid; 164 } 165} 166 167function PromptContinueToWebChat( result, fnLaunchWebchat, steamURL ) 168{ 169 ShowConfirmDialog( 'Got Steam?', 'We couldn\'t find Steam running on your machine. Would you like to launch web chat?', 170 'Use web chat' , null, 'Get Steam' ).done( function( choice ) { 171 if ( choice == 'OK' ) 172 fnLaunchWebchat(); 173 else 174 window.location = 'https://store.steampowered.com/about/' 175 }); 176} 177 178 179// proto functions used to accept an id or an element. 180// This can be used to migrate them to returning jquery instead of proto-wrapped element 181function $JFromIDOrElement( elem ) 182{ 183 if ( elem instanceof jQuery ) 184 return elem; 185 else if ( typeof elem == 'string' ) 186 return $J('#' + elem.replace( /\./, '\\.' ) ); 187 else 188 return $J( elem ); 189} 190 191/** Show a popup dialog like confirm(), with two buttons. Clicking ok resolves with done(), cancel or closing the window resolves with fail() 192 * 193 * @param strTitle Title bar text 194 * @param strDescription Message text 195 * @param strOKButton Text to show on OK button (default "OK") 196 * @param strCancelButton Text to show on Cancel button (default "Cancel") 197 * @param strSecondaryActionButton Add a secondary ok button (three buttons total). Resolves with done() like OK but passes 'SECONDARY' as argument to handler 198 * @returns CModal 199 */ 200function ShowConfirmDialog( strTitle, strDescription, strOKButton, strCancelButton, strSecondaryActionButton, rgModalParams ) 201{ 202 if ( !strOKButton ) 203 strOKButton = 'OK'; 204 if ( !strCancelButton ) 205 strCancelButton = 'Cancel'; 206 207 var deferred = new jQuery.Deferred(); 208 var fnOK = function() { deferred.resolve( 'OK' ); }; 209 var fnSecondary = function() { deferred.resolve( 'SECONDARY' ); }; 210 var fnCancel = function( bWasCancelButton ) { deferred.reject( bWasCancelButton ); }; 211 212 var rgButtons = []; 213 214 var $OKButton = _BuildDialogButton( strOKButton, true ); 215 $OKButton.click( fnOK ); 216 rgButtons.push( $OKButton ); 217 218 if ( strSecondaryActionButton ) 219 { 220 var $SecondaryActionButton = _BuildDialogButton( strSecondaryActionButton, false, {strClassName: 'btn_blue_steamui btn_medium' } );
221 $SecondaryActionButton.click( fnSecondary ); 222 rgButtons.push( $SecondaryActionButton ); 223 } 224 225 var $CancelButton = _BuildDialogButton( strCancelButton ); 226 $CancelButton.click( function() { fnCancel( true ); } ); 227 rgButtons.push( $CancelButton ); 228 229 var Modal = _BuildDialog( strTitle, strDescription, rgButtons, fnCancel, rgModalParams ); 230 Modal.Show(); 231 232 _BindOnEnterKeyPressForDialog( Modal, deferred, fnOK ); 233 234 deferred.always( function() { Modal.Dismiss(); } ); 235 // attach the deferred's events to the modal 236 deferred.promise( Modal ); 237 238 return Modal; 239} 240 241/** Show a dialog with a single button, like alert(). Button click or closing the modal resolve deferred with done(). 242 * 243 * @param strTitle Title bar text 244 * @param strDescription Message text 245 * @param strOKButton Text on the OK button ("OK" by default) 246 * @returns CModal 247 */ 248function ShowAlertDialog( strTitle, strDescription, strOKButton, rgModalParams ) 249{ 250 if ( !strOKButton ) 251 strOKButton = 'OK'; 252 253 var deferred = new jQuery.Deferred(); 254 var fnOK = function( bWasCancelButton ) { deferred.resolve( bWasCancelButton ); }; 255 256 var $OKButton = _BuildDialogButton( strOKButton ); 257 $OKButton.click( function() { fnOK( true ); } ); 258 259 var Modal = _BuildDialog( strTitle, strDescription, [ $OKButton ], fnOK, rgModalParams ); 260 deferred.always( function() { Modal.Dismiss(); } ); 261 Modal.Show(); 262 263 _BindOnEnterKeyPressForDialog( Modal, deferred, fnOK ); 264 265 // attach the deferred's events to the modal 266 deferred.promise( Modal ); 267 268 return Modal; 269 270} 271 272/** Show a popup dialog. Has no buttons. Closing the dialog resolves deferred with done(). 273 * 274 * @param strTitle Title bar text 275 * @param strDescription Message text 276 * @param rgModalParams See CModal 277 * @returns CModal 278 */ 279function ShowDialog( strTitle, strDescription, rgModalParams ) 280{ 281 var deferred = new jQuery.Deferred(); 282 var fnOK = function() { deferred.resolve(); }; 283 284 var Modal = _BuildDialog( strTitle, strDescription, [], fnOK, rgModalParams ); 285 deferred.always( function() { Modal.Dismiss(); } ); 286 Modal.Show(); 287 288 // attach the deferred's events to the modal 289 deferred.promise( Modal ); 290 291 return Modal; 292} 293 294/** 295 * Show a dialog, but take the arguments as an object a la React. 296 * @returns CModal 297 */ 298function ShowPromptDialogWithProps( rgParams ) 299{ 300 let { strOKButton, strCancelButton, strTitle, strDescription, defaultValue, inputType, inputMaxSize, bNoPromiseDismiss, bInputReadOnly } = rgParams; 301 if ( !strOKButton ) 302 strOKButton = 'OK'; 303 if ( !strCancelButton ) 304 strCancelButton = 'Cancel'; 305 if ( !inputType ) 306 inputType = 'text'; 307 308 var $Body = $J('<form/>'); 309 var $Input = $J('<input/>', {type: inputType, 'class': '' } ).val( defaultValue ); 310 if ( inputMaxSize ) 311 { 312 $Input.attr( 'maxlength', inputMaxSize ); 313 } 314 if ( bInputReadOnly ) 315 { 316 $Input.attr( 'readonly', true ); 317 } 318 $Body.append( $J('<div/>', {'class': 'newmodal_prompt_description' } ).append( strDescription ) ); 319 $Body.append( $J('<div/>', {'class': 'newmodal_prompt_input gray_bevel for_text_input fullwidth' } ).append( $Input ) ); 320 321 var deferred = new jQuery.Deferred(); 322 var fnOK = function() { deferred.resolve( $Input.val() ); }; 323 var fnCancel = function() { deferred.reject(); }; 324 325 $Body.submit( function( event ) { event.preventDefault(); fnOK(); } ); 326 327 var elButtonLabel = $J( '<span/>' ).text( strOKButton ); 328 var $OKButton = $J('<button/>', {type: 'submit', 'class': 'btn_green_white_innerfade btn_medium' } ).append( elButtonLabel ); 329 $OKButton.click( fnOK ); 330 var $CancelButton = _BuildDialogButton( strCancelButton ); 331 $CancelButton.click( fnCancel ); 332 333 var Modal = _BuildDialog( strTitle, $Body, [ $OKButton, $CancelButton ], fnCancel, rgParams ); 334 if( !bNoPromiseDismiss ) 335 deferred.always( function() { Modal.Dismiss(); } ); 336 337 Modal.Show(); 338 339 $Input.focus(); 340 341 // attach the deferred's events to the modal 342 deferred.promise( Modal ); 343 344 return Modal; 345} 346 347/** 348 * A wrapper around ShowDialogPrompt to be compatible with existing code. Prefer ShowPromptDialogWithProps instead. 349 * @returns CModal 350 */ 351function ShowPromptDialog( strTitle, strDescription, strOKButton, strCancelButton, rgModalParams, defaultValue ) 352{ 353 let rgParams = {}; 354 if ( rgModalParams ) 355 Object.assign( rgParams, rgModalParams ); 356 rgParams.strTitle = strTitle; 357 rgParams.strDescription = strDescription; 358 rgParams.strOKButton = strOKButton; 359 rgParams.strCancelButton = strCancelButton; 360 rgParams.defaultValue = defaultValue; 361 return ShowPromptDialogWithProps( rgParams ); 362} 363 364/** 365 * @returns CModal 366 */ 367function ShowPromptWithTextAreaDialog( strTitle, strInitialText, strOKButton, strCancelButton, textAreaMaxLength, strDescription ) 368{ 369 if ( !strOKButton ) 370 strOKButton = 'OK'; 371 if ( !strCancelButton ) 372 strCancelButton = 'Cancel'; 373 374 var $Body = $J('<form/>'); 375 var $TextArea = $J('<textarea/>', { 'class': 'newmodal_prompt_textarea' } ); 376 $TextArea.text( strInitialText ); 377 if ( textAreaMaxLength ) 378 { 379 $TextArea.attr( 'maxlength', textAreaMaxLength ); 380 $TextArea.bind( "keyup change", 381 function() 382 { 383 var str = $J(this).val(); 384 var mx = parseInt($J(this).attr('maxlength')); 385 if (str.length > mx) 386 { 387 $J(this).val(str.substr(0, mx)); 388 return false;
389 } 390 } 391 ); 392 } 393 394 if ( strDescription ) 395 { 396 $Body.append( $J('<div/>', {'class': 'newmodal_prompt_with_textarea newmodal_prompt_description' } ).text( strDescription ) ); 397 } 398 399 $Body.append( $J('<div/>', {'class': 'newmodal_prompt_with_textarea gray_bevel fullwidth' } ).append( $TextArea ) ); 400 401 var deferred = new jQuery.Deferred(); 402 var fnOK = function() { deferred.resolve( $TextArea.val() ); }; 403 var fnCancel = function() { deferred.reject(); }; 404 405 $Body.submit( function( event ) { event.preventDefault(); fnOK(); } ); 406 407 var elButtonLabel = $J( '<span/>' ).text( strOKButton ); 408 var $OKButton = $J('<button/>', {type: 'submit', 'class': 'btn_green_white_innerfade btn_medium' } ).append( elButtonLabel ); 409 $OKButton.click( fnOK ); 410 var $CancelButton = _BuildDialogButton( strCancelButton ); 411 $CancelButton.click( fnCancel ); 412 413 var Modal = _BuildDialog( strTitle, $Body, [ $OKButton, $CancelButton ], fnCancel ); 414 deferred.always( function() { Modal.Dismiss(); } ); 415 Modal.Show(); 416 417 $TextArea.focus(); 418 419 // attach the deferred's events to the modal 420 deferred.promise( Modal ); 421 422 return Modal; 423} 424 425/** 426 * @returns CModal 427 */ 428function ShowEditablePrompt( strTitle, obj, onOk, onCancel) 429{ 430 strEditButton = 'Edit'; 431 strOKButton = 'OK'; 432 strCancelButton = 'Cancel'; 433 434 var $Body = $J('<form/>'); 435 var $TextArea = $J('<textarea/>', {'class': 'newmodal_prompt_textarea', 'id': 'json_window'}); 436 $TextArea[0].readOnly = true; 437 $TextArea.text( JSON.stringify(obj, null, 2) ); 438 $Body.append( $J('<div/>', {'class': 'newmodal_prompt_with_textarea gray_bevel fullwidth ' } ).append( $TextArea ) ); 439 440 $Body.submit( function( event ) { event.preventDefault(); fnOK(); } ); 441 442 var elButtonLabel = $J( '<span/>' ).text( strOKButton ); 443 var deferredAction = new jQuery.Deferred(); 444 445 var editButtonLabel = $J( '<span/>' ).text( strEditButton ); 446 var $EditButton = $J('<button/>', {type: 'button', 'class': 'btn_darkred_white_innerfade btn_medium' } ).append( editButtonLabel ); 447 $EditButton.click( function() { 448 $EditButton[0].disable(); 449 $OKButton[0].enable(); 450 $TextArea[0].readOnly = false; 451 $TextArea.focus(); 452 }) 453 454 var $OKButton = $J('<button/>', {type: 'submit', 'class': 'btn_green_white_innerfade btn_medium' } ).append( elButtonLabel ); 455 $OKButton.click( function() { deferredAction.resolve( $TextArea.val() ); } ); 456 $OKButton[0].disabled = true; 457 458 fnCancel = function() { deferredAction.reject(); }; 459 var $CancelButton = _BuildDialogButton( strCancelButton ); 460 $CancelButton.click( fnCancel ); 461 462 var Modal = _BuildDialog( strTitle, $Body, [ $EditButton, $OKButton, $CancelButton ], fnCancel ); 463 deferredAction.always( function() { Modal.Dismiss(); } ); 464 465 if (onOk) 466 deferredAction.done(onOk); 467 if (onCancel) 468 deferredAction.fail(onCancel); 469 470 Modal.Show(); 471 472 $TextArea.focus(); 473 474 // attach the deferred's events to the modal 475 deferredAction.promise( Modal ); 476 477 return Modal; 478} 479 480/** 481 * @returns CModal 482 */ 483function ShowBlockingWaitDialog( strTitle, strDescription ) 484{ 485 var deferred = new jQuery.Deferred(); 486 var fnOK = function() { deferred.resolve(); }; 487 488 var container = $J('<div/>', {'class': 'waiting_dialog_container'} ); 489 var throbber = $J('<div/>', {'class': 'waiting_dialog_throbber'} ); 490 container.append( throbber ); 491 container.append( strDescription ); 492 if ( !strDescription ) 493 { 494 container.addClass( 'waiting_dialog_centered' ); 495 } 496 497 var Modal = _BuildDialog( strTitle, container, [], fnOK, { bExplicitDismissalOnly: true } ); 498 deferred.always( function() { Modal.Dismiss(); } ); 499 Modal.Show(); 500 501 // attach the deferred's events to the modal 502 deferred.promise( Modal ); 503 504 return Modal; 505} 506 507function _BindOnEnterKeyPressForDialog( Modal, deferred, fnOnEnter ) 508{ 509 var fnOnKeyUp = function( event ) { 510 if ( Modal.BIsActiveModal() && !event.isDefaultPrevented() && event.which == 13 && ( !event.target || event.target.nodeName != 'TEXTAREA' ) ) 511 fnOnEnter(); 512 }; 513 $J(document).on( 'keyup.SharedConfirmDialog', fnOnKeyUp ); 514 deferred.always( function() { $J(document).off( 'keyup.SharedConfirmDialog' ); } ); 515} 516 517/** 518 * @returns CModal 519 * @private 520 */ 521function _BuildDialog( strTitle, strDescription, rgButtons, fnOnCancel, rgModalParams ) 522{ 523 var $Dialog = $J('<div/>', {'class': 'newmodal'} ); 524 var $CloseButton = $J('<div/>', {'class': 'newmodal_close', 'data-panel': '{"focusable":true,"clickOnActivate":true}' } ); 525 var $Header = ( $J('<div/>', {'class': 'newmodal_header' }) ); 526 var $TopBar = ( $J('<div/>', {'class': 'modal_top_bar' }) ); 527 if ( strTitle ) 528 $Header.append( $CloseButton ).append( $J('<div/>', {'class': 'title_text' } ).text( strTitle ) ); 529 if ( rgModalParams && rgModalParams.strSubTitle ) { 530 var $SubTitle = (rgModalParams.strSubTitle); 531 $Header.append( $J('<div/>', {'class': 'subtitle_text' } ).text( $SubTitle ) ) 532 } 533 $Header = $J('<div/>', {'class': 'newmodal_header_border'}).append( $Header ); 534 $Dialog.append( $TopBar ).append( $Header ); 535 var $Content = $J('<div/>', {'class': 'newmodal_content' } ); 536 $Content.append( $J('<div/>').append( strDescription ) ); 537 538 if ( rgButtons.length > 0 ) 539 { 540 var $Buttons = $J('<div/>', {'class': 'newmodal_buttons', 'data-panel': '{"flow-children":"row"}' } ); 541 $Content.append( $Buttons ); 542 for( var i = 0; i < rgButtons.length; i++ ) 543 { 544 var $Button = rgButtons[i]; 545 if ( i == 0 )
546 $Button.attr( 'data-panel', '{"autoFocus":true,"focusable":true,"clickOnActivate":true}' ); 547 else 548 $Button.attr( 'data-panel', '{"focusable":true,"clickOnActivate":true}' ); 549 $Buttons.append( $Button ); 550 } 551 } 552 553 $Dialog.append( $J('<div/>', {'class': 'newmodal_content_border' } ).append( $Content ) ); 554 555 if ( rgModalParams && rgModalParams.bExplicitDismissalOnly ) 556 $CloseButton.hide(); 557 558 var Modal = new CModal( $Dialog, rgModalParams ); 559 if ( fnOnCancel ) 560 { 561 Modal.OnDismiss( fnOnCancel ); 562 $CloseButton.click( function() { Modal.Dismiss(); } ); 563 } 564 565 // on responsive pages, the 'newmodal' element covers the whole viewable area (so that we can control scrolling 566 // if the modal is very tall). If the modal doesn't cover the whole area, we dismiss on clicks to this background 567 // area 568 if ( Modal.m_fnBackgroundClick ) 569 { 570 $Dialog.click( function(e) { if ( e.target == this ) Modal.m_fnBackgroundClick(); } ); 571 } 572 573 Modal.SetRemoveContentOnDismissal( true ); 574 575 return Modal; 576} 577 578function _BuildDialogButton( strText, bActive, rgOptions ) 579{ 580 if ( !rgOptions ) 581 rgOptions = {}; 582 583 var strClassName = bActive ? 'btn_green_steamui btn_medium' : 'btn_grey_steamui btn_medium'; 584 if ( rgOptions.strClassName ) 585 strClassName = rgOptions.strClassName; 586 587 var elButtonLabel = $J( '<span/>' ).html( strText ); 588 var elButton = $J('<div/>', {'class': strClassName } ).append( elButtonLabel ); 589 return elButton; 590} 591 592/** Implemented for Gamepad: show content in a fullscreen, borderless, modal which gets navigation support. 593 * Used on the app details page to show screenshots in full screen. 594 * Closing the dialog resolves deferred with done(). 595 * 596 * @returns CModal 597 */ 598function GPShowFullScreenModal( content ) 599{ 600 var deferred = new jQuery.Deferred(); 601 var fnOK = function() { deferred.resolve(); }; 602 603 var Modal = _BuilGPFullScreenModal( content, fnOK ); 604 deferred.always( function() { Modal.Dismiss(); } ); 605 Modal.Show(); 606 607 // attach the deferred's events to the modal 608 deferred.promise( Modal ); 609 610 return Modal; 611} 612 613function _BuilGPFullScreenModal( content, fnOnCancel ) 614{ 615 var $Dialog = $J('<div/>').append( $J('<div/>', {'style': 'display:flex'} ).append( content ) ); 616 var Modal = new CModal( $Dialog ); 617 618 Modal.SetRemoveContentOnDismissal( true ); 619 if ( fnOnCancel ) 620 { 621 Modal.OnDismiss( fnOnCancel ); 622 } 623 624 return Modal; 625} 626 627/* modal params: 628 bExplicitDismissalOnly - by default, clicking outside of the modal dismisses it. Set this to true to override that behavior 629 bIgnoreResizeEvents - don't resize the modal when the window resizes 630 */ 631 632function CModal( $Content, rgParams ) 633{ 634 rgParams = rgParams || {}; 635 636 this.m_$Content = $Content; 637 this.m_bVisible = false; 638 639 this.m_bIgnoreResizeEvents = rgParams.bIgnoreResizeEvents; 640 this.m_fnSizing = null; 641 this.m_fnBackgroundClick = null; 642 this.m_fnOnResize = null; 643 this.m_bDismissOnBackgroundClick = !rgParams.bExplicitDismissalOnly; 644 this.m_nMaxWidth = rgParams.nMaxWidth || 0; 645 this.m_nMaxHeight = rgParams.nMaxHeight || 0; 646 647 this.m_fnOnDismiss = null; 648 this.m_fnGPOnCloseModal = null; 649 this.m_bRemoveContentOnDismissal = false;
650 651 this.m_nInitialOffsetTop = $J(window).scrollTop(); 652 this.m_nInitialOffsetLeft = $J(window).scrollLeft(); 653 this.m_$Content.css( 'position', 'fixed' ); 654 this.m_$Content.css( 'z-index', 1000 ); 655 656 /* default gamepad behavior is B button closes the dialog */ 657 if ( !this.m_$Content.attr('panel' ) ) 658 this.m_$Content.attr( 'data-panel', '{"onCancelButton":"CModal.DismissActiveModal()"}' ); 659 660 this.m_$StandardContent = null; 661 this.m_$SizedContent = null; 662 this.OnContentChanged(); //this will look for StandardContent and SizedContent in the modal body 663 664 665 var _modal = this; 666 this.m_fnBackgroundClick = function() { if ( _modal.BIsActiveModal() && _modal.m_bDismissOnBackgroundClick ) { _modal.Dismiss(); } }; 667 this.m_fnOnEscapeKeyPress = function( event ) { if ( _modal.BIsActiveModal() && event.which == 27 ) _modal.m_fnBackgroundClick(); }; 668 this.m_fnSizing = function() { _modal.AdjustSizing(); }; 669 670 /* make sure the content is parented correctly */ 671 $J(document.body).append( this.m_$Content ); 672} 673 674CModal.prototype.OnDismiss = function( fn ) 675{ 676 this.m_fnOnDismiss = fn; 677}; 678 679CModal.prototype.OnResize = function( fn ) 680{ 681 this.m_fnOnResize = fn; 682}; 683 684/** 685 * @returns jQuery 686 */ 687CModal.prototype.GetContent = function () 688{ 689 return this.m_$Content; 690}; 691 692CModal.prototype.GetBoundOnResizeEvent = function() 693{ 694 // in case someone outside needs to tell the modal to resize on certain events (eg images or iframes loading in the modal) 695 return this.m_fnSizing; 696}; 697 698CModal.prototype.OnContentChanged = function() 699{ 700 // make sure we're holding the right elements 701 this.m_$StandardContent = this.m_$Content.find( '.newmodal_content' ); 702 if ( !this.m_$StandardContent.length ) 703 this.m_$StandardContent = this.m_$Content; 704 this.m_$SizedContent = this.m_$Content.find( '.newmodal_sized_content' ); 705}; 706 707CModal.prototype.SetRemoveContentOnDismissal = function ( bRemoveContent ) 708{ 709 this.m_bRemoveContentOnDismissal = bRemoveContent; 710}; 711 712CModal.prototype.SetDismissOnBackgroundClick = function ( bDismissOnBackgroundClick ) 713{ 714 this.m_bDismissOnBackgroundClick = bDismissOnBackgroundClick; 715}; 716 717CModal.prototype.SetMaxWidth = function ( nMaxWidth ) 718{ 719 this.m_nMaxWidth = nMaxWidth; 720 if ( this.m_bVisible ) 721 this.AdjustSizing(); 722}; 723 724CModal.prototype.SetMaxHeight = function ( nMaxHeight ) 725{ 726 this.m_nMaxHeight = nMaxHeight; 727 if ( this.m_bVisible ) 728 this.AdjustSizing(); 729}; 730 731CModal.prototype.AdjustSizing = function( duration ) 732{ 733 if ( !this.m_$Content ) 734 return; 735 736 var bResponsiveScreen = window.UseTouchFriendlyMode && UseTouchFriendlyMode(); 737 var bUseTabletScreenMode = window.UseTabletScreenMode && window.UseTabletScreenMode(); 738 739 var nViewportWidth = document.compatMode === 'BackCompat' ? document.body.clientWidth : $J(window).width(); 740 var nViewportHeight = document.compatMode === 'BackCompat' ? document.body.clientHeight : $J(window).height(); 741 742 var nMaxWidth = Math.max( nViewportWidth - ( bResponsiveScreen? 24 : 80 ), bResponsiveScreen ? 200 : 500 ); 743 var nMaxHeight = Math.floor( nViewportHeight - 120 ); 744 745 if ( this.m_nMaxWidth && nMaxWidth > this.m_nMaxWidth ) 746 { 747 nMaxWidth = this.m_nMaxWidth; 748 } 749 750 if ( this.m_nMaxHeight && nMaxHeight > this.m_nMaxHeight ) 751 { 752 nMaxHeight = this.m_nMaxHeight; 753 } 754 755 // if the modal has a 'newmodal_sized_content' div, it wants to be the max height, so set it now 756 // before we compute height ( "- 18" is a fudge for a possible horizontal scrollbar ) 757 this.m_$SizedContent.css( 'min-height', ( nMaxHeight - 18 ) + 'px' ); 758 if ( this.m_fnOnResize ) 759 { 760 this.m_fnOnResize( nMaxWidth - 40, nMaxHeight ); 761 } 762 763 if ( !duration ) 764 { 765 // set sizes right away so we can calculate a good left and top 766 this.m_$Content.css( 'max-width', nMaxWidth + 'px' ); 767 if ( !bResponsiveScreen ) 768 { 769 this.m_$StandardContent.css( 'max-height', nMaxHeight + 'px' ); 770 } 771 else 772 { 773 this.m_$StandardContent.css( 'max-height', '' ); 774 } 775 } 776 777 var nContentWidth = this.m_$Content.width(); 778 var nContentHeight = this.m_$Content.height(); 779 var nLeft = Math.max( Math.floor( ( nViewportWidth - nContentWidth ) / 2 ), 12 ); 780 var nTop = Math.max( Math.floor( ( nViewportHeight - nContentHeight ) / 2 ), 12 ); 781 782 // only use absolute position on mobile screens 783 if ( bResponsiveScreen && !bUseTabletScreenMode ) 784 { 785 nLeft += this.m_nInitialOffsetLeft; 786 nTop += this.m_nInitialOffsetTop; 787 this.m_$Content.css( 'position', 'absolute' ); 788 } 789 else 790 { 791 this.m_$Content.css( 'position', 'fixed' ); 792 } 793 794 if ( duration ) 795 { 796 this.m_$Content.animate( { 'max-width': nMaxWidth, left: nLeft, top: nTop }, duration ); 797 this.m_$StandardContent.animate( {'max-height': nMaxHeight }, duration ); 798 } 799 else 800 { 801 this.m_$Content.css( 'left', nLeft );
802 this.m_$Content.css( 'top', nTop ); 803 } 804}; 805 806CModal.prototype.Show = function() 807{ 808 if ( this.m_bVisible ) 809 return; 810 811 CModal.ShowModalBackground(); 812 813 if ( !this.m_bIgnoreResizeEvents ) 814 { 815 $J(window).on( 'resize', null, this.m_fnSizing ); 816 } 817 CModal.s_$Background.on( 'click.CModal', this.m_fnBackgroundClick ); 818 $J(document).on( 'keyup.CModal', this.m_fnOnEscapeKeyPress ); 819 820 this.AdjustSizing(); 821 822 // if we're in gamepad, notify gamepad navigation of the modal 823 if ( typeof GPOnShowingModalWindow === "function" ) 824 this.m_fnGPOnCloseModal = GPOnShowingModalWindow( this.m_$Content.get( 0 ) ); 825 826 this.m_$Content.show(); 827 828 // resize as any child image elements load in. 829 this.m_$Content.find('img').load( this.m_fnSizing ); 830 831 this.m_bVisible = true; 832 CModal.PushActiveModal( this ); 833 834 var _this = this; 835 if ( typeof GPNavFocusChild !== 'undefined' ) 836 window.setTimeout( function() { GPNavFocusChild( _this.m_$Content ) }, 1 ); }; 837 838CModal.prototype.Dismiss = function() 839{ 840 if ( !this.m_bVisible ) 841 return; 842 843 this.m_bVisible = false; 844 845 // tell gamepad navigation we're closing this modal 846 if ( this.m_fnGPOnCloseModal ) 847 { 848 this.m_fnGPOnCloseModal(); 849 this.m_fnGPOnCloseModal = null; 850 } 851 852 this.m_$Content.hide(); 853 854 if ( !this.m_bIgnoreResizeEvents ) 855 { 856 $J(window).off( 'resize', null, this.m_fnSizing ); 857 } 858 859 if ( this.m_fnOnDismiss ) 860 this.m_fnOnDismiss(); 861 862 if ( this.m_bRemoveContentOnDismissal ) 863 { 864 this.m_$Content.remove(); 865 this.m_$Content = null; 866 } 867 868 CModal.PopActiveModal( this ); 869 if ( !CModal.s_rgModalStack.length ) 870 { 871 CModal.s_$Background.off( 'click.CModal', this.m_fnBackgroundClick ); 872 $J(document).off( 'keyup.CModal', this.m_fnOnEscapeKeyPress ); 873 CModal.HideModalBackground(); 874 } 875}; 876 877CModal.prototype.BIsActiveModal = function() 878{ 879 return CModal.s_rgModalStack.length && CModal.s_rgModalStack[ CModal.s_rgModalStack.length - 1 ] == this; 880}; 881 882/* static */ 883CModal.ShowModalBackground = function() 884{ 885 if ( !CModal.s_$Background ) 886 { 887 CModal.s_$Background = $J('<div/>', {'class': 'newmodal_background'}); 888 CModal.s_$Background.css( 'opacity', 0 ); 889 $J(document.body).append( CModal.s_$Background ); 890 } 891 CModal.s_$Background.stop(); // stop running animations 892 CModal.s_$Background.fadeTo( 200, 0.8 ); 893}; 894 895CModal.HideModalBackground = function() 896{ 897 if ( CModal.s_$Background ) 898 { 899 CModal.s_$Background.stop(); // stop running animations 900 CModal.s_$Background.fadeOut( 200, 0 ); 901 } 902}; 903 904CModal.s_rgModalStack = []; 905CModal.GetActiveModal = function() 906{ 907 if ( CModal.s_rgModalStack.length ) 908 return CModal.s_rgModalStack[CModal.s_rgModalStack.length-1]; 909 else 910 return null; 911}; 912CModal.DismissActiveModal = function() 913{ 914 if ( CModal.s_rgModalStack.length ) 915 CModal.s_rgModalStack[CModal.s_rgModalStack.length-1].Dismiss(); 916}; 917 918CModal.PushActiveModal = function( Modal ) 919{ 920 for ( var i = 0; i < CModal.s_rgModalStack.length; i++ ) 921 { 922 // push below background 923 CModal.s_rgModalStack[i].m_$Content.css( 'z-index', 899 ); 924 } 925 CModal.s_rgModalStack.push( Modal ); 926}; 927 928CModal.PopActiveModal = function( Modal ) 929{ 930 for ( var i = 0; i < CModal.s_rgModalStack.length; i++ ) 931 { 932 if ( CModal.s_rgModalStack[i] == Modal ) 933 { 934 CModal.s_rgModalStack.splice( i, 1 ); 935 break; 936 } 937 } 938 939 if ( CModal.s_rgModalStack.length ) 940 CModal.s_rgModalStack[ CModal.s_rgModalStack.length - 1 ].m_$Content.css( 'z-index', 1000 ); 941}; 942 943// this will set the right headers for a cross-domain request to community 944function GetDefaultCommunityAJAXParams( path, method ) 945{ 946 var rgParams = { url: 'https://steamcommunity.com/' + path }; 947 if ( method ) 948 rgParams.type = method; 949 950 // if this js file was hosted off the store, add CORS request headers 951 if ( window.location.href.indexOf( 'https://steamcommunity.com/' ) != 0 ) 952 { 953 rgParams.crossDomain = true; 954 rgParams.xhrFields = { withCredentials: true }; 955 } 956 return rgParams; 957} 958 959function CrossDomainPost( url, params ) 960{ 961 return $J.ajax( { 962 type: 'post', 963 url: url, 964 data: params, 965 crossDomain: true, 966 xhrFields: { withCredentials: true } 967 } ); 968} 969 970// spped of the miniprofile fading in and out 971var MINIPROFILE_ANIM_SPEED = 150; 972// how long the mouse must remain over an element before we'll make an AJAX call 973var MINIPROFILE_DELAY_BEFORE_AJAX = 100; 974// the delay before we'll show the hover, must be longer than DELAY_BEFORE_AJAX 975var MINIPROFILE_DELAY_BEFORE_SHOW = 250; 976 977function CDelayedAJAXData( strURL, msDelayBeforeAJAX ) 978{ 979 this.m_$Data = null; 980 this.m_bAJAXFailed = false;
981 this.m_timerDelayedAJAX = null; 982 this.m_bAJAXRequestMade = false; 983 this.m_msDelayBeforeAJAX = msDelayBeforeAJAX; 984 this.m_strURL = strURL; 985 986 this.m_fnOnAJAXComplete = null; 987} 988 989CDelayedAJAXData.prototype.GetAJAXParams = function() 990{ 991 return GetDefaultCommunityAJAXParams( this.m_strURL, 'GET' ); 992}; 993 994CDelayedAJAXData.prototype.QueueAjaxRequestIfNecessary = function() 995{ 996 if ( !this.m_$Data && !this.m_bAJAXRequestMade ) 997 { 998 var _this = this; 999 this.m_timerDelayedAJAX = window.setTimeout( function() { 1000 _this.m_timerDelayedAJAX = null; 1001 _this.m_bAJAXRequestMade = true; 1002 var rgAJAXParams = _this.GetAJAXParams(); 1003 $J.ajax( rgAJAXParams ) 1004 .done( function(data) { 1005 _this.m_$Data = $J(data); 1006 if ( _this.m_fnOnAJAXComplete ) 1007 _this.m_fnOnAJAXComplete(); 1008 }).fail( function() { 1009 _this.m_bAJAXFailed = true; 1010 }); 1011 }, this.m_msDelayBeforeAJAX ); 1012 } 1013}; 1014 1015CDelayedAJAXData.prototype.CancelAJAX = function() 1016{ 1017 if ( this.m_timerDelayedAJAX ) 1018 window.clearTimeout( this.m_timerDelayedAJAX ); 1019 1020 this.m_fnOnAJAXComplete = null; 1021}; 1022 1023CDelayedAJAXData.prototype.RunWhenAJAXReady = function( fnOnReady ) 1024{ 1025 if ( this.m_$Data ) 1026 fnOnReady(); 1027 else if ( !this.m_bAJAXFailed ) 1028 { 1029 this.m_fnOnAJAXComplete = fnOnReady; 1030 this.QueueAjaxRequestIfNecessary(); 1031 } 1032 // if ajax failed we will not call fnOnReady 1033}; 1034 1035CDelayedAJAXData.prototype.Show = function( $HoverContent ) 1036{ 1037 $HoverContent.children().detach(); 1038 $HoverContent.append( this.m_$Data ); 1039}; 1040 1041// time the cookie preferences popup waits before appearing 1042var COOKIE_PREFERENCES_POPUP_DELAY = 4000; 1043 1044function InitCookiePreferencesPopup() 1045{ 1046 1047 var $popupDialog = $J( '#cookiePrefPopup' ); 1048 if ( $popupDialog.length == 0 ) 1049 { 1050 $AllowURL = encodeURI( 'https://store.steampowered.com/' + 'account/ajaxsetcookiepreferences'); 1051 1052 var $CPopupContent = $J( "<style>\n.replyButton {\n\twidth: 100%;\n text-align: center;\n margin-bottom: 4px;\n}\n\n.buttonGroup {\n\tflex: 18%;\n\tmargin-left: 20px;\n\tmargin-right: 0px;\n}\n.cookieMessage {\n\tflex: 85%;\n\tmargin: auto;\n}\n.popupTextTitle {\n\tpadding-bottom: 10px;\n font-size: 13px;\n line-height: 17px;\n color: #c6d4df;\n}\n.popupTextTitle a {\n text-decoration: underline;\n}\n.cookiepreferences_popup {\n\tdisplay: none;\n\tposition: fixed;\n\tbottom: 0;\n\twidth: 100%;\n\tz-index: 2000;\n\tpadding-top: 10px;\n}\n.cookiepreferences_popup_content {\n\tdisplay: flex;\n box-sizing: border-box;\n\twidth: 90%;\n\tmargin: auto;\n\tmargin-bottom: 20px;\n\tpadding: 16px 16px 10px 16px;\n max-width: 980px;\n\tbackground: linear-gradient(90.85deg, #333840 0.58%, #25282E 74.92%);\n\tbox-shadow: 0px 0px 10px #000000;\n border-left: 2px solid #00CCFF;\n}\n@media all and (max-width: 910px) {\n body.responsive_page .cookiepreferences_popup_content {\n flex-direction: column;\n }\n body.responsive_page .cookiepreferences_popup_content .cookieMessage {\n margin-bottom: 20px;\n }\n body.responsive_page .cookiepreferences_popup_content .buttonGroup {\n flex-direction: row;\n margin-left: 0px;\n }\n body.responsive_page .cookiepreferences_popup_content .buttonGroup .replyButton {\n width: unset;\n }\n}\n<\/style>\n\n<div id=\"cookiePrefPopup\" class=\"cookiepreferences_popup\">\n\t<div class=\"cookiepreferences_popup_content\">\n\t\t<div class=\"cookieMessage\">\n\t\t\t<div class=\"popupTextTitle\">Do you mind if we use optional cookies to provide you personalized content and to analyze site traffic?<\/div>\n\t\t\t<div class=\"popupTextTitle\">We don't use a lot of cookies; you can see and manage them at any time on our <a href=\"https:\/\/store.steampowered.com\/account\/cookiepreferences\">Cookie Settings page<\/a>. If you click 'Accept All,' you consent to the use of cookies on Steam websites. Learn more about cookies in our <a href=\"https:\/\/store.steampowered.com\/privacy_agreement\">Privacy Policy<\/a>.<\/div>\n\t\t<\/div>\n\t\t<div class=\"buttonGroup\">\n\t\t\t<div id=\"acceptAllButton\" class=\"btn_blue_steamui btn_medium replyButton\"><span>Accept All<\/span><\/div>\n\t\t\t<div id=\"rejectAllButton\" class=\"btn_grey_steamui btn_medium replyButton\"><span>Reject All<\/span><\/div>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n" ); 1053 1054 var $onAcceptButton = $CPopupContent.find( '#acceptAllButton' ); 1055 var $onRejectButton = $CPopupContent.find( '#rejectAllButton' ); 1056 1057 if ( $onAcceptButton.length == 0 || $onRejectButton.length == 0 ) 1058 { 1059 // template appears broken 1060 console.error("unable to display preferences popup"); 1061 return; 1062 } 1063 1064 var fnPostPreference = function ( bAllowChoice ) 1065 { 1066 $J.ajax( { type: "POST", 1067 url: $AllowURL,
1068 data: { bAllow: bAllowChoice, sessionid: g_sessionID }, 1069 crossDomain: true, 1070 xhrFields: { withCredentials: true } 1071 } ).done( function ( data ) 1072 { 1073 if ( data && data.transfer_urls && data.transfer_params ) 1074 { 1075 for ( var i = 0; i < data.transfer_urls.length; i++ ) 1076 { 1077 $J.ajax( { type: "POST", 1078 url: data.transfer_urls[i], 1079 data: { transfer_params: data.transfer_params }, 1080 crossDomain: true, 1081 xhrFields: { withCredentials: true } 1082 } ); 1083 } 1084 } 1085 }); 1086 $J( '#cookiePrefPopup' ).hide(); 1087 1088 } 1089 1090 $onAcceptButton.on('click', '', function() { 1091 fnPostPreference( 1 ); 1092 } ); 1093 1094 $onRejectButton.on('click', '', function () 1095 { 1096 fnPostPreference( 0 ); 1097 } ); 1098 1099 $J('body').append( $CPopupContent ); 1100 1101 timerCookiePopup = window.setTimeout( function() { 1102 $J("#cookiePrefPopup").show(); 1103 }, COOKIE_PREFERENCES_POPUP_DELAY ); 1104 } 1105} 1106 1107// The /miniprofile/ is cacheable. We need to specify the origin URL of the request to avoid CORS see cl 5773210 1108function InitMiniprofileHovers( origin ) 1109{ 1110 var $Hover = $J('<div/>', {'class': 'miniprofile_hover'} ); 1111 var $HoverContent = $J('<div/>', {'class': 'miniprofile_hover_inner shadow_content'} ); 1112 1113 $Hover.append( $J('<div class="shadow_ul"></div><div class="shadow_top"></div><div class="shadow_ur"></div><div class="shadow_left"></div><div class="shadow_right"></div><div class="shadow_bl"></div><div class="shadow_bottom"></div><div class="shadow_br"></div>'), $HoverContent ); 1114 1115 $Hover.hide(); 1116 $J(document.body).append( $Hover ); 1117 1118 var fnDataFactory = function( key ) { 1119 // key is either a number (accountid) or a string of "accountid_appid" 1120 var rgKey = typeof key === "string" ? key.split( /_/ ) : [ key ]; 1121 var strURL = 'miniprofile/' + rgKey[0]; 1122 1123 if ( rgKey[1] ) 1124 { 1125 strURL += '?appid=' + rgKey[1]; 1126 1127 if( origin ) 1128 { 1129 strURL += '&origin=' + origin; 1130 } 1131 } 1132 else if( origin ) 1133 { 1134 strURL += '?origin=' + origin; 1135 } 1136 1137 return new CDelayedAJAXData( strURL, MINIPROFILE_DELAY_BEFORE_AJAX ); 1138 }; 1139 1140 var fnReadKey = function ( $Target ) { 1141 var appid = $Target.parents('[data-miniprofile-appid]').first().data('miniprofile-appid'); 1142 if( appid ) 1143 { 1144 return $Target.data('miniprofile') + '_' + appid; 1145 } 1146 else 1147 { 1148 return $Target.data('miniprofile') 1149 } 1150 }; 1151 1152 var rgCallbacks = BindAJAXHovers( $Hover, $HoverContent, { 1153 fnDataFactory: fnDataFactory, 1154 fnPositionHover: PositionMiniprofileHover, 1155 fnReadKey: fnReadKey, 1156 strDataName: 'miniprofile', 1157 strURLMatch: 'miniprofile' 1158 } ); 1159} 1160 1161function _RegisterAJAXHoverHideFunction( fnHide ) 1162{ 1163 if ( typeof g_rgfnHideAJAXHover == 'undefined' ) 1164 { 1165 g_rgfnHideAJAXHover = []; 1166 $J(window).blur( HideAJAXHovers ); 1167 } 1168 1169 g_rgfnHideAJAXHover.push( fnHide ); 1170} 1171 1172function HideAJAXHovers() 1173{ 1174 if ( typeof g_rgfnHideAJAXHover != 'undefined' ) 1175 { 1176 for ( var i = 0; i < g_rgfnHideAJAXHover.length; i++ ) 1177 g_rgfnHideAJAXHover[i](); 1178 } 1179} 1180 1181function BindAJAXHovers( $Hover, $HoverContent, oParams ) 1182{ 1183 var fnDataFactory = oParams.fnDataFactory; 1184 var fnPositionHover = oParams.fnPositionHover || PositionMiniprofileHover; 1185 var strDataName = oParams.strDataName; 1186 var strURLMatch = oParams.strURLMatch; 1187 var fnReadKey = function( $Element ) { return $Element.data( strDataName ); }; 1188 if ( oParams.fnReadKey ) 1189 fnReadKey = oParams.fnReadKey; 1190 var strSelector = oParams.strSelector || '[data-' + strDataName + ']'; 1191 var nDelayBeforeShow = oParams.nDelayBeforeShow || MINIPROFILE_DELAY_BEFORE_SHOW; 1192 1193 // indexed by accountid 1194 var rgHoverData = {}; 1195 var HoverTarget = null; 1196 var timerHover = null; 1197 1198 var fnOnHover = function( $Target, key ) { 1199 1200 var bHoverVisible = ( $Hover.css('display') != 'none' ); 1201 1202 var HoverData = rgHoverData[key]; 1203 if ( !HoverData ) 1204 { 1205 HoverData = rgHoverData[key] = fnDataFactory( key ); 1206 } 1207 1208 if ( HoverTarget == HoverData && bHoverVisible ) 1209 { 1210 //really only want to do this while fading out 1211 $Hover.stop(); 1212 fnPositionHover( $Hover, $Target ); 1213 $Hover.show(); // PositionMiniprofile toggles visibility 1214 $Hover.fadeTo( MINIPROFILE_ANIM_SPEED, 1.0 ); 1215 } 1216 else if ( !timerHover || HoverData != HoverTarget ) 1217 { 1218 // this is the new target 1219 if ( HoverTarget && HoverTarget != HoverData ) 1220 HoverTarget.CancelAJAX(); 1221 HoverTarget = HoverData; 1222 1223 if ( timerHover ) 1224 { 1225 window.clearTimeout( timerHover ); 1226 timerHover = null; 1227 } 1228 1229 HoverData.QueueAjaxRequestIfNecessary(); 1230 1231 timerHover = window.setTimeout( function() { 1232 window.clearTimeout( timerHover ); 1233 timerHover = null; 1234 1235 HoverData.RunWhenAJAXReady( function() { 1236 HoverData.Show( $HoverContent ); 1237 $Hover.stop(); 1238 $Hover.css( 'opacity', '' ); //clean up jquery animation 1239 fnPositionHover( $Hover, $Target ); 1240 $Hover.fadeIn( MINIPROFILE_ANIM_SPEED ); 1241 var videoElem = $Hover.find( 'video' ); 1242 if ( videoElem.length != 0 ) 1243 {
1244 var playPromise = videoElem[0].play(); 1245 if ( playPromise ) 1246 { 1247 playPromise.catch( function( e ) { 1248 } ); 1249 } 1250 } 1251 } ); 1252 }, nDelayBeforeShow ); 1253 } 1254 }; 1255 1256 var fnCancelHover = function( key ) 1257 { 1258 var bHoverVisible = ( $Hover.css('display') != 'none' ); 1259 1260 if ( key ) 1261 { 1262 var HoverData = rgHoverData[key]; 1263 if ( HoverData ) 1264 { 1265 HoverData.CancelAJAX(); 1266 } 1267 } 1268 1269 if ( timerHover ) 1270 { 1271 window.clearTimeout( timerHover ); 1272 timerHover = null; 1273 } 1274 1275 if ( bHoverVisible ) 1276 { 1277 $Hover.stop(); 1278 $Hover.fadeOut( MINIPROFILE_ANIM_SPEED ); 1279 } 1280 }; 1281 1282 var strEventNamespace = 'AjaxHover' + strDataName; 1283 $J(document ).on( 'mouseenter.' + strEventNamespace + ' touchend.' + strEventNamespace, strSelector, function(e) { 1284 var $Target = $J(this); 1285 fnOnHover( $Target, fnReadKey( $Target) ); 1286 1287 // Prevent click if we're sticky 1288 if ( typeof $Target.data('stickyhover') !== 'undefined' ) 1289 { 1290 e.preventDefault(); 1291 } 1292 } ); 1293 $J(document ).on( 'click.' + strEventNamespace + ' mouseleave.' + strEventNamespace, strSelector, function(e) { 1294 var $Target = $J(this); 1295 1296 // Cancel if we're not sticky 1297 if ( typeof $Target.data('stickyhover') == 'undefined' ) 1298 { 1299 fnCancelHover(); 1300 } 1301 } ); 1302 // Cancel when clicking/touching elsewhere regardless if sticky 1303 $J(document ).on( 'click', null, fnCancelHover ); 1304 1305 // register this hover so HideAJAXHovers() can hide it when invoked 1306 _RegisterAJAXHoverHideFunction( fnCancelHover ); 1307 1308 return { 1309 fnCancelHover: fnCancelHover 1310 }; 1311} 1312 1313function PositionMiniprofileHover( $Hover, $Target, oParams ) 1314{ 1315 if ( !oParams ) 1316 oParams = {}; 1317 var bPreferRightSide = oParams.bPreferRightSide || false; 1318 var nPxArrowOverlap = ( oParams.nPxArrowOverlap != undefined ) ? oParams.nPxArrowOverlap : 2; 1319 1320 $Hover.css( 'visibility', 'hidden' ); 1321 $Hover.show(); 1322 1323 var offset = $Target.offset(); 1324 $Hover.css( 'left', offset.left + 'px' ); 1325 $Hover.css( 'top', offset.top + 'px'); 1326 1327 var $HoverBox = $Hover.children( '.shadow_content' ); 1328 if ( !$HoverBox.length ) 1329 $HoverBox = $J( $Hover.children()[0] ); 1330 1331 var $HoverArrowLeft = $Hover.children( '.hover_arrow.left' ); 1332 var $HoverArrowRight = $Hover.children( '.hover_arrow.right' ); 1333 1334 var nWindowScrollTop = $J(window).scrollTop(); 1335 var nWindowScrollLeft = $J(window).scrollLeft(); 1336 var nViewportWidth = $J(window).width(); 1337 var nViewportHeight = $J(window).height(); 1338 1339 var $HoverArrow = $HoverArrowRight; 1340 var nBoxRightViewport = ( offset.left - nWindowScrollLeft ) + $Target.outerWidth() + $HoverBox.width() + 14; 1341 var nSpaceRight = nViewportWidth - nBoxRightViewport; 1342 var nSpaceLeft = offset.left - $Hover.width(); 1343 if ( ( ( nSpaceLeft > 0 || nSpaceLeft > nSpaceRight ) && !bPreferRightSide ) || ( bPreferRightSide && nSpaceRight < 14 && nSpaceLeft > nSpaceRight ) ) 1344 { 1345 $Hover.css( 'left', ( offset.left - $Hover.width() + nPxArrowOverlap + 3 ) + 'px' ); 1346 $HoverArrowLeft.hide(); 1347 $HoverArrowRight.show(); 1348 } 1349 else 1350 { 1351 $Hover.css( 'left', ( offset.left + $Target.outerWidth() - nPxArrowOverlap ) + 'px' ); 1352 $HoverArrow = $HoverArrowLeft; 1353 $HoverArrowLeft.show(); 1354 $HoverArrowRight.hide(); 1355 } 1356 1357 var nTopAdjustment = 0; 1358 1359 if ( $Target.height() < 48 ) 1360 nTopAdjustment = Math.floor( $Target.height() / 2 ) - 24; 1361 var nDesiredHoverTop = offset.top - 15 + nTopAdjustment; 1362 $Hover.css( 'top', nDesiredHoverTop + 'px' ); 1363 1364 // see if the hover is cut off by the bottom of the window, and bump it up if neccessary 1365 var nTargetTopViewport = ( offset.top - nWindowScrollTop ) + nTopAdjustment; 1366 if ( nTargetTopViewport + $HoverBox.height() + 35 > nViewportHeight ) 1367 { 1368 var nViewportAdjustment = ( $HoverBox.height() + 35 ) - ( nViewportHeight - nTargetTopViewport ); 1369 1370 // if the hover has the "in-game" block at the bottom, we need to have more space at the bottom of the hover 1371 // so that the arrow will appear in the blue part of the hover. This means the game info may be off-screen below. 1372 var bHaveInGameInfo = $HoverBox.find('.miniprofile_ingame').length > 0; 1373 var nHoverBoxBottomMinimum = ( bHaveInGameInfo ? 78 : 24 ); // the minimum amount of space we need below the arrow 1374 nViewportAdjustment = Math.min( $HoverBox.height() - nHoverBoxBottomMinimum, nViewportAdjustment ); 1375 1376 var nViewportAdjustedHoverTop = offset.top - nViewportAdjustment;
1377 $Hover.css( 'top', nViewportAdjustedHoverTop + 'px' ); 1378 1379 // arrow is normally offset 30pixels. we move it down the same distance we moved the hover up, so it is "fixed" to where it was initially 1380 $HoverArrow.css( 'top', ( 30 + nDesiredHoverTop - nViewportAdjustedHoverTop ) + 'px' ); 1381 } 1382 else 1383 { 1384 $HoverArrow.css( 'top', '' ); 1385 } 1386 1387 $Hover.hide(); 1388 $Hover.css( 'visibility', '' ); 1389} 1390 1391 1392/* Emoticon hovers */ 1393 1394function CEmoticonDelayedAJAXData( strEmoticonName, msDelayBeforeAJAX ) 1395{ 1396 CDelayedAJAXData.apply( this, [ 'economy/emoticonhover/' + strEmoticonName + '/jsonp.js', msDelayBeforeAJAX ]); 1397 this.m_strEmoticonName = strEmoticonName; 1398} 1399 1400// subclass CDelayedAJAXData so we can request via JSONP 1401CEmoticonDelayedAJAXData.prototype = new CDelayedAJAXData; 1402CEmoticonDelayedAJAXData.prototype.constructor = CEmoticonDelayedAJAXData; 1403 1404CEmoticonDelayedAJAXData.prototype.GetAJAXParams = function() 1405{ 1406 return { 1407 url: 'https://community.fastly.steamstatic.com/' + this.m_strURL, 1408 dataType: 'jsonp', 1409 jsonpCallback: 'OnLoadEmoticon_' + this.m_strEmoticonName, //consistent name for cachability 1410 cache: true, 1411 data: {l: 'english' } 1412 } 1413}; 1414 1415function InitEmoticonHovers() 1416{ 1417 var $Hover = $J('<div/>', {'class': 'emoticon_hover'} ); 1418 var $HoverContent = $J('<div/>', {'class': 'emoticon_hover_content'} ); 1419 $Hover.append( $HoverContent, $J('<div/>', {'class': 'hover_arrow left emoticon_hover_arrow' } ), $J('<div/>', {'class': 'hover_arrow right emoticon_hover_arrow' } ) ); 1420 $Hover.hide(); 1421 1422 var fnOneTimeEmoticonSetup = function() { 1423 $J(document.body).append( $Hover ); 1424 }; 1425 1426 var fnReadKey = function ( $Element ) { 1427 if ( $Element.data('emoticon') ) 1428 { 1429 return $Element.data('emoticon'); 1430 } 1431 else if ( $Element.attr( 'src' ) ) 1432 { 1433 var rgMatches = $Element.attr( 'src' ).match( 'emoticon/(.*)$' ); 1434 if ( rgMatches && rgMatches[1] ) 1435 { 1436 var strEmoticon = rgMatches[1]; 1437 if ( strEmoticon.length > 1 ) 1438 { 1439 if ( strEmoticon[0] == ':' ) 1440 strEmoticon = strEmoticon.substr( 1 ); 1441 if ( strEmoticon[ strEmoticon.length - 1 ] == ':' ) 1442 strEmoticon = strEmoticon.substr( 0, strEmoticon.length - 1 ); 1443 } 1444 return strEmoticon; 1445 } 1446 } 1447 1448 return null; 1449 }; 1450 1451 var fnDataFactory = function( key ) { 1452 if ( fnOneTimeEmoticonSetup ) 1453 { 1454 fnOneTimeEmoticonSetup(); 1455 fnOneTimeEmoticonSetup = null; 1456 } 1457 1458 return new CEmoticonDelayedAJAXData( key, 50 ); 1459 }; 1460 1461 var rgCallbacks = BindAJAXHovers( $Hover, $HoverContent, { 1462 fnDataFactory: fnDataFactory, 1463 fnPositionHover: function( $Hover, $Target ) { 1464 PositionMiniprofileHover( $Hover, $Target, { 1465 bPreferRightSide: true, 1466 nPxArrowOverlap: 0 1467 } ); 1468 //slide it down a little for emoticon option popup 1469 if ( $Target.hasClass('emoticon_option') ) 1470 $Hover.css( 'top', parseInt( $Hover.css('top') ) + 5 ); 1471 }, 1472 fnReadKey: fnReadKey, 1473 strSelector: 'img.emoticon', 1474 strURLMatch: 'emoticonhover', 1475 nDelayBeforeShow: 50 1476 } ); 1477 1478 window.DismissEmoticonHover = rgCallbacks.fnCancelHover; 1479} 1480 1481function V_EscapeRegExp( str ) 1482{ 1483 return str.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); 1484} 1485 1486function V_EscapeHTML( str ) 1487{ 1488 return String(str).replace( /&/g, '&' ).replace( /["]/g, '"' ).replace( /[']/g, ''' ).replace( /</g, '<').replace( />/g, '>'); 1489} 1490 1491function v_trim( str ) 1492{ 1493 if ( str.trim ) 1494 return str.trim(); 1495 else 1496 { 1497 return str.replace(/^\s+/, '').replace(/\s+$/, ''); 1498 } 1499} 1500 1501function V_ParseJSON( str ) 1502{ 1503 if ( typeof JSON == 'object' && JSON.parse ) 1504 return JSON.parse( str ); // built-in / json2.js 1505 else 1506 str.evalJSON(); // prototype 1507} 1508 1509function V_ToJSON( object ) 1510{ 1511 if ( typeof JSON == 'object' && JSON.stringify ) 1512 return JSON.stringify( object ); // built-in / json2.js 1513 else 1514 Object.toJSON( object ); // prototype 1515} 1516 1517function V_IsJSON( str ) 1518{ 1519 try 1520 { 1521 if( typeof JSON == 'object' && JSON.parse ) 1522 { 1523 var o = JSON.parse(str); 1524 if ( o !== null ) 1525 return true; 1526 1527 } else { 1528 return str.isJSON(); 1529 } 1530 } 1531 catch (e) { } 1532 return false; 1533} 1534 1535 1536function V_GetCookie( strCookieName ) 1537{ 1538 var rgMatches = document.cookie.match( '(^|; )' + strCookieName + '=([^;]*)' ); 1539 if ( rgMatches && rgMatches[2] ) 1540 return rgMatches[2]; 1541 else 1542 return null; 1543} 1544 1545 1546function V_GetDecodedCookie( strCookieName ) 1547{ 1548 var value = V_GetCookie( strCookieName ); 1549 return value && decodeURIComponent( value ); 1550} 1551 1552function V_SetCookie( strCookieName, strValue, expiryInDays, path ) 1553{ 1554 if ( !path ) 1555 path = '/'; 1556 1557 var strDate = ''; 1558 1559 if( typeof expiryInDays != 'undefined' && expiryInDays ) 1560 { 1561 var dateExpires = new Date(); 1562 dateExpires.setTime( dateExpires.getTime() + 1000 * 60 * 60 * 24 * expiryInDays ); 1563 strDate = '; expires=' + dateExpires.toGMTString(); 1564 } 1565 1566 document.cookie = strCookieName + '=' + strValue + strDate + ';path=' + path; 1567} 1568 1569function _GetStorageFromCookie() 1570{ 1571 var oStorage = {}; 1572 1573 var strStorageJSON = V_GetCookie( 'storage' ); 1574 if ( strStorageJSON ) 1575 { 1576 try { 1577 oStorage = V_ParseJSON(decodeURIComponent(strStorageJSON)); 1578 } 1579 catch (e) { 1580 oStorage = {}; 1581 } 1582 } 1583 1584 return oStorage; 1585} 1586 1587function BInsideIFrame() 1588{ 1589 try 1590 { 1591 return window.self !== window.top; 1592 } 1593 catch( e ) 1594 { 1595 return true; 1596 } 1597} 1598 1599function SetValueLocalStorage( strPreferenceName, value ) 1600{ 1601 if ( !BInsideIFrame() && window.localStorage ) 1602 { 1603 window.localStorage[strPreferenceName] = value; 1604 } 1605 else 1606 { 1607 var oStorage = _GetStorageFromCookie(); 1608 1609 oStorage[strPreferenceName] = value; 1610 1611 V_SetCookie( 'storage', encodeURIComponent( V_ToJSON( oStorage ) ), 365 ) 1612 } 1613} 1614 1615function UnsetValueLocalStorage( strPreferenceName ) 1616{ 1617 if ( !BInsideIFrame() && window.localStorage ) 1618 { 1619 delete window.localStorage[strPreferenceName]; 1620 } 1621 else 1622 { 1623 var oStorage = _GetStorageFromCookie(); 1624 1625 delete oStorage[strPreferenceName]; 1626 1627 V_SetCookie( 'storage', encodeURIComponent( V_ToJSON( oStorage ) ), 365 ) 1628 } 1629} 1630 1631function GetValueLocalStorage( strPreferenceName, defaultValue ) 1632{ 1633 if ( !BInsideIFrame() && window.localStorage ) 1634 { 1635 return window.localStorage[strPreferenceName] || defaultValue; 1636 } 1637 else 1638 { 1639 var oStorage = _GetStorageFromCookie(); 1640 1641 return oStorage[strPreferenceName] || defaultValue; 1642 } 1643} 1644 1645function DynamicLink_PlayYouTubeVideoInline( elem, videoid ) 1646{ 1647 var el = $J(elem); 1648 var youtubeurl = location.protocol + '//www.youtube.com/embed/' + videoid + '?showinfo=0&autohide=1&fs=1&hd=1&modestbranding=1&rel=0&showsearch=0&wmode=direct&autoplay=1'; 1649 var wrapper = $J( '<div/>', { 'class': 'dynamiclink_youtubeviewvideo' } ); 1650 var embed_wrapper = $J( '<div/>', { 'class' : 'dynamiclink_youtubeviewvideoembedded', 'frameborder' : '0' } );
1651 var iframeContent = $J( '<iframe/>', { 'frameborder' : '0', src: youtubeurl, 'allowfullscreen': '' } ); 1652 if ( el.length ) 1653 { 1654 embed_wrapper.append( iframeContent ); 1655 wrapper.append( embed_wrapper ); 1656 el.after( wrapper ); 1657 el.remove(); 1658 } 1659} 1660 1661function DynamicLink_PlayVimeoVideoInline( elem, videoid ) 1662{ 1663 var el = $J(elem); 1664 var videourl = 'https://player.vimeo.com/video/' + videoid + '?badge=0&autopause=0&autoplay=1&player_id=0'; 1665 var wrapper = $J( '<div/>', { 'class': 'dynamiclink_youtubeviewvideo' } ); 1666 var embed_wrapper = $J( '<div/>', { 'class' : 'dynamiclink_youtubeviewvideoembedded', 'frameborder' : '0' } ); 1667 var iframeContent = $J( '<iframe/>', { 'frameborder' : '0', src: videourl, webkitallowfullscreen : '', mozallowfullscreen : '', allowfullscreen : '' } ); 1668 if ( el.length ) 1669 { 1670 embed_wrapper.append( iframeContent ); 1671 wrapper.append( embed_wrapper ); 1672 el.after( wrapper ); 1673 el.remove(); 1674 } 1675} 1676 1677function DynamicLink_ShowSketchfabModelInline( elem, details ) 1678{ 1679 var url = "https://sketchfab.com/models/" + details.modelid + "/embed?autostart=1"; 1680 var el = $J(elem); 1681 var wrapper = $J( '<div/>', { 'class' : 'dynamiclink_sketchfabmodelembedded', 'frameborder' : '0' } ); 1682 var iframeContent = $J( '<iframe/>', { 'frameborder' : '0', "mozallowfullscreen" : true, "webkitallowfullscreen" : true, src: url } ); 1683 if ( el.length ) 1684 { 1685 wrapper.append( iframeContent ); 1686 el.after( wrapper ); 1687 el.remove(); 1688 } 1689} 1690 1691function ReplaceDynamicLink( id, strHTML ) 1692{ 1693 var el = $J('#'+id); 1694 if ( el.length && strHTML.length > 0 ) 1695 { 1696 var newEl = $J( strHTML ); 1697 el.after( newEl ); 1698 el.remove(); 1699 1700 if ( typeof window['HandleNewDynamicLink'] === "function" ) 1701 { 1702 HandleNewDynamicLink( newEl ); 1703 } 1704 } 1705} 1706 1707function ShowBannedDynamicLink( el ) 1708{ 1709 el = $J( el ); 1710 if ( el.length > 0 ) 1711 { 1712 var bannedContentURL = el.data( 'bannedurl' ); 1713 var link = $J( "<a>", { href: bannedContentURL, text: bannedContentURL } ); 1714 el.html( link ); 1715 el.attr( 'onclick', '' ); 1716 } 1717} 1718 1719 1720 1721function CScrollOffsetWatcher( el, fnCallback ) 1722{ 1723 this.m_$Element = $J(el); 1724 this.nOffsetTop = this.m_$Element.offset().top; 1725 this.nBufferHeight = 500; 1726 1727 this.nOffsetTopTrigger = this.nOffsetTop - this.nBufferHeight; 1728 1729 this.fnOnHit = fnCallback; 1730 1731 CScrollOffsetWatcher.RegisterWatcher( this ); 1732} 1733 1734CScrollOffsetWatcher.prototype.SetBufferHeight = function( nHeight ) 1735{ 1736 this.nBufferHeight = nHeight; 1737 this.Recalc(); 1738}; 1739 1740CScrollOffsetWatcher.prototype.Recalc = function() 1741{ 1742 this.nOffsetTop = this.m_$Element.offset().top; 1743 this.nOffsetTopTrigger = this.nOffsetTop - this.nBufferHeight; 1744}; 1745 1746CScrollOffsetWatcher.sm_rgWatchers = []; 1747CScrollOffsetWatcher.m_nTimeoutInitialLoad = 0; 1748CScrollOffsetWatcher.RegisterWatcher = function( Watcher ) 1749{ 1750 var bHadWatchers = CScrollOffsetWatcher.sm_rgWatchers.length > 0; 1751 1752 // keep the list sorted by offset trigger 1753 var iInsertionPoint; 1754 for( iInsertionPoint = CScrollOffsetWatcher.sm_rgWatchers.length; iInsertionPoint > 0 ; iInsertionPoint-- ) 1755 { 1756 if ( Watcher.nOffsetTopTrigger > CScrollOffsetWatcher.sm_rgWatchers[iInsertionPoint - 1].nOffsetTopTrigger ) 1757 break; 1758 } 1759 CScrollOffsetWatcher.sm_rgWatchers.splice( iInsertionPoint, 0, Watcher ); 1760 1761 if ( !bHadWatchers ) 1762 { 1763 $J(window).on( 'scroll.ScrollOffsetWatcher', CScrollOffsetWatcher.OnScroll ); 1764 1765 var nRecalcTimer = 0; 1766 $J(window).on( 'resize.ScrollOffsetWatcher', function() { 1767 if ( nRecalcTimer ) 1768 window.clearTimeout( nRecalcTimer ); 1769 nRecalcTimer = window.setTimeout( CScrollOffsetWatcher.ForceRecalc, 500 ); 1770 } ); 1771 } 1772 1773 // use a 1ms timeout to roll these together as much as possible on page load 1774 if ( !CScrollOffsetWatcher.m_nTimeoutInitialLoad ) 1775 CScrollOffsetWatcher.m_nTimeoutInitialLoad = window.setTimeout( function() { CScrollOffsetWatcher.OnScroll(); CScrollOffsetWatcher.m_nTimeoutInitialLoad = 0; }, 1 ); 1776}; 1777 1778CScrollOffsetWatcher.ForceRecalc = function() 1779{ 1780 for ( var i = 0; i < CScrollOffsetWatcher.sm_rgWatchers.length; i++ ) 1781 { 1782 CScrollOffsetWatcher.sm_rgWatchers[i].Recalc(); 1783 } 1784 1785 CScrollOffsetWatcher.OnScroll(); 1786}; 1787 1788CScrollOffsetWatcher.OnScroll = function() 1789{ 1790 var supportPageOffset = window.pageYOffset !== undefined; 1791 var isCSS1Compat = ((document.compatMode || "") === "CSS1Compat"); 1792 1793 var nScrollY = supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop; 1794 var nOffsetBottom = nScrollY + window.innerHeight; 1795 1796 for( let i = CScrollOffsetWatcher.sm_rgWatchers.length - 1; i >= 0; i-- ) 1797 { 1798 var Watcher = CScrollOffsetWatcher.sm_rgWatchers[i]; 1799 if ( nOffsetBottom > Watcher.nOffsetTopTrigger ) 1800 { 1801 // make sure the page hasn't changed and we really need to show content 1802 Watcher.Recalc(); 1803 if ( nOffsetBottom > Watcher.nOffsetTopTrigger ) 1804 { 1805 Watcher.fnOnHit(); 1806 CScrollOffsetWatcher.sm_rgWatchers.splice( i, 1 ); 1807 } 1808 } 1809 } 1810 1811 if ( CScrollOffsetWatcher.sm_rgWatchers.length === 0 ) 1812 { 1813 $J(window).off( 'scroll.ScrollOffsetWatcher' ); 1814 $J(window).off( 'resize.ScrollOffsetWatcher' ); 1815 } 1816}; 1817 1818function LoadImageGroupOnScroll( elTarget, strGroup ) 1819{ 1820 var $Element = $J( '#' + elTarget ); 1821 if ( $Element.length ) 1822 new CScrollOffsetWatcher( $Element, function() { LoadDelayedImages(strGroup); } ); 1823} 1824 1825function LoadDelayedImages( group, bForce ) 1826{ 1827 if ( ( typeof g_rgDelayedLoadImages != 'undefined' && g_rgDelayedLoadImages[group] ) || bForce ) 1828 { 1829 $J('[data-delayed-image-group=' + group + ']').each( function() 1830 { 1831 $J(this).attr('src', $J(this).attr('data-delayed-image') ); 1832 }); 1833 1834 delete g_rgDelayedLoadImages[group]; 1835 } 1836} 1837 1838WebStorage = { 1839 GetLocal: function ( key, bSessionOnly ) 1840 { 1841 var type = ( bSessionOnly ) ? 'session' : 'local'; 1842 1843 if ( !window[type + 'Storage'] )
1844 return WebStorage.GetCookie( key ); 1845 1846 var storage = window[type + 'Storage']; 1847 var value; 1848 try { 1849 value = storage.getItem(key); 1850 } catch(err) { 1851 // storage.getItem can throw SecurityError based on user's browser setting. 1852 value = null; 1853 } 1854 1855 try { 1856 value = V_ParseJSON( value ); 1857 } catch(err){ 1858 return null; 1859 } 1860 1861 if( value == null ) 1862 { 1863 // Check if we have the value stored in a cookie instead. If so, move that to LS and remove the cookie 1864 value = V_GetCookie( key ); 1865 if( value != null ) 1866 { 1867 WebStorage.SetLocal( key, value, bSessionOnly ); 1868 WebStorage.ClearCookie( key ); 1869 } 1870 } 1871 return value; 1872 }, 1873 GetLocalSession: function( key ) 1874 { 1875 return WebStorage.GetLocal( key, true ); 1876 }, 1877 SetLocal: function ( key, value, bSessionOnly ) 1878 { 1879 var type = ( bSessionOnly ) ? 'session' : 'local'; 1880 1881 if ( !window[type + 'Storage'] ) 1882 return WebStorage.SetCookie( key, value, ( bSessionOnly ) ? null : 365 ); 1883 1884 value = V_ToJSON( value ); 1885 1886 var storage = window[type + 'Storage']; 1887 storage.setItem( key, value ); 1888 }, 1889 SetLocalSession: function( key, value ) 1890 { 1891 WebStorage.SetLocal( key, value, true ); 1892 }, 1893 GetCookie: function( key ) 1894 { 1895 var keyValue = V_GetCookie( key ); 1896 if ( keyValue ) 1897 keyValue = decodeURIComponent( keyValue ); 1898 1899 return V_IsJSON( keyValue ) ? V_ParseJSON( keyValue ) : keyValue; 1900 }, 1901 SetCookie: function( key, value, duration ) 1902 { 1903 value = encodeURIComponent( V_ToJSON( value ) ); 1904 V_SetCookie( key, value, duration ); 1905 }, 1906 ClearCookie: function( key ) 1907 { 1908 WebStorage.SetCookie(key, null, -1 ); 1909 }, 1910 RemoveLocal : function ( key, bSessionOnly ) 1911 { 1912 var type = ( bSessionOnly ) ? 'session' : 'local'; 1913 1914 if ( !window[type + 'Storage'] ) 1915 return WebStorage.ClearCookie( key ); 1916 1917 var storage = window[type + 'Storage']; 1918 1919 storage.removeItem( key ); 1920 } 1921}; 1922 1923// takes an integer 1924function v_numberformat( n, decimals, strDecimalSymbol, strThousandsSeperator ) 1925{ 1926 if ( typeof strDecimalSymbol == 'undefined' && typeof strThousandsSeperator == 'undefined' && typeof Number != 'undefined' && typeof Number.toLocaleString != 'undefined' ) 1927 { 1928 var options = {}; 1929 if ( typeof decimals != 'undefined' && decimals !== 0 ) 1930 { 1931 options.minimumFractionDigits = decimals; 1932 options.maximumFractionDigits = decimals; 1933 } 1934 1935 var num = new Number(n); 1936 try 1937 { 1938 return num.toLocaleString( false, options ); 1939 } 1940 catch( e ) 1941 { 1942 return num.toString(); 1943 } 1944 } 1945 1946 var str = '' + ( n ? n : 0 ); 1947 var len = str.indexOf( '.' ); 1948 if ( len == -1 ) 1949 len = str.length; 1950 var out = ''; 1951 for ( var i = 0; i < len; i++ ) 1952 { 1953 var c = str.charAt(i); 1954 out += c; 1955 if ( i < len - 1 && (len - i - 1) % 3 == 0 && c != '-' ) 1956 out += ( typeof strThousandsSeperator == 'undefined' ? ',' : strThousandsSeperator ); 1957 } 1958 if ( ( len < str.length || decimals ) && typeof decimals != 'undefined' && decimals !== 0 ) 1959 { 1960 len++; 1961 out += ( typeof strDecimalSymbol == 'undefined' ? ',' : strDecimalSymbol ); 1962 for ( var i = 0; i < ( decimals ? decimals : str.length - len ); i++ ) 1963 { 1964 if ( len + i < str.length ) 1965 out += str.charAt( len + i ); 1966 else 1967 out += '0'; 1968 } 1969 } 1970 1971 return out; 1972} 1973 1974 1975function v_shuffle( rgArray ) 1976{ 1977 for ( var i = 0; i < rgArray.length; i++ ) 1978 { 1979 var iSwap = Math.floor( Math.random() * ( rgArray.length - i ) + i ); 1980 if ( iSwap != i ) 1981 { 1982 var temp = rgArray[iSwap]; 1983 rgArray[iSwap] = rgArray[i]; 1984 rgArray[i] = temp; 1985 } 1986 } 1987 1988 return rgArray; 1989} 1990 1991 1992function UpdateFormattedNumber( element, delta ) 1993{ 1994 var $Element = $J(element); 1995 $Element.text( v_numberformat( parseInt( $Element.text().replace( /,/, '' ) ) + delta ) ); 1996} 1997 1998function RateAnnouncement( rateURL, gid, bVoteUp, clanID ) 1999{ 2000 if ( bVoteUp && $J('#VoteUpBtn_' + gid).hasClass( "btn_active" ) ) 2001 { 2002 return; 2003 } 2004 if ( !bVoteUp && $J('#VoteDownBtn_' + gid).hasClass( "btn_active" ) ) 2005 { 2006 return; 2007 } 2008 2009 rateURL = rateURL + gid; 2010 $J.post( rateURL, { 2011 'voteup' : bVoteUp, 2012 'clanid' : clanID, 2013 'sessionid' : g_sessionID 2014 } 2015 ).done( function( json ) { 2016 2017 var votesUpCount = $J('#VotesUpCount_' + gid); 2018 if ( votesUpCount ) 2019 { 2020 var increment = 0; 2021 if ( bVoteUp ) 2022 { 2023 increment = 1; 2024 } 2025 else if ( $J('#VoteUpBtn_' + gid).hasClass( 'btn_active' ) ) 2026 { 2027 increment = -1; 2028 } 2029 UpdateFormattedNumber( votesUpCount, increment ); 2030 2031 if ( parseInt( votesUpCount.html().replace(/,/g, '') ) == 0 ) 2032 { 2033 $J('#VotesUpCountContainer_' + gid).hide(); 2034 } 2035 else 2036 {
2037 $J('#VotesUpCountContainer_' + gid).show(); 2038 } 2039 } 2040 2041 if ( bVoteUp ) 2042 { 2043 $J('#VoteUpBtn_' + gid).addClass( "btn_active" ); 2044 $J('#VoteDownBtn_' + gid).removeClass( "btn_active" ); 2045 } 2046 else 2047 { 2048 $J('#VoteDownBtn_' + gid).addClass( "btn_active" ); 2049 $J('#VoteUpBtn_' + gid).removeClass( "btn_active" ); 2050 } 2051 2052 2053 2054 } ) 2055 .fail( function( jqxhr ) { 2056 var responseJSON = jqxhr.responseText.evalJSON(); 2057 switch ( responseJSON.success ) 2058 { 2059 case 21: 2060 ShowAlertDialog( 'Error', 'You must be logged in to perform that action.' ); 2061 break; 2062 case 24: 2063 ShowAlertDialog( 'Error', 2064 'Your account does not meet the requirements to use this feature. <a class="whiteLink" href="https://help.steampowered.com/en/wizard/HelpWithLimitedAccount" target="_blank" rel="noreferrer">Visit Steam Support</a> for more information.' 2065 ); 2066 break; 2067 case 15: 2068 ShowAlertDialog( 'Error', 'You do not have permission to perform that action.' ); 2069 break; 2070 default: 2071 ShowAlertDialog( 'Error', 'There was an error encountered while processing your request: ' + responseJSON.success ); 2072 break; 2073 } 2074 } ); 2075 return false; 2076} 2077 2078function GetResponsiveHeaderFixedOffsetAdjustment() 2079{ 2080 // for responsive pages - we need to adjust for the menu 2081 var $ResponsiveHeader = $J('.responsive_header:visible'); 2082 if ( $ResponsiveHeader.length && $ResponsiveHeader.css('position') == 'fixed' ) 2083 return $ResponsiveHeader.outerHeight(); 2084 else 2085 return 0; 2086} 2087 2088/* Scroll to an element if it's not already in view. If it's at the bottom of the viewport, then it will be 2089 scrolled to the top if less than nRequiredPixelsToShow are visible (defaults to the height of the element) 2090 */ 2091function ScrollToIfNotInView( elem, nRequiredPixelsToShow, nSpacingBefore, nAnimationSpeed ) 2092{ 2093 var $Elem = $JFromIDOrElement(elem); 2094 2095 if ( typeof nSpacingBefore == 'undefined' ) 2096 nSpacingBefore = 0; 2097 2098 // for responsive pages - we need to adjust for the menu 2099 nSpacingBefore += GetResponsiveHeaderFixedOffsetAdjustment(); 2100 2101 var elemTop = $Elem.offset().top; 2102 var nViewportOffsetTop = elemTop - $J(window).scrollTop(); 2103 var bNeedToScroll = false; 2104 if ( nViewportOffsetTop < 0 ) 2105 { 2106 bNeedToScroll = true; 2107 } 2108 else 2109 { 2110 if ( !nRequiredPixelsToShow ) 2111 nRequiredPixelsToShow = $Elem.outerHeight(); 2112 2113 var nViewportOffsetBottom = nViewportOffsetTop + nRequiredPixelsToShow; 2114 2115 if ( nViewportOffsetBottom > $J(window).height() ) 2116 bNeedToScroll = true; 2117 } 2118 2119 if ( bNeedToScroll ) 2120 { 2121 if ( nSpacingBefore ) 2122 nViewportOffsetTop -= nSpacingBefore; 2123 2124 if ( typeof nAnimationSpeed != 'undefined' ) 2125 $J('html, body' ).animate( {scrollTop: nViewportOffsetTop}, nAnimationSpeed ); 2126 else 2127 window.scrollBy( 0, nViewportOffsetTop ); 2128 } 2129} 2130 2131 2132function CAjaxInfiniteScrollingControls( rgSearchData, url ) 2133{ 2134 this.m_strActionURL = null; 2135 this.m_cPageSize = null; 2136 this.m_strElementPrefix = ""; 2137 this.m_strClassPrefix = ""; 2138 this.m_StrRowsId = ""; 2139 this.m_rgStaticParams = null; 2140 2141 this.m_bLoading = false; 2142 2143 this.m_fnPreRequestHandler = null; 2144 this.m_fnResponseHandler = null; 2145 this.m_fnPageChangingHandler = null; 2146 this.m_fnPageChangedHandler = null; 2147 this.m_fnRawScrollHandler = null; 2148 2149 this.m_LoadingDialog = null; 2150 this.m_bRestoringScrollTop = false; 2151 2152 this.m_strActionURL = url; 2153 2154 this.m_strQuery = rgSearchData['query']; 2155 this.m_cTotalCount = rgSearchData['total_count']; 2156 this.m_iCurrentPage = rgSearchData['page'] || 0; 2157 this.m_cPageSize = rgSearchData['pagesize']; 2158 this.m_cMaxPages = Math.ceil( this.m_cTotalCount / this.m_cPageSize ); 2159 2160 this.m_iTriggerHeight = rgSearchData['trigger_height'] || 750; // How many px from the bottom should we trigger a page load. 2161 2162 this.m_iCooldownTime = 0; // ms from the epoch when we can next update. 2163 this.m_iCooldownInterval = 200; // Minimum time (in ms) between updates 2164 2165 if ( rgSearchData['prefix'] ) 2166 this.m_strElementPrefix = rgSearchData['prefix']; 2167 2168 // Rows element name that's updated when we scroll. 2169 this.m_StrRowsId = this.m_strElementPrefix + 'Rows'; 2170 2171 if ( rgSearchData['class_prefix'] ) 2172 this.m_strClassPrefix = rgSearchData['class_prefix']; 2173 2174 // We want our handlers to get a copy of our infiniteScroll object when they're called, so we save it 2175 // into a var and build a closure around it. 2176 var thisControl = this; 2177 2178 // By saving the bound scroll-handler to a variable, we can then unload it when our search 2179 // options are upadated. 2180 this.m_fnRawScrollHandler = function() { thisControl.OnScroll() }; 2181 $J(document).scroll( this.m_fnRawScrollHandler ); 2182 2183 // If we have a _scroll_top element, then set our OnUnload handler. 2184 if ( $J( "#" + this.m_strElementPrefix + '_scroll_top').length ) 2185 window.addEventListener('beforeunload', function() { thisControl.OnUnload() } ); 2186 2187 // Optional: Dynamic data returned from the last request, to provided to the next request. Lets them 2188 // being stringed together with some memory 2189 this.m_rgDynamicData = null; 2190 2191 this.RestoreScrollTop( true ); 2192} 2193 2194CAjaxInfiniteScrollingControls.prototype.DoneRestoreScrollTop = function() 2195{ 2196 this.ClearLoadingDialog(); 2197 this.m_bRestoringScrollTop = false;
2198}; 2199 2200CAjaxInfiniteScrollingControls.prototype.ClearLoadingDialog = function() 2201{ 2202 if ( this.m_LoadingDialog ) 2203 { 2204 this.m_LoadingDialog.Dismiss(); 2205 this.m_LoadingDialog = null; 2206 } 2207}; 2208 2209// Stops this infinite scroll handler. 2210CAjaxInfiniteScrollingControls.prototype.Stop = function() 2211{ 2212 // Clear loading notifiers. 2213 this.ClearLoadingDialog(); 2214 this.HideThrobber(); 2215 2216 // Clear scheduled scroll, if there is one. 2217 if ( this.m_oScheduledScroll ) 2218 clearTimeout( this.m_oScheduledScroll ); 2219 2220 // Unhook scroll handler. 2221 $J(document).off( "scroll", this.m_fnRawScrollHandler ); 2222 2223 // TODO: Do we need to call or clear BeforeUnload? 2224}; 2225 2226CAjaxInfiniteScrollingControls.prototype.RestoreScrollTop = function( bForce ) 2227{ 2228 this.m_bRestoringScrollTop |= bForce; 2229 if ( !this.m_bRestoringScrollTop ) 2230 { 2231 return; 2232 } 2233 var scrollTopPrevious = parseInt( $J( "#" + this.m_strElementPrefix + '_scroll_top').val() ); 2234 if ( scrollTopPrevious ) 2235 { 2236 var viewport = document.viewport.getDimensions(); // Gets the viewport as an object literal 2237 var windowHeight = viewport.height; // Usable window height 2238 var bodyHeight = $(document.body).getHeight(); 2239 2240 // done? 2241 if ( scrollTopPrevious < bodyHeight - windowHeight ) 2242 { 2243 this.DoneRestoreScrollTop(); 2244 window.scrollTo( 0, scrollTopPrevious ); 2245 } 2246 else 2247 { 2248 if ( this.m_LoadingDialog == null ) 2249 { 2250 this.m_LoadingDialog = ShowBlockingWaitDialog( 'Please Wait', 'Returning to where you were last on this page...' ); 2251 } 2252 window.scrollTo( 0, scrollTopPrevious ); 2253 } 2254 } 2255}; 2256 2257CAjaxInfiniteScrollingControls.prototype.OnUnload = function() 2258{ 2259 var scrollOffset = document.viewport.getScrollOffsets(); 2260 var scrollTop = scrollOffset.top; 2261 $J( "#" + this.m_strElementPrefix + '_scroll_top').val( scrollTop ); 2262}; 2263 2264CAjaxInfiniteScrollingControls.prototype.OnScroll = function() 2265{ 2266 if ( this.m_bLoading ) 2267 return; 2268 2269 var iNow = new Date().getTime(); 2270 2271 // How soon can we scroll? 2272 var iScrollWait = this.m_iCooldownTime - iNow; 2273 2274 // If we haven't reached our cooldown, do nothing. 2275 if ( iScrollWait > 0 ) 2276 { 2277 // console.log("InfiniScrolling too fast; engaging throttle"); 2278 2279 // Schedule a scroll event for when they would be allowed to scroll. Without this, the 2280 // user needs to jiggle their scrollbar. 2281 if ( this.m_oScheduledScroll == null ) 2282 { 2283 this.m_oScheduledScroll = setTimeout( this.m_fnRawScrollHandler, iScrollWait ); 2284 this.ShowThrobber(); // Show throbber while waiting to begin load. 2285 } 2286 2287 return; 2288 } 2289 2290 this.m_oScheduledScroll = null; 2291 2292 // The bottom of our screen is equal to how far we've scrolled, plus the height of our window. 2293 var nCurrentScroll = $J(window).scrollTop() + $J(window).height(); 2294 2295 var rows = $J('#' + this.m_StrRowsId); 2296 var offset = rows.offset(); 2297 2298 // The bottom of our content is the height of our results, plus its offset from the top of the page. 2299 // We want to trigger a load this.m_iTriggerHeight before the user sees that. 2300 var nTriggerPoint = rows.height() + offset.top - this.m_iTriggerHeight; 2301 2302 if ( nCurrentScroll > nTriggerPoint ) 2303 { 2304 this.m_iCooldownTime = iNow + this.m_iCooldownInterval; 2305 2306 this.NextPage(); 2307 } 2308}; 2309 2310CAjaxInfiniteScrollingControls.prototype.GetActionURL = function( action ) 2311{ 2312 var url = this.m_strActionURL + action + '/'; 2313 return url; 2314}; 2315 2316CAjaxInfiniteScrollingControls.prototype.SetPreRequestHandler = function( fnHandler ) 2317{ 2318 this.m_fnPreRequestHandler = fnHandler; 2319}; 2320 2321CAjaxInfiniteScrollingControls.prototype.SetResponseHandler = function( fnHandler ) 2322{ 2323 this.m_fnResponseHandler = fnHandler; 2324}; 2325 2326CAjaxInfiniteScrollingControls.prototype.SetPageChangingHandler = function ( fnHandler ) 2327{ 2328 this.m_fnPageChangingHandler = fnHandler; 2329}; 2330 2331CAjaxInfiniteScrollingControls.prototype.SetPageChangedHandler = function ( fnHandler ) 2332{ 2333 this.m_fnPageChangedHandler = fnHandler; 2334}; 2335 2336CAjaxInfiniteScrollingControls.prototype.SetStaticParameters = function ( rgParams ) 2337{ 2338 this.m_rgStaticParams = rgParams; 2339}; 2340 2341CAjaxInfiniteScrollingControls.prototype.OnAJAXComplete = function() 2342{ 2343 this.m_bLoading = false; 2344}; 2345 2346CAjaxInfiniteScrollingControls.prototype.NextPage = function() 2347{ 2348 if ( this.m_iCurrentPage < this.m_cMaxPages - 1 ) { 2349 this.LoadPage( this.m_iCurrentPage + 1 ); 2350 } 2351 else 2352 { 2353 // We're reached the end of our results! 2354 this.Stop(); 2355 } 2356 2357}; 2358 2359CAjaxInfiniteScrollingControls.prototype.LoadPage = function( iPage, bForce ) 2360{ 2361 if ( typeof( bForce )== 'undefined' || !bForce ) 2362 { 2363 if ( this.m_bLoading || iPage >= this.m_cMaxPages || iPage < 0 ) 2364 { 2365 return false;
2366 } 2367 else if ( iPage == this.m_iCurrentPage ) 2368 { 2369 this.RestoreScrollTop( false ); 2370 } 2371 } 2372 2373 var params = { 2374 query: this.m_strQuery, 2375 start: this.m_cPageSize * iPage, 2376 count: this.m_cPageSize, 2377 dynamic_data: this.m_rgDynamicData, 2378 }; 2379 2380 if ( this.m_rgStaticParams != null ) 2381 { 2382 for ( var sParamName in this.m_rgStaticParams ) 2383 { 2384 2385 if ( typeof sParamName != "string" ) 2386 continue; 2387 2388 var typeOfParam = typeof this.m_rgStaticParams[sParamName]; 2389 2390 if ( typeOfParam != "string" && typeOfParam != "number" && typeOfParam != "boolean" ) 2391 continue; 2392 2393 params[sParamName] = this.m_rgStaticParams[sParamName]; 2394 } 2395 } 2396 2397 if ( this.m_fnPageChangingHandler != null ) 2398 this.m_fnPageChangingHandler( iPage ); 2399 2400 if ( this.m_fnPreRequestHandler != null ) 2401 this.m_fnPreRequestHandler( params ); 2402 2403 this.ShowThrobber(); 2404 2405 this.m_bLoading = true; 2406 2407 new Ajax.Request( this.GetActionURL( '' ), { 2408 method: 'get', 2409 parameters: params, 2410 onSuccess: this.OnResponseRenderResults.bind( this ), 2411 onComplete: this.OnAJAXComplete.bind( this ) 2412 }); 2413 2414 return true; 2415}; 2416 2417CAjaxInfiniteScrollingControls.prototype.GetThrobber = function() { 2418 // NB: This is using prototype, *not* jQuery, hence the lack of 'J'. 2419 return $(this.m_strElementPrefix + '_loading'); 2420}; 2421 2422CAjaxInfiniteScrollingControls.prototype.HideThrobber = function() { 2423 var throbber = this.GetThrobber(); 2424 2425 if (throbber) 2426 throbber.hide(); 2427}; 2428 2429CAjaxInfiniteScrollingControls.prototype.ShowThrobber = function() { 2430 var throbber = this.GetThrobber(); 2431 2432 if (throbber) 2433 throbber.show(); 2434}; 2435 2436CAjaxInfiniteScrollingControls.prototype.OnResponseRenderResults = function( transport ) 2437{ 2438 2439 if ( transport.responseJSON && transport.responseJSON.success ) 2440 { 2441 this.HideThrobber(); 2442 2443 if ( typeof RecordAJAXPageView !== "undefined" ) 2444 { 2445 RecordAJAXPageView( transport.request.url ); 2446 } 2447 2448 var response = transport.responseJSON; 2449 this.m_cTotalCount = response.total_count; 2450 this.m_cMaxPages = Math.ceil( response.total_count / this.m_cPageSize ); 2451 this.m_iCurrentPage = Math.floor( response.start / this.m_cPageSize ); 2452 2453 if ( this.m_iCurrentPage != 0 && this.m_cTotalCount <= response.start ) 2454 { 2455 // this page is no longer valid, flip back a page (deferred so that the AJAX handler exits and reset m_bLoading) 2456 this.DoneRestoreScrollTop(); 2457 return; 2458 } 2459 2460 var elResults = $(this.m_StrRowsId); 2461 2462 elResults.insert( response.results_html ); 2463 2464 this.m_rgDynamicData = ( response.dynamic_data ) ? response.dynamic_data : null; 2465 2466 if ( this.m_fnResponseHandler != null ) 2467 { 2468 this.m_fnResponseHandler( response ); 2469 } 2470 2471 if ( this.m_fnPageChangedHandler != null ) 2472 this.m_fnPageChangedHandler( this.m_iCurrentPage, elResults ); 2473 2474 this.m_bLoading = false; 2475 if ( this.m_iCurrentPage < this.m_cMaxPages - 1 ) 2476 { 2477 this.RestoreScrollTop( false ); 2478 } 2479 else 2480 { 2481 this.DoneRestoreScrollTop(); 2482 } 2483 } 2484 else 2485 { 2486 this.DoneRestoreScrollTop(); 2487 } 2488}; 2489 2490function CAjaxPagingControls( rgSearchData, url ) 2491{ 2492 this.m_strActionURL = null; 2493 this.m_cPageSize = null; 2494 this.m_strElementPrefix = ""; 2495 this.m_strClassPrefix = ""; 2496 this.m_rgStaticParams = null; 2497 2498 this.m_strQuery = null; 2499 this.m_cTotalCount = 0; 2500 this.m_iCurrentPage = 0; 2501 this.m_cMaxPages = 0; 2502 this.m_bLoading = false; 2503 2504 this.m_fnPreRequestHandler = null; 2505 this.m_fnResponseHandler = null; 2506 this.m_fnPageChangingHandler = null; 2507 this.m_fnPageChangedHandler = null; 2508 2509 2510 this.m_strActionURL = url; 2511 2512 this.m_strQuery = rgSearchData['query']; 2513 this.m_cTotalCount = rgSearchData['total_count']; 2514 this.m_iCurrentPage = 0; 2515 this.m_cPageSize = rgSearchData['pagesize']; 2516 this.m_cMaxPages = Math.ceil( this.m_cTotalCount / this.m_cPageSize ); 2517 this.m_strDefaultAction = typeof rgSearchData['action'] != 'undefined' ? rgSearchData['action'] : 'render'; 2518 this.m_rgAvailableSizes = []; 2519 2520 if ( rgSearchData['prefix'] ) 2521 this.m_strElementPrefix = rgSearchData['prefix']; 2522 2523 if ( rgSearchData['class_prefix'] ) 2524 this.m_strClassPrefix = rgSearchData['class_prefix']; 2525 2526 $(this.m_strElementPrefix + '_btn_prev').observe( 'click', this.PrevPage.bind( this ) ); 2527 $(this.m_strElementPrefix + '_btn_next').observe( 'click', this.NextPage.bind( this ) ); 2528 var elPageSizeCtn = $(this.m_strElementPrefix + '_paging_size_ctn'); 2529 for( var i = 0; elPageSizeCtn && i < $( elPageSizeCtn ).children.length; ++i ) 2530 { 2531 $(this.m_strElementPrefix + '_paging_size_' + i ).observe( 'click', this.OnChangeSize.bind( this ) ); 2532 2533 this.m_rgAvailableSizes.push( $( elPageSizeCtn ).children[i].dataset.size ); 2534 } 2535 2536 this.UpdatePagingDisplay(); 2537} 2538 2539CAjaxPagingControls.prototype.GetActionURL = function( action ) 2540{ 2541 var url = action ? this.m_strActionURL + action + '/' : this.m_strActionURL; 2542 return url; 2543}; 2544 2545CAjaxPagingControls.prototype.SetPreRequestHandler = function( fnHandler ) 2546{ 2547 this.m_fnPreRequestHandler = fnHandler; 2548}; 2549 2550CAjaxPagingControls.prototype.SetResponseHandler = function( fnHandler ) 2551{ 2552 this.m_fnResponseHandler = fnHandler; 2553}; 2554 2555CAjaxPagingControls.prototype.SetPageChangingHandler = function ( fnHandler ) 2556{ 2557 this.m_fnPageChangingHandler = fnHandler; 2558}; 2559 2560CAjaxPagingControls.prototype.SetPageChangedHandler = function ( fnHandler ) 2561{ 2562 this.m_fnPageChangedHandler = fnHandler; 2563}; 2564 2565CAjaxPagingControls.prototype.SetStaticParameters = function ( rgParams ) 2566{ 2567 this.m_rgStaticParams = rgParams; 2568}; 2569 2570CAjaxPagingControls.prototype.OnAJAXComplete = function() 2571{ 2572 this.m_bLoading = false;
2573}; 2574 2575CAjaxPagingControls.prototype.OnChangeSize = function( event ) 2576{ 2577 if( event.target && event.target.dataset.size != this.m_cPageSize ) 2578 { 2579 this.m_cPageSize = event.target.dataset.size; 2580 this.GoToPage( 0, true ); 2581 } 2582}; 2583 2584CAjaxPagingControls.prototype.NextPage = function() 2585{ 2586 if ( this.m_iCurrentPage < this.m_cMaxPages - 1 ) 2587 this.GoToPage( this.m_iCurrentPage + 1 ); 2588}; 2589 2590CAjaxPagingControls.prototype.PrevPage = function() 2591{ 2592 if ( this.m_iCurrentPage > 0 ) 2593 this.GoToPage( this.m_iCurrentPage - 1 ); 2594}; 2595 2596CAjaxPagingControls.prototype.GoToPage = function( iPage, bForce ) 2597{ 2598 if ( typeof( bForce )== 'undefined' || !bForce ) 2599 { 2600 if ( this.m_bLoading || iPage >= this.m_cMaxPages || iPage < 0 || iPage == this.m_iCurrentPage ) 2601 return false; 2602 } 2603 2604 var params = { 2605 query: this.m_strQuery, 2606 start: this.m_cPageSize * iPage, 2607 count: this.m_cPageSize 2608 }; 2609 2610 if ( this.m_rgStaticParams != null ) 2611 { 2612 for ( var sParamName in this.m_rgStaticParams ) 2613 { 2614 if ( typeof sParamName != "string" ) 2615 continue; 2616 2617 params[sParamName] = this.m_rgStaticParams[sParamName]; 2618 } 2619 } 2620 2621 if ( this.m_fnPageChangingHandler != null ) 2622 this.m_fnPageChangingHandler( iPage ); 2623 2624 if ( this.m_fnPreRequestHandler != null ) 2625 this.m_fnPreRequestHandler( params ); 2626 2627 this.m_bLoading = true; 2628 new Ajax.Request( this.GetActionURL( this.m_strDefaultAction ), { 2629 method: 'get', 2630 parameters: params, 2631 onSuccess: this.OnResponseRenderResults.bind( this ), 2632 onComplete: this.OnAJAXComplete.bind( this ) 2633 }); 2634 2635 return true; 2636}; 2637 2638CAjaxPagingControls.prototype.OnResponseRenderResults = function( transport ) 2639{ 2640 if ( transport.responseJSON && transport.responseJSON.success ) 2641 { 2642 if ( typeof RecordAJAXPageView !== "undefined" ) 2643 { 2644 RecordAJAXPageView( transport.request.url ); 2645 } 2646 2647 var response = transport.responseJSON; 2648 this.m_cTotalCount = response.total_count; 2649 this.m_cMaxPages = Math.ceil( response.total_count / this.m_cPageSize ); 2650 this.m_iCurrentPage = Math.floor( response.start / this.m_cPageSize ); 2651 2652 if ( this.m_iCurrentPage != 0 && this.m_cTotalCount <= response.start ) 2653 { 2654 // this page is no longer valid, flip back a page (deferred so that the AJAX handler exits and reset m_bLoading) 2655 this.GoToPage.bind( this, this.m_iCurrentPage - 1 ).defer(); 2656 2657 return; 2658 } 2659 2660 var elResults = $(this.m_strElementPrefix + 'Rows'); 2661 2662 if( elResults) 2663 elResults.update( response.results_html ); 2664 2665 if ( this.m_fnResponseHandler != null ) 2666 this.m_fnResponseHandler( response ); 2667 2668 this.UpdatePagingDisplay(); 2669 2670 ScrollToIfNotInView( $(this.m_strElementPrefix + 'Table'), 40 ); 2671 } 2672}; 2673 2674CAjaxPagingControls.prototype.UpdatePagingDisplay = function() 2675{ 2676 var elemNoResults = $(this.m_strElementPrefix + '_no_results'); 2677 if ( this.m_cTotalCount == 0 ) 2678 { 2679 $(this.m_strElementPrefix + '_ctn').hide(); 2680 if ( elemNoResults ) 2681 elemNoResults.show(); 2682 } 2683 else 2684 { 2685 $(this.m_strElementPrefix + '_ctn').show(); 2686 if ( elemNoResults ) 2687 elemNoResults.hide(); 2688 2689 $(this.m_strElementPrefix + '_total').update( v_numberformat( this.m_cTotalCount ) ); 2690 $(this.m_strElementPrefix + '_start').update( v_numberformat( this.m_iCurrentPage * this.m_cPageSize + 1 ) ); 2691 $(this.m_strElementPrefix + '_end').update( Math.min( ( this.m_iCurrentPage + 1 ) * this.m_cPageSize, this.m_cTotalCount ) ); 2692 } 2693 2694 if( this.m_rgAvailableSizes && this.m_rgAvailableSizes.length > 0 ) 2695 { 2696 if( this.m_cTotalCount <= this.m_rgAvailableSizes[0] ) 2697 { 2698 $(this.m_strElementPrefix + '_per_page_ctn').hide(); 2699 } 2700 else 2701 { 2702 $(this.m_strElementPrefix + '_per_page_ctn').show(); 2703 } 2704 } 2705 2706 if ( this.m_cMaxPages <= 1 ) 2707 { 2708 $(this.m_strElementPrefix + '_controls').hide(); 2709 } 2710 else 2711 { 2712 $(this.m_strElementPrefix + '_controls').show(); 2713 if ( this.m_iCurrentPage > 0 ) 2714 $(this.m_strElementPrefix + '_btn_prev').removeClassName('disabled'); 2715 else 2716 $(this.m_strElementPrefix + '_btn_prev').addClassName('disabled'); 2717 2718 if ( this.m_iCurrentPage < this.m_cMaxPages - 1 ) 2719 $(this.m_strElementPrefix + '_btn_next').removeClassName('disabled'); 2720 else 2721 $(this.m_strElementPrefix + '_btn_next').addClassName('disabled'); 2722 2723 var elPageLinks = $(this.m_strElementPrefix + '_links'); 2724 elPageLinks.update(''); 2725 // we always show first, last, + 3 page links closest to current page 2726 var cPageLinksAheadBehind = 2; 2727 var firstPageLink = Math.max( this.m_iCurrentPage - cPageLinksAheadBehind, 1 ); 2728 var lastPageLink = Math.min( this.m_iCurrentPage + (cPageLinksAheadBehind*2) + ( firstPageLink - this.m_iCurrentPage ), this.m_cMaxPages - 2 ); 2729 2730 if ( lastPageLink - this.m_iCurrentPage < cPageLinksAheadBehind ) 2731 firstPageLink = Math.max( this.m_iCurrentPage - (cPageLinksAheadBehind*2) + ( lastPageLink - this.m_iCurrentPage ), 1 ); 2732 2733 this.AddPageLink( elPageLinks, 0 ); 2734 if ( firstPageLink != 1 ) 2735 elPageLinks.insert( ' ... ' ); 2736 2737 for ( var iPage = firstPageLink; iPage <= lastPageLink; iPage++ ) 2738 { 2739 this.AddPageLink( elPageLinks, iPage ); 2740 } 2741 2742 if ( lastPageLink != this.m_cMaxPages - 2 ) 2743 elPageLinks.insert( ' ... ' ); 2744 this.AddPageLink( elPageLinks, this.m_cMaxPages - 1 ); 2745 } 2746 2747 if( this.m_rgAvailableSizes.indexOf( this.m_cPageSize ) > -1 ) 2748 { 2749 for( var i = 0; i < this.m_rgAvailableSizes.length; ++ i ) 2750 { 2751 var elSizeItem = $(this.m_strElementPrefix + '_paging_size_' + i ); 2752 if( elSizeItem.dataset.size == this.m_cPageSize ) 2753 { 2754 elSizeItem.addClassName( 'size_selected' ); 2755 } 2756 else 2757 {
2758 elSizeItem.removeClassName( 'size_selected' ); 2759 } 2760 } 2761 } 2762 2763 if ( this.m_fnPageChangedHandler != null ) 2764 this.m_fnPageChangedHandler( this.m_iCurrentPage ); 2765}; 2766 2767CAjaxPagingControls.prototype.AddPageLink = function( elPageLinks, iPage ) 2768{ 2769 var el = new Element( 'span', {'class': ( this.m_strClassPrefix != "" ? this.m_strClassPrefix : this.m_strElementPrefix ) + '_paging_pagelink' } ); 2770 el.update( (iPage + 1) + ' ' ); 2771 2772 if ( iPage == this.m_iCurrentPage ) 2773 el.addClassName( 'active' ); 2774 else 2775 el.observe( 'click', this.GoToPage.bind( this, iPage ) ); 2776 2777 elPageLinks.insert( el ); 2778}; 2779 2780 2781function CSlider( $Container, $Grabber, args ) 2782{ 2783 this.m_$Container = $Container; 2784 this.m_$Grabber = $Grabber || $Container.find('.handle'); 2785 this.m_nMinVal = args.min || 0; 2786 this.m_nMaxVal = args.max || 100; 2787 this.m_nIncrement = args.increment || 1; 2788 this.m_nValue = args.value || 0; 2789 this.m_fnOnChange = args.fnOnChange || function( value, bInDrag ) {}; 2790 2791 this.m_$Grabber.css( 'position', 'absolute' ); 2792 this.CalcRatios(); 2793 this.SetValue( this.m_nValue ); 2794 2795 var fnGetPageX = function( event ) 2796 { 2797 if ( event.type.indexOf( 'touch' ) == 0 ) 2798 { 2799 var TouchEvent = event.originalEvent; 2800 var rgTouches = TouchEvent ? TouchEvent.touches : null; 2801 if ( !rgTouches || rgTouches.length < 1 ) 2802 return event.pageX || 0; //probably wrong 2803 return rgTouches[0].pageX || 0; 2804 } 2805 else 2806 { 2807 return event.pageX || 0; 2808 } 2809 }; 2810 2811 var _this = this; 2812 this.m_$Container.on( 'mousedown touchstart', function( event ) { 2813 _this.CalcRatios(); 2814 2815 if ( !_this.m_$Grabber.is( event.target ) ) 2816 { 2817 // jump the grabber to this position and start the drag 2818 var nPosition = fnGetPageX( event ) - _this.m_$Container.offset().left; 2819 // we want the grabber centered under the mosue if possible 2820 nPosition -= Math.floor( _this.m_$Grabber.width() / 2 ); 2821 var nNewPosition = Math.min( Math.max( nPosition, 0 ), _this.m_nWidth ); 2822 2823 _this.m_$Grabber.css('left', nNewPosition + 'px' ); 2824 _this.m_nValue = nNewPosition / _this.m_flRatio; 2825 if ( _this.m_nIncrement > 1 && _this.m_nValue < _this.m_nMaxVal ) { 2826 //_this.m_nValue += _this.m_nIncrement - 1; 2827 _this.m_nValue = parseInt(_this.m_nValue / _this.m_nIncrement) * _this.m_nIncrement; 2828 } 2829 2830 _this.m_fnOnChange( _this.m_nValue, true ); 2831 } 2832 var nInitialPosition = parseInt( _this.m_$Grabber.css('left') ); 2833 var nStartDragX = fnGetPageX( event ); 2834 2835 $J(document).on( 'mousemove.CSlider touchmove.CSlider', function( event ) { 2836 var nDelta = fnGetPageX( event ) - nStartDragX; 2837 2838 var nNewPosition = Math.min( Math.max( nInitialPosition + nDelta, 0 ), _this.m_nWidth ); 2839 2840 _this.m_$Grabber.css('left', nNewPosition + 'px' ); 2841 _this.m_nValue = nNewPosition / _this.m_flRatio; 2842 if ( _this.m_nIncrement > 1 && _this.m_nValue < _this.m_nMaxVal ) { 2843 //_this.m_nValue += _this.m_nIncrement - 1; 2844 _this.m_nValue = parseInt(_this.m_nValue / _this.m_nIncrement) * _this.m_nIncrement; 2845 } 2846 2847 _this.m_fnOnChange( _this.m_nValue, true ); 2848 }); 2849 $J(document).on( 'mouseup.CSlider touchend.CSlider', function( event ) { 2850 $J(document).off('.CSlider'); 2851 _this.m_fnOnChange( _this.m_nValue, false ); 2852 }); 2853 2854 event.preventDefault(); 2855 }); 2856} 2857 2858CSlider.prototype.CalcRatios = function() 2859{ 2860 var nGrabberWidth = this.m_$Grabber.width(); 2861 this.m_nWidth = this.m_$Container.width() - nGrabberWidth; 2862 2863 this.m_flRatio = this.m_nWidth / ( this.m_nMaxVal - this.m_nMinVal ); 2864}; 2865 2866CSlider.prototype.SetValue = function( nValue, nAnimationSpeed ) 2867{ 2868 this.m_nValue = Math.min( Math.max( nValue, this.m_nMinVal ), this.m_nMaxVal ); 2869 2870 var nNewPosition = Math.floor( ( this.m_nValue - this.m_nMinVal ) * this.m_flRatio ); 2871 2872 this.m_$Grabber.stop(); 2873 if ( nAnimationSpeed ) 2874 this.m_$Grabber.animate( {left: nNewPosition }, nAnimationSpeed ); 2875 else 2876 this.m_$Grabber.css( 'left', nNewPosition + 'px' ); 2877}; 2878 2879CSlider.prototype.GetValue = function() 2880{ 2881 return this.m_nValue; 2882}; 2883 2884CSlider.prototype.GetMin = function() 2885{ 2886 return this.m_nMinVal; 2887}; 2888 2889CSlider.prototype.GetMax = function() 2890{ 2891 return this.m_nMaxVal; 2892}; 2893 2894CSlider.prototype.SetRange = function( nMinVal, nMaxVal, nValue ) 2895{ 2896 this.m_nMinVal = nMinVal; 2897 this.m_nMaxVal = nMaxVal; 2898 if ( typeof nValue != 'undefined' ) 2899 this.m_nValue = nValue; 2900 this.CalcRatios(); 2901 this.SetValue( this.m_nValue ); 2902}; 2903 2904CSlider.prototype.SetIncrement = function( nIncrement ) 2905{ 2906 this.m_nIncrement = nIncrement; 2907}; 2908 2909function CScrollSlider( $Scroll, $Container, $Grabber, args ) 2910{ 2911 this.m_$Scroll = $Scroll; 2912 this.m_$SliderCtn = $Container; 2913 2914 var $Slider = $Container.children('.slider'); 2915 2916 this.m_Slider = new CSlider( $Slider.length ? $Slider : $Container, $Grabber, { fnOnChange: $J.proxy( this.OnSliderChange, this )} ); 2917 2918 this.m_$Scroll.css('overflowX', 'scroll'); 2919 // add momentum on iOS 2920 this.m_$Scroll.css('-webkit-overflow-scrolling', 'touch'); 2921 2922 var _this = this; 2923 var bDidInitialRecalc = false;
2924 this.m_$Scroll.on( 'scroll.ScrollSlider', function() { 2925 if ( !bDidInitialRecalc ) 2926 { 2927 // we don't want to do this all the time, but on some browsers the first call to 2928 // update ranges is too early and values don't compute correctly. 2929 _this.UpdateRanges(); 2930 bDidInitialRecalc = true; 2931 } 2932 _this.m_Slider.SetValue( _this.m_$Scroll.scrollLeft() ); 2933 }); 2934 $J(window).on('resize.ScrollSlider', function() { 2935 _this.UpdateRanges(); 2936 } ); 2937 this.m_$Scroll.on( 'v_contentschanged', function() { 2938 _this.UpdateRanges(); 2939 } ); 2940 2941 this.UpdateRanges(); 2942} 2943 2944CScrollSlider.prototype.SetValue = function( value, nAnimationSpeed ) { 2945 if ( nAnimationSpeed ) 2946 { 2947 this.m_$Scroll.stop().animate( {'scrollLeft': value }, nAnimationSpeed ); 2948 } 2949 else 2950 { 2951 this.m_$Scroll.stop().scrollLeft( value ); 2952 } 2953}; 2954 2955CScrollSlider.prototype.GetValue = function() { 2956 return this.m_Slider.GetValue(); 2957}; 2958 2959CScrollSlider.prototype.UpdateRanges = function() 2960{ 2961 var nParentWidth = this.m_$Scroll.width(); 2962 var nScrollWidth = this.m_$Scroll[0].scrollWidth; 2963 2964 if ( nScrollWidth <= nParentWidth ) 2965 { 2966 this.m_$SliderCtn.hide(); 2967 } 2968 else 2969 { 2970 this.m_Slider.SetRange( 0, nScrollWidth - nParentWidth, this.m_$Scroll.scrollLeft() ); 2971 this.m_$SliderCtn.show(); 2972 } 2973}; 2974 2975CScrollSlider.prototype.OnSliderChange = function( value, bInDrag ) 2976{ 2977 this.m_$Scroll.stop().scrollLeft( value ); 2978}; 2979 2980function IsValidEmailAddress( email ) 2981{ 2982 var email_regex = /^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i; 2983 return ( email != '' && email_regex.test(email) ); 2984} 2985 2986 2987(function ($) { 2988 $.deparam = function (params, coerce) { 2989 var obj = {}, 2990 coerce_types = { 'true': !0, 'false': !1, 'null': null }; 2991 2992 $.each(params.replace(/\+/g, ' ').split('&'), function (j,v) { 2993 var param = v.split('='), 2994 key = decodeURIComponent(param[0]), 2995 val, 2996 cur = obj, 2997 i = 0, 2998 2999 keys = key.split(']['), 3000 keys_last = keys.length - 1; 3001 3002 if (/\[/.test(keys[0]) && /\]$/.test(keys[keys_last])) { 3003 keys[keys_last] = keys[keys_last].replace(/\]$/, ''); 3004 3005 keys = keys.shift().split('[').concat(keys); 3006 3007 keys_last = keys.length - 1; 3008 } else { 3009 keys_last = 0; 3010 } 3011 3012 if (param.length === 2) { 3013 val = decodeURIComponent(param[1]); 3014 3015 if (coerce) { 3016 val = val && !isNaN(val) ? +val // number 3017 : val === 'undefined' ? undefined // undefined 3018 : coerce_types[val] !== undefined ? coerce_types[val] // true, false, null 3019 : val; // string 3020 } 3021 3022 if ( keys_last ) { 3023 for (; i <= keys_last; i++) { 3024 key = keys[i] === '' ? cur.length : keys[i]; 3025 3026 if ( typeof cur != 'object' ) 3027 break; 3028 3029 cur = cur[key] = i < keys_last 3030 ? cur[key] || (keys[i+1] && isNaN(keys[i+1]) ? {} : []) 3031 : val; 3032 } 3033 3034 } else { 3035 3036 if ($.isArray(obj[key])) { 3037 obj[key].push( val ); 3038 3039 } else if (obj[key] !== undefined) { 3040 obj[key] = [obj[key], val]; 3041 3042 } else { 3043 obj[key] = val; 3044 } 3045 } 3046 3047 } else if (key) { 3048 obj[key] = coerce 3049 ? undefined 3050 : ''; 3051 } 3052 }); 3053 3054 return obj; 3055 }; 3056})(jQuery); 3057 3058/** 3059 * Generic search field that handles: 3060 * 1.) Showing default text if the input field is empty 3061 * 2.) When the input field gets focus, the text field clears 3062 * 3.) Adding CSS class to the input field when it is default text 3063 * 4.) When the user presses return/enter in the field 3064 * 3065 * Call ClearIfDefaultValue() before submitting the form 3066 */ 3067function SearchFieldWithText( elemID, defaultSearchText, onEnterFunc, defaultTextCSSClass ) 3068{ 3069 var elem = $( elemID ); 3070 3071 this.elem = elem; 3072 this.defaultSearchText = defaultSearchText; 3073 this.defaultTextCSSClass = defaultTextCSSClass;
3074 this.onEnterFunc = onEnterFunc; 3075 3076 Event.observe( elem, 'click', this.handleClickOrFocus.bind(this)); 3077 Event.observe( elem, 'focus', this.handleClickOrFocus.bind(this)); 3078 Event.observe( elem, 'blur', this.handleBlur.bind(this)); 3079 Event.observe( elem, 'keypress', this.handleKeypress.bind(this)); 3080 Event.observe( elem.form, 'submit', this.ClearIfDefaultValue.bind(this)); 3081 3082 this.handleBlur(); 3083} 3084 3085SearchFieldWithText.prototype.handleClickOrFocus = function() 3086{ 3087 if ( this.elem.value == this.defaultSearchText ) 3088 { 3089 this.elem.value = ''; 3090 if ( this.defaultTextCSSClass ) 3091 this.elem.removeClassName( this.defaultTextCSSClass ); 3092 } 3093}; 3094 3095SearchFieldWithText.prototype.handleBlur = function() 3096{ 3097 if ( this.elem.value == '') 3098 { 3099 this.elem.value = this.defaultSearchText; 3100 if ( this.defaultTextCSSClass ) 3101 this.elem.addClassName( this.defaultTextCSSClass ); 3102 } 3103}; 3104 3105SearchFieldWithText.prototype.handleKeypress = function() 3106{ 3107 if ( !this.onEnterFunc ) 3108 return; 3109 3110 var keyCode = null; 3111 if( event.which ) 3112 { 3113 keyCode = event.which; 3114 } 3115 else if( event.keyCode ) 3116 { 3117 keyCode = evt.keyCode; 3118 } 3119 if ( 13 == keyCode ) 3120 { 3121 this.onEnterFunc(); 3122 } 3123}; 3124 3125SearchFieldWithText.prototype.ClearIfDefaultValue = function() 3126{ 3127 if ( this.elem.value == this.defaultSearchText ) 3128 { 3129 this.elem.value = ''; 3130 } 3131}; 3132 3133 3134function CWebAPI( strWebAPIHost, strSecureWebAPIHost, strOAuth2Token ) 3135{ 3136 this.m_strHost = strWebAPIHost; 3137 this.m_strSecureWebAPIHost = strSecureWebAPIHost; 3138 this.m_strOAuth2Token = strOAuth2Token; 3139} 3140 3141CWebAPI.prototype.BuildURL = function( strInterface, strMethod, bSecure, strVersion ) 3142{ 3143 if ( !strVersion ) 3144 strVersion = 'v0001'; 3145 3146 var strURL = ( bSecure ? this.m_strSecureWebAPIHost : this.m_strHost ); 3147 strURL += strInterface + '/' + strMethod + '/' + strVersion + '/'; 3148 3149 return strURL; 3150}; 3151 3152CWebAPI.prototype.ExecJSONP = function( strInterface, strMethod, rgParams, bSecure, strVersion, cTimeoutSecs ) 3153{ 3154 rgParams.access_token = this.m_strOAuth2Token; 3155 3156 var rgJQueryParams = { 3157 url: this.BuildURL( strInterface, strMethod, bSecure, strVersion ), 3158 dataType: 'jsonp', 3159 jsonp: 'jsonp', data: rgParams 3160 }; 3161 3162 if ( cTimeoutSecs ) 3163 rgJQueryParams['timeout'] = cTimeoutSecs * 1000; 3164 3165 return $J.ajax( rgJQueryParams ); 3166 3167 /* 3168 // using jsonp plugin instead of built-in jquery jsonp handling. this library supposedly 3169 // works around the firefox "waiting for host..." issue, but it doesn't work. 3170 return $J.jsonp({ 3171 url: this.BuildURL( strInterface, strMethod, bSecure, strVersion ), 3172 callbackParameter: 'jsonp', data: rgParams 3173 }); 3174 */ 3175}; 3176 3177// Send a "beacon", which is specifically intended for use in OnUnload events (as outsdanding AJAX/JSONP requests may be cancelled) 3178// as of sept 2015, only chrome and firefox support this, no iOS or IE support. Check for support before calling. 3179// Beacons are (per spec) always a POST request, and always include CORS headers. WebAPI respondes properly to CORS for Valve domains. 3180CWebAPI.prototype.ExecBeacon = function( strInterface, strMethod, rgParams, bSecure, strVersion ) 3181{ 3182 rgParams.access_token = this.m_strOAuth2Token; 3183 3184 var fdParams = new FormData(); 3185 for ( var key in rgParams ) 3186 fdParams.append( key, rgParams[key] ); 3187 3188 navigator.sendBeacon( this.BuildURL( strInterface, strMethod, bSecure, strVersion ), fdParams ); 3189}; 3190 3191 3192CWebAPI.prototype.ExecPOST = function( strInterface, strMethod, rgParams, bSecure, strVersion ) 3193{ 3194 rgParams.access_token = this.m_strOAuth2Token; 3195 rgParams.format = 'json'; 3196 3197 return $J.ajax( { 3198 url: this.BuildURL( strInterface, strMethod, bSecure, strVersion ), 3199 type: 'POST', 3200 data: rgParams 3201 }); 3202}; 3203 3204// register some events to dismiss popup (ie, user clicking elsewhere on the window, escape) 3205// cleans up event binds afterwards. clicks to children of "elemIgnore" will not dismiss popup 3206function RegisterPopupDismissal( dismissFunc, elemIgnore ) 3207{ 3208 var $Ignore = $JFromIDOrElement( elemIgnore ); 3209 // delay registration by one frame so that we don't catch the event that triggered this popup. 3210 window.setTimeout( function() { 3211 $J(document).on('click.RegisterPopupDismissal keydown.RegisterPopupDismissal', function (event) { 3212 3213 if (event.keyCode && event.keyCode != 27 /* KEY_ESC */) { 3214 return; 3215 } 3216 var elem = $J(event.target); 3217 3218 if ( $Ignore.length && $J.contains( $Ignore[0], elem[0] ) ) 3219 return; 3220 3221 dismissFunc(); 3222 UnregisterPopupDismissal( elemIgnore ); 3223 }); 3224 3225 // support gamepad B button to dismiss 3226 $Ignore.on( 'vgp_oncancel', function ( event ) { 3227 dismissFunc(); 3228 UnregisterPopupDismissal( elemIgnore ); 3229 3230 event.stopPropagation(); 3231 event.preventDefault(); 3232 }); 3233 }, 1 ); 3234} 3235 3236/* Cleanup function for the above RegisterPopupDismissal() 3237 This needs to be accessible to popup owners so they can call this if they're closing the popup */ 3238function UnregisterPopupDismissal( elemIgnore ) 3239{ 3240 $J(document).off( '.RegisterPopupDismissal' ); 3241 3242 if ( elemIgnore ) 3243 { 3244 // unregister gamepad B button press handling 3245 $JFromIDOrElement( elemIgnore ).off( 'vgp_oncancel' ); 3246 } 3247} 3248 3249 3250function ShowMenu( elemLink, elemPopup, align, valign, bLinkHasBorder ) 3251{ 3252 var $Popup = $JFromIDOrElement(elemPopup); 3253 3254 // If we're in tablet screen mode put the menu content in a modal dialog 3255 var bUseTabletScreenMode = window.UseTabletScreenMode && window.UseTabletScreenMode(); 3256 if ( bUseTabletScreenMode ) 3257 { 3258 // detach this element and when the dialog closes re-attach to the document body 3259 $Popup.detach(); 3260 var originalPopupPosition = $Popup.css( 'position' ); 3261 $Popup.css( 'position', 'static' ); // clear possible absolute positioning 3262 $Popup.show(); 3263 3264 ShowDialog( '', $Popup ).always( 3265 function() { 3266 // save it away again for later 3267 $Popup.hide(); 3268 $Popup.css( 'position', originalPopupPosition ); // restore positioning 3269 $J( document.body ).append( $Popup ); 3270 } 3271 ); 3272 3273 return; 3274 } 3275 3276 var $Link = $JFromIDOrElement(elemLink); 3277 if ( $Link.hasClass('focus') ) 3278 { 3279 HideMenu( elemLink, elemPopup ); 3280 return; 3281 } 3282 3283 AlignMenu( $Link, $Popup, align, valign, bLinkHasBorder ); 3284 3285 ShowWithFade( $Popup ); 3286 $Link.addClass('focus'); 3287 RegisterPopupDismissal( function() { HideMenu( elemLink, elemPopup ); }, $Popup ); 3288 3289 // If we use this control on gamepad (haven't found an example yet we're going to keep) then we'll need to 3290 // add calling: GPOnShowingModalWindow( $Popup[0] ). See shared_responsive_adapter.js for example 3291} 3292 3293function HideMenu( elemLink, elemPopup ) 3294{ 3295 var $Link = $JFromIDOrElement(elemLink); 3296 var $Popup = $JFromIDOrElement(elemPopup); 3297 3298 $Link.data( 'menu-active', false ); 3299 HideWithFade( $Popup ); 3300 $Link.removeClass( 'focus' ); 3301 3302 UnregisterPopupDismissal( elemPopup ); 3303} 3304 3305function HideMenuFast( elemLink, elemPopup ) 3306{ 3307 var $Link = $JFromIDOrElement(elemLink); 3308 var $Popup = $JFromIDOrElement(elemPopup); 3309 3310 $Popup.hide(); 3311 $Link.removeClass( 'focus' ); 3312 3313 UnregisterPopupDismissal( elemPopup ); 3314} 3315 3316 3317function RegisterFlyout( elemLink, elemPopup, align, valign, bLinkHasBorder ) 3318{ 3319 var $Link = $JFromIDOrElement( elemLink ); 3320 var $Popup = $JFromIDOrElement( elemPopup ); 3321 3322 $Link.on( 'mouseenter', function( event ) { 3323 FlyoutMenu( $Link, $Popup, align, valign, bLinkHasBorder ); 3324 }); 3325 3326 $Link.add( $Popup ).on( 'mouseleave', function( event ) { 3327 HideFlyoutMenu( event, $Link, $Popup ); 3328 }); 3329} 3330 3331function UseSmallScreenMenu() 3332{ 3333 // the new mobile app uses the small screen menu regardless of screen size 3334 return ( window.UseSmallScreenMode && window.UseSmallScreenMode() ) || ( window.UseNewMobileAppMode && window.UseNewMobileAppMode() ); 3335} 3336 3337function FlyoutMenu( elemLink, elemPopup, align, valign, bLinkHasBorder, elemAlternateAlignTo ) 3338{ 3339 var $Link = $JFromIDOrElement(elemLink); 3340 var $Popup = $JFromIDOrElement(elemPopup); 3341 3342 if ( !$Popup.is(':visible') || $Popup.css('opacity') < 1.0 ) 3343 { 3344 if ( $Popup.hasClass( 'responsive_slidedown') && UseSmallScreenMenu() ) 3345 { 3346 $Popup.stop().slideDown(); 3347 } 3348 else 3349 { 3350 // Responsive mode calls slideDown, which applies position: relative to the element and we need to clear it. 3351 // This will only happen if the user interacted with the menu in the responsive mode, then resized 3352 // their window to be large and hovered the elements. 3353 $Popup.css( { position: '' } ); 3354 AlignMenu( $Link, $Popup, align, valign, bLinkHasBorder, elemAlternateAlignTo || null ); 3355 ShowWithFade( $Popup ); 3356 } 3357 3358 $Link.addClass('focus'); 3359 } 3360 3361} 3362 3363function HideFlyoutMenu( event, elemLink, elemPopup ) 3364{ 3365 var $Link = $JFromIDOrElement(elemLink); 3366 var $Popup = $JFromIDOrElement(elemPopup); 3367 3368 if ( !$Link.hasClass('focus') ) 3369 return; 3370 3371 if ( event ) 3372 { 3373 var reltarget = $J( event.relatedTarget ); 3374 if ( !reltarget.length || 3375 ( $Link.length && $J.contains( $Link[0], reltarget[0] ) ) || 3376 ( $Popup.length && $J.contains( $Popup[0], reltarget[0] ) ) || 3377 $Link.is( reltarget ) ) 3378 return; 3379 } 3380 // start hiding in a little bit, have to let the fade in animation start before we can cancel it 3381 3382 if ( $Popup.hasClass( 'responsive_slidedown') && UseSmallScreenMenu() ) 3383 $Popup.stop().slideUp(); 3384 else 3385 window.setTimeout( function() { HideWithFade( $Popup ); }, 33 ); 3386 3387 $Link.removeClass('focus'); 3388 $J(document).off('.FlyoutDismiss'); 3389} 3390 3391function AlignMenu( elemLink, elemPopup, align, valign, bLinkHasBorder, elemAlternateAlignTo ) 3392{ 3393 // alternate align to is used to align the genre menu on the store; it's only used for horizontal alignment, 3394 // trying to align to it vertically messes up responsive view. 3395 var $LinkVertical = $JFromIDOrElement(elemLink); 3396 var $LinkHorizontal = elemAlternateAlignTo ? $JFromIDOrElement(elemAlternateAlignTo) : $LinkVertical; 3397 3398 var align = align ? align : 'left'; 3399 // flip alignments for RTL 3400 if ( $LinkVertical?.[0]?.matches( ':dir(rtl)' ) ?? document.dir === "rtl" ) 3401 { 3402 if ( align === 'left' ) 3403 { 3404 align = 'right'; 3405 } 3406 else if ( align === 'right' ) 3407 { 3408 align = 'left'; 3409 } 3410 else if ( align === 'leftsubmenu' ) 3411 { 3412 align = 'rightsubmenu'; 3413 } 3414 else if ( align === 'rightsubmenu' ) 3415 { 3416 align = 'leftsubmenu'; 3417 } 3418 } 3419 3420 var $Popup = $JFromIDOrElement(elemPopup); 3421 3422 3423 var offsetLinkVertical = $LinkVertical.offset(); 3424 var offsetLinkHorizontal = $LinkHorizontal.offset(); 3425 var nWindowScrollTop = $J(window).scrollTop();
3426 var nViewportHeight = $J(window).height(); 3427 3428 var nLinkViewportTop = offsetLinkVertical.top - nWindowScrollTop; 3429 3430 // add a little bit of padding so we don't position it flush to an edge if possible 3431 var nPopupHeight = $Popup.height() + 8; 3432 3433 if ( !valign ) 3434 { 3435 //if there's not enough room between our spot and the top of the document, we definitely want to drop down 3436 if ( nWindowScrollTop + offsetLinkVertical.top < nPopupHeight ) 3437 { 3438 valign = 'bottom'; 3439 } 3440 else 3441 { 3442 var nSpaceAbove = nLinkViewportTop; 3443 var nSpaceBelow = nViewportHeight - ( nLinkViewportTop + $LinkVertical.height() ); 3444 //otherwise we only want to drop down if we've got enough space below us (measured based on view area) 3445 // or if there's not enough space above to pop in either direction and there's more space below 3446 if ( nSpaceBelow > nPopupHeight || ( nSpaceAbove < nPopupHeight && nSpaceBelow > nSpaceAbove ) ) 3447 valign = 'bottom'; 3448 else 3449 valign = 'top'; 3450 3451 } 3452 } 3453 3454 var borderpx = bLinkHasBorder ? 1 : 0; 3455 var shadowpx = $Popup.hasClass( 'popup_block_new' ) ? 0 : 12; 3456 var offsetLeft = 0; 3457 3458 if ( align == 'left' ) 3459 { 3460 //elemPopup.style.left = ( elemLink.positionedOffset()[0] - 12 ) + 'px'; 3461 offsetLeft = -shadowpx - borderpx; 3462 } 3463 else if ( align == 'right' ) 3464 { 3465 //elemPopup.style.left = ( elemLink.positionedOffset()[0] + elemLink.getWidth() - elemPopup.getWidth() + 13 ) + 'px'; 3466 offsetLeft = $LinkHorizontal.outerWidth() - $Popup.outerWidth() + shadowpx + borderpx; 3467 } 3468 else if ( align == 'leftsubmenu' ) 3469 { 3470 //elemPopup.style.left = ( elemLink.positionedOffset()[0] - elemPopup.getWidth() + 12 ) + 'px'; 3471 offsetLeft = -$Popup.outerWidth() + shadowpx - borderpx; 3472 } 3473 else if ( align == 'rightsubmenu' ) 3474 { 3475 //elemPopup.style.left = ( elemLink.positionedOffset()[0] + elemLink.getWidth() - 12 ) + 'px'; 3476 offsetLeft = $LinkHorizontal.outerWidth() - shadowpx + 2 * borderpx; 3477 } 3478 3479 var offsetTop = 0; 3480 if ( valign == 'bottom' ) 3481 { 3482 //elemPopup.style.top = ( elemLink.positionedOffset()[1] + elemLink.getHeight() - 12 ) + 'px'; 3483 offsetTop = $LinkVertical.outerHeight() - shadowpx; 3484 } 3485 else if ( valign == 'top' ) 3486 { 3487 //elemPopup.style.top = ( elemLink.positionedOffset()[1] - elemPopup.getHeight() + 12 ) + 'px'; 3488 offsetTop = -$Popup.outerHeight() + shadowpx; 3489 } 3490 else if ( valign == 'bottomsubmenu' ) 3491 { 3492 //elemPopup.style.top = ( elemLink.positionedOffset()[1] - 12 ) + 'px'; 3493 offsetTop = -shadowpx; 3494 } 3495 3496 3497 var bPopupHidden = !$Popup.is(':visible'); 3498 3499 if ( bPopupHidden ) 3500 { 3501 // IE can't do this with display: none elements 3502 $Popup.css( 'visibility', 'hidden' ); 3503 $Popup.show(); 3504 } 3505 3506 $Popup.offset( { 3507 top: Math.max( offsetLinkVertical.top + offsetTop, 0 ), 3508 left: Math.max( offsetLinkHorizontal.left + offsetLeft, 0 ) 3509 }); 3510 3511 if ( bPopupHidden ) 3512 { 3513 // restore visibility 3514 $Popup.hide(); 3515 $Popup.css( 'visibility', 'visible' ); 3516 } 3517} 3518 3519function BindAutoFlyoutEvents() 3520{ 3521 var fnShowFlyout = function( $Tab, bIsHover, bTakeFocus ) 3522 { 3523 var $Content = $J('#' + $Tab.data('flyout') ); 3524 var bResponsiveSlidedownMenu = UseSmallScreenMenu() && $Content.hasClass('responsive_slidedown'); 3525 3526 if ( !$Content.length || $Content.data('flyout-event-running') || 3527 (bIsHover && bResponsiveSlidedownMenu ) ) 3528 return; 3529 3530 $Content.data( 'flyout-event-running', true ); 3531 window.setTimeout( function() { $Content.data('flyout-event-running', false ); }, 1 ); 3532 3533 if ( $Content.is(':visible') ) 3534 { 3535 if ( !bIsHover ) 3536 HideFlyoutMenu( null, $Tab, $Content ); 3537 3538 return; 3539 } 3540 3541 if ( !$Content.data('flyout-events-bound') ) 3542 { 3543 $Content.on('mouseleave.Flyout', function( e ) { 3544 if ( UseSmallScreenMenu() && $Content.hasClass('responsive_slidedown') ) 3545 return; 3546 3547 if ( $Tab.is( e.relatedTarget ) || $J.contains( $Tab[0], e.relatedTarget ) ) 3548 return; 3549 3550 HideFlyoutMenu( null, $Tab, $Content ); 3551 }); 3552 3553 $Content.add($Tab).on('vgp_oncancel', function( e ) { 3554 HideFlyoutMenu( null, $Tab, $Content ); 3555 if ( typeof GPNavFocusChild != 'undefined' ) 3556 GPNavFocusChild( $Tab ); 3557 }); 3558 3559 $Content.on('vgp_onblur', function( e ) { 3560 var node = e.originalEvent.detail.focusedNode; 3561 console.log( node ); 3562 if ( !node || !$J.contains( e.currentTarget, node.Element ) ) 3563 HideFlyoutMenu( null, $Tab, $Content ); 3564 }); 3565
3566 $Content.data('flyout-events-bound', true ); 3567 } 3568 3569 3570 FlyoutMenu( $Tab, $Content, $Tab.data('flyout-align'), $Tab.data('flyout-valign'), false, $Tab.data('flyout-align-to-element' ) ); 3571 3572 if ( bTakeFocus && typeof GPNavFocusChild !== 'undefined' ) 3573 { 3574 window.setTimeout( function() { 3575 console.log( 'Request focus in', $Content[0] ); 3576 GPNavFocusChild( $Content ); 3577 }, 5 ); 3578 } 3579 3580 if ( window.UseTouchFriendlyMode && window.UseTouchFriendlyMode() ) 3581 { 3582 window.setTimeout( function() { 3583 $J(document).on('click.FlyoutDismiss', function(e) { 3584 if ( $J.contains( $Content[0], e.target ) || $Content.is( e.target ) ) 3585 return; 3586 3587 HideFlyoutMenu( null, $Tab, $Content ); 3588 e.preventDefault(); 3589 }); 3590 }, 1 ); 3591 } 3592 }; 3593 3594 $J(document).on( 'mouseenter.Flyout click.Flyout', '.flyout_tab', function(e) { 3595 var $Tab = $J(this); 3596 var msDelay = $Tab.data('flyout-delay'); 3597 3598 var bIsHover = e.type == 'mouseenter'; 3599 3600 if ( !bIsHover || !msDelay ) 3601 { 3602 fnShowFlyout( $Tab, bIsHover ); 3603 } 3604 else if ( !$Tab.data( 'iFlyoutInterval' ) ) 3605 { 3606 var fnCleanupFlyoutInterval = function() { 3607 $Tab.removeData( 'iFlyoutInterval' ); 3608 $Tab.off( 'mouseleave.FlyoutCancel' ); 3609 } 3610 $Tab.on( 'mouseleave.FlyoutCancel', function(e) { 3611 window.clearTimeout( $Tab.data('iFlyoutInterval') ); 3612 fnCleanupFlyoutInterval(); 3613 }); 3614 $Tab.data( 'iFlyoutInterval', window.setTimeout( function() { 3615 fnCleanupFlyoutInterval(); 3616 fnShowFlyout( $Tab, bIsHover ); 3617 }, msDelay ) ); 3618 } 3619 3620 }); 3621 3622 $J(document).on('mouseleave.Flyout', '.flyout_tab', function(e) { 3623 var $Tab = $J(this); 3624 var $Content = $J('#' + $Tab.data('flyout') ); 3625 var bResponsiveSlidedownMenu = UseSmallScreenMenu() && $Content.hasClass('responsive_slidedown'); 3626 3627 if ( !$Content.length || $Content.data('flyout-event-running') || bResponsiveSlidedownMenu || 3628 $Content.is( e.relatedTarget ) || $J.contains( $Content[0], e.relatedTarget ) ) 3629 return; 3630 3631 if ( $Content.is(':visible') ) 3632 { 3633 HideFlyoutMenu( null, $Tab, $Content ); 3634 3635 return; 3636 } 3637 }); 3638 3639 $J(document).on('vgp_onok', '.flyout_tab', function( e ) { 3640 fnShowFlyout( $J(this), false, true ); 3641 e.stopPropagation(); 3642 }); 3643} 3644 3645function PollOnUserActionAfterInterval( strNamespace, nPollInterval, fnCallback, nPollImmediateInterval ) 3646{ 3647 var bCallbackInvoked = false; 3648 var tsLastUserAction = $J.now(); 3649 var fnInvokeCallbackOnNextUserAction; 3650 3651 var strEvents = ['touchstart','mousemove','focus','scroll',''].join( '.' + strNamespace + ' ' ); 3652 3653 var fnDoPoll = function() { 3654 if ( !bCallbackInvoked ) 3655 fnCallback(); 3656 bCallbackInvoked = true; 3657 fnInvokeCallbackOnNextUserAction(); 3658 }; 3659 3660 fnInvokeCallbackOnNextUserAction = function() { 3661 window.setTimeout( function() { 3662 bCallbackInvoked = false; 3663 if ( nPollImmediateInterval && $J.now() <= tsLastUserAction + nPollImmediateInterval ) 3664 { 3665 fnDoPoll(); 3666 } 3667 else 3668 { 3669 $J(window).on( strEvents, function() { 3670 $J(window ).off( '.' + strNamespace ); 3671 tsLastUserAction = $J.now(); 3672 fnDoPoll(); 3673 }); 3674 } 3675 }, nPollInterval ); 3676 }; 3677 3678 fnInvokeCallbackOnNextUserAction(); 3679} 3680 3681var DELAY_BETWEEN_NOTIFICATION_COUNT_POLLS_MS = 60 * 1000; 3682function EnableNotificationCountPolling() 3683{ 3684 PollOnUserActionAfterInterval( 'NotificationCountPoll', DELAY_BETWEEN_NOTIFICATION_COUNT_POLLS_MS, UpdateNotificationCounts ); 3685} 3686 3687function UpdateNotificationCounts() 3688{ 3689 if ( window.RefreshSteamNotifications ) 3690 window.RefreshSteamNotifications(); 3691} 3692 3693function PostToURLWithSession( url, rgParams ) 3694{ 3695 var $Form = $J('<form/>', {'action': url, 'method': 'POST' } ); 3696 3697 // site must set this js var or pass to this function 3698 if ( typeof g_sessionID != 'undefined' ) 3699 $Form.append( $J('<input/>', {'type': 'hidden', 'name': 'sessionid', 'value': g_sessionID } ) ); 3700 3701 if ( rgParams ) 3702 { 3703 for ( var name in rgParams ) 3704 $Form.append( $J('<input/>', {'type': 'hidden', 'name': name, 'value': rgParams[name] } ) ); 3705 } 3706 $Form.appendTo( 'body' ); 3707 $Form.submit(); 3708} 3709 3710 3711function ShowWithFade( elem, speed ) 3712{ 3713 var $Elem = $JFromIDOrElement(elem); 3714 3715 $Elem.stop(); 3716 $Elem.fadeTo( speed || 200, 1.0 ); //fadeTo rather than fadeIn in case it was already in a fade 3717} 3718 3719function HideWithFade( elem, speed ) 3720{ 3721 var $Elem = $JFromIDOrElement(elem); 3722 3723 $Elem.stop(); 3724 $Elem.fadeOut( speed || 200 ); 3725} 3726 3727 3728function LaunchWebChat( params, paramsFriendsUI ) 3729{
3730 var winChat = window.open( '', 'SteamWebChat', 'height=790,width=1015,resize=yes,scrollbars=yes' ); 3731 if ( !winChat ) 3732 { 3733 // popup blocked - this sometimes happens when chat is initiated from the store. just roll with it. 3734 return; 3735 } 3736 3737 var bNewPopup = false; 3738 var bCrossOrigin = false; 3739 3740 try { 3741 bNewPopup = (winChat.location == 'about:blank' ); 3742 } 3743 catch ( e ) 3744 { 3745 // cross-origin exception on http pages etc 3746 bCrossOrigin = true; 3747 } 3748 3749 if ( bNewPopup ) 3750 { 3751 if ( params ) 3752 SetValueLocalStorage( 'rgChatStartupParam', V_ToJSON( params ) ); 3753 3754 if ( paramsFriendsUI ) 3755 { 3756 var fnBoundMessageListener = function( event ) 3757 { 3758 if ( event.source == winChat && event.data == "FriendsUIReady" && 3759 event.origin == 'https://steamcommunity.com') 3760 { 3761 winChat.postMessage( paramsFriendsUI, 'https://steamcommunity.com/' ); 3762 window.removeEventListener( 'message', fnBoundMessageListener ); 3763 } 3764 }; 3765 window.addEventListener( 'message', fnBoundMessageListener ); 3766 } 3767 3768 // created a new window, set the url 3769 winChat.location = 'https://steamcommunity.com/chat/'; 3770 } 3771 else 3772 { 3773 if ( params && !bCrossOrigin && winChat.OnWebchatLaunchURL ) 3774 winChat.OnWebchatLaunchURL( params ); 3775 3776 if ( paramsFriendsUI ) 3777 winChat.postMessage( paramsFriendsUI, 'https://steamcommunity.com/' ); 3778 } 3779 winChat.focus(); 3780} 3781 3782function ShowSupportAlerts(url) 3783{ 3784 window.open( url, 'SupportAlerts', 'height=700,width=700,resize=yes,scrollbars=yes' ); 3785} 3786 3787function UnlockFamilyView( strURL ) 3788{ 3789 window.location = strURL; 3790} 3791 3792function LockFamilyView( bStore ) 3793{ 3794 3795 var urlFirst = ''; 3796 var urlSecond = ''; 3797 if ( bStore ) 3798 { 3799 urlFirst = 'https://store.steampowered.com/'; 3800 urlSecond = 'https://steamcommunity.com/'; 3801 } 3802 else 3803 { 3804 urlFirst = 'https://steamcommunity.com/'; 3805 urlSecond = 'https://store.steampowered.com/'; 3806 } 3807 3808 ShowConfirmDialog( 'Return to Family View', 3809 'Are you sure you want to return to Family View?', 3810 'Return to Family View' 3811 ).done( function() { 3812 ShowBlockingWaitDialog( 'Return to Family View' ); 3813 3814 CrossDomainPost( urlFirst + '/parental/ajaxlock', {} ).then( function() { 3815 $J.when( 3816 CrossDomainPost( 'https://checkout.steampowered.com/parental/ajaxlock', {} ), 3817 CrossDomainPost( 'https://steam.tv/parental/ajaxlock', {} ), 3818 CrossDomainPost( urlSecond + 'parental/ajaxlock', {} ) 3819 ).always( function() 3820 { 3821 window.location = urlFirst; 3822 } ); 3823 }); 3824 } ); 3825} 3826 3827function setTimezoneCookies() 3828{ 3829 var now = new Date(); 3830 var expire = new Date(); 3831 3832 // One year expiration, this way we don't need to wait at least one page 3833 // load to have accurate timezone info each session, but only each time the user 3834 // comes with cleared cookies 3835 expire.setTime( now.getTime() + 3600000*24*365 ); 3836 var tzOffset = now.getTimezoneOffset() * -1 * 60; 3837 var isDST = 0; 3838 3839 var sameSite = ''; 3840 if ( window.location.protocol == 'https' ) 3841 sameSite = '; Secure; SameSite=None;' 3842 document.cookie = "timezoneOffset=" + tzOffset + "," + isDST + ";expires="+expire.toGMTString() + ";path=/" + sameSite; 3843} 3844 3845function FlushStyleChanges( element ) 3846{ 3847 $J( element ).css( 'opacity'); 3848} 3849 3850var k_EScrollbarDirectionVertical = 1; 3851var k_EScrollbarDirectionHorizontal = 2; 3852 3853window.VScrollbar = function( eleTarget, eleHandle, direction ) 3854{ 3855 this.m_eleHandle = eleHandle; 3856 this.m_eleTarget = eleTarget; 3857 3858 var instance = this; 3859 3860 var propOffset, propSize, propOuterSize, propDimension, propPage, directionInvert, propOffsetEvent; 3861 3862 if( direction == k_EScrollbarDirectionVertical) 3863 { 3864 propOffset = 'top'; 3865 propSize = 'height'; 3866 propOuterSize = 'outerHeight'; 3867 propDimension = 'y'; 3868 propPage = 'pageY'; 3869 propOffsetEvent = 'offsetY'; 3870 } else if( direction == k_EScrollbarDirectionHorizontal ) 3871 { 3872 propOffset = 'left'; 3873 propSize = 'width'; 3874 propOuterSize = 'outerWidth'; 3875 propDimension = 'x'; 3876 propPage = 'pageX'; 3877 propOffsetEvent = 'offsetX'; 3878 3879 this.m_eleTarget.first().css({'white-space': 'nowrap'}); 3880 } 3881 3882 // Set up some CSS properties we need 3883 this.m_eleHandle.css({position: 'absolute'}); 3884 if( this.m_eleHandle.parent().css('position') == 'static' ) 3885 this.m_eleHandle.parent().css({position: 'relative'}); // Needs to be relative or absolute, only set it if we didn't do it in CSS 3886 this.m_eleTarget.css({position: 'relative', float: 'left'}); 3887 this.m_eleTarget.css(propOffset, '0px'); 3888 this.m_eleTarget.addClass('animating'); 3889 this.m_eleTarget.parent().addClass('v_scrollbar_target'); 3890 this.m_eleTarget.parent().css({position: 'relative', overflow: 'hidden'}); 3891 this.m_nDesiredPosition = 0; 3892 this.m_flPercent = 0; 3893 3894 3895 var funcUpdate = function( bDisableTransitions ) 3896 { 3897 var bScrolled = true; 3898 if( instance.m_eleTarget[propSize]() - instance.m_eleTarget.parent()[propSize]() <= 0 ) 3899 { 3900 instance.m_eleHandle.parent().addClass('disabled'); 3901 instance.m_flPercent = 0; 3902 bScrolled = false;
3903 } else 3904 instance.m_eleHandle.parent().removeClass('disabled'); 3905 3906 if( instance.m_flPercent < 0 ) 3907 instance.m_flPercent = 0; 3908 3909 if( instance.m_flPercent > 1 ) 3910 instance.m_flPercent = 1; 3911 3912 3913 var percent = instance.m_flPercent; 3914 3915 if( bDisableTransitions ) 3916 instance.DisableTransitions(); 3917 3918 instance.m_nDesiredPosition = -percent * ( instance.m_eleTarget[propSize]() - instance.m_eleTarget.parent()[propSize]()); 3919 3920 // Update container 3921 instance.m_eleTarget[0].style[propOffset] = instance.m_nDesiredPosition + 'px'; 3922 3923 // Update scroll handle 3924 var handleMax = instance.m_eleHandle.parent()[propSize]() - instance.m_eleHandle[propOuterSize](); 3925 instance.m_eleHandle[0].style[propOffset] = ( percent * handleMax ) + 'px'; 3926 3927 if( bDisableTransitions ) 3928 instance.EnableTransitions(); 3929 3930 return bScrolled; 3931 }; 3932 3933 var funcMouseMove = function( event ) { 3934 var localDimension = event[propPage] - instance.m_eleHandle.parent().offset()[propOffset]; 3935 var localMax = instance.m_eleHandle.parent()[propSize]() - instance.m_eleHandle[propOuterSize](); 3936 3937 var percent = localDimension / localMax; 3938 instance.m_flPercent = percent; 3939 3940 if( funcUpdate(true) ) 3941 event.preventDefault(); 3942 3943 3944 }; 3945 3946 $J(eleTarget).bind('mousewheel DOMMouseScroll',function( event ){ 3947 var delta = event.originalEvent.wheelDelta || event.originalEvent.detail * -12; 3948 3949 var localY = instance.m_nDesiredPosition * -1; 3950 var localMax = instance.m_eleTarget[propOuterSize]() - instance.m_eleTarget.parent()[propSize](); 3951 3952 if( localY <= 0 && delta > 0 || localY == localMax && delta < 0 ) 3953 return; 3954 3955 localY -= delta; 3956 3957 if( localY < 0 ) localY = 0; 3958 if( localY > localMax ) localY = localMax; 3959 3960 var percent = localY / localMax; 3961 3962 instance.m_flPercent = percent; 3963 3964 if( funcUpdate() ) 3965 event.preventDefault(); 3966 }); 3967 3968 $J(eleHandle.parent()).bind('click',function( event ){ 3969 var localY = instance.m_eleTarget.position()[propOffset] * -1; 3970 var localMax = instance.m_eleTarget[propOuterSize]() - instance.m_eleTarget.parent()[propSize](); 3971 3972 var step = instance.m_eleTarget.parent()[propSize](); 3973 3974 if( event[propOffsetEvent] < instance.m_eleHandle.position()[propOffset] ) 3975 step *= -1; 3976 3977 localY += step; 3978 3979 if( localY < 0 ) localY = 0; 3980 if( localY > localMax ) localY = localMax; 3981 3982 var percent = localY / localMax; 3983 3984 instance.m_flPercent = percent; 3985 3986 if( funcUpdate() ) 3987 event.preventDefault(); 3988 }); 3989 3990 eleHandle.mousedown(function( event ){ 3991 $J(window).bind('mousemove.scroll', funcMouseMove); 3992 event.stopPropagation(); 3993 }); 3994 3995 eleHandle.click(function( event ){ 3996 event.stopPropagation(); 3997 }); 3998 3999 $J(window).mouseup(function( event ){ 4000 $J(window).unbind('mousemove.scroll'); 4001 event.stopPropagation(); 4002 }); 4003 4004 // Resets scroll position to 0 and updates the window. Useful when adding/removing elements or resizing the scroll area 4005 this.Reset = function() { 4006 instance.m_flPercent = 0; 4007 4008 return funcUpdate(true); 4009 }; 4010 4011 // Ensures target element is visible. This is taken from position() (NOT OFFSET) so the element must be a direct child of the scroll area 4012 this.EnsureVisible = function( ele ) { 4013 var $ele = $J(ele); 4014 var eleSize = $ele[propOuterSize](true); 4015 var minEdge = $ele.position()[propOffset] - (eleSize * 0.5); 4016 var maxEdge = minEdge + (eleSize * 2); 4017 4018 4019 var viewportMax = instance.m_eleTarget[propOuterSize]() - instance.m_eleTarget.parent()[propSize](); 4020 var viewportSize = instance.m_eleTarget.parent()[propSize](); 4021 var viewportPosition = instance.m_eleTarget.position()[propOffset] * -1; 4022 4023 if( viewportPosition > minEdge ) 4024 instance.m_flPercent = minEdge / viewportMax; 4025 else if( ( viewportPosition + viewportSize ) < maxEdge ) 4026 instance.m_flPercent = ( maxEdge - viewportSize ) / viewportMax; 4027 4028 return funcUpdate(); 4029 }; 4030 4031 this.EnableTransitions = function() { 4032 instance.m_eleTarget.addClass('animating'); 4033 }; 4034 4035 this.DisableTransitions = function() { 4036 instance.m_eleTarget.removeClass('animating'); 4037 }; 4038 4039 funcUpdate(true); 4040 4041 4042}; 4043 4044function InitAutoComplete( elInput, fnSearch, fnOnChange ) 4045{ 4046 return new CAutoComplete( elInput, fnSearch, fnOnChange ); 4047} 4048 4049function CAutoComplete( elInput, fnSearch, fnOnChange ) 4050{ 4051 this.m_bSuppresseNextKeyUp = false;
4052 this.m_hSearchTimeout = 0; 4053 this.m_strLastSearch = ''; 4054 4055 this.m_$Input = $J( elInput ); 4056 this.m_fnSearch = fnSearch; 4057 this.m_fnOnChange = fnOnChange; 4058 4059 var strName = this.m_$Input.attr( 'name' ); 4060 if ( !strName ) 4061 strName = 'unknown'; 4062 4063 strName = strName + '_autocomplete'; 4064 4065 this.m_$Popup = $J ( '<div class="' + strName + '"></div>' ); 4066 this.m_$Popup.css( 'width', this.m_$Input.outerWidth() + 'px' ); 4067 this.m_$Input.after( this.m_$Popup ); 4068 this.m_$Popup.hide(); 4069 4070 var _this = this; 4071 this.m_$Input.on( 'keydown.autocomplete', function( event ) { _this.OnInputKeyDown( event ); } ); 4072 this.m_$Input.on( 'keyup.autocomplete', function( event ) { _this.OnInputKeyUp( event ); } ); 4073 this.m_$Input.on( 'blur.autocomplete', function( event ) { _this.OnInputBlur( event ); } ); 4074 this.m_$Input.on( 'change input paste', function( event ) { _this.OnInputChange( event ); } ); 4075 4076 this.m_$Popup.on( 'mousedown.autocomplete', function( event ) { _this.m_$Popup.data( 'mousedown', true ); } ); 4077 this.m_$Popup.on( 'mouseup.autocomplete', function( event ) { _this.m_$Popup.data( 'mousedown', false ); } ); 4078} 4079 4080CAutoComplete.KEY_ENTER = 13; 4081CAutoComplete.KEY_UP = 38; 4082CAutoComplete.KEY_DOWN = 40; 4083 4084CAutoComplete.prototype.OnInputChange = function( event ) 4085{ 4086 if ( this.m_fnOnChange ) 4087 this.m_fnOnChange( this.m_$Input, null ); 4088 4089 if ( this.m_hSearchTimeout == 0 ) 4090 { 4091 var _this = this; 4092 var fnDoSearch = function() 4093 { 4094 _this.m_hSearchTimeout = 0; 4095 var strSearch = _this.m_$Input.val(); 4096 if ( strSearch == _this.m_strLastSearch ) 4097 return; 4098 4099 _this.m_strLastSearch = strSearch; 4100 _this.m_fnSearch( _this.m_$Input, strSearch, function( $Contents ) { _this.SetPopupContents( $Contents ); } ); 4101 }; 4102 4103 this.m_hSearchTimeout = window.setTimeout( fnDoSearch, 300 ); 4104 } 4105} 4106 4107CAutoComplete.prototype.SetPopupContents = function( $Contents ) 4108{ 4109 if ( !$Contents ) 4110 { 4111 this.ClosePopup(); 4112 return; 4113 } 4114 4115 this.m_$Popup.html( '' ); 4116 this.m_$Popup.append( $Contents ); 4117 4118 var _this = this; 4119 this.m_$Popup.children().on( 'click.autocomplete', function( event ) { _this.SelectSuggestion( $J( this ) ); } ); 4120 this.m_$Popup.show(); 4121} 4122 4123CAutoComplete.prototype.OnInputBlur = function( event ) 4124{ 4125 if ( !this.m_$Popup.data( 'mousedown' ) ) 4126 this.ClosePopup(); 4127} 4128 4129CAutoComplete.prototype.SelectSuggestion = function( $Suggestion ) 4130{ 4131 if ( $Suggestion.length == 0 ) 4132 return; 4133 4134 this.m_$Input.val( $Suggestion.data( 'suggestion' ) ); 4135 if ( this.m_fnOnChange ) 4136 this.m_fnOnChange( this.m_$Input, $Suggestion ); 4137 4138 this.ClosePopup(); 4139 this.m_$Input.focus(); 4140} 4141 4142CAutoComplete.prototype.ClosePopup = function() 4143{ 4144 if ( this.m_hSearchTimeout != 0 ) 4145 { 4146 window.clearTimeout( this.m_hSearchTimeout ); 4147 this.m_hSearchTimeout = 0; 4148 } 4149 4150 this.m_strLastSearch = ''; 4151 this.m_$Popup.hide(); 4152} 4153 4154CAutoComplete.prototype.OnInputKeyDown = function( event ) 4155{ 4156 var $CurSuggestion = this.m_$Popup.children('.focus'); 4157 var $NewSuggestion = $J(); 4158 4159 if ( event.keyCode == CAutoComplete.KEY_ENTER ) 4160 { 4161 this.SelectSuggestion( $CurSuggestion ); 4162 this.m_bSuppressNextKeyUp = true; 4163 event.preventDefault(); 4164 return; 4165 } 4166 4167 if ( event.keyCode == CAutoComplete.KEY_UP || event.keyCode == CAutoComplete.KEY_DOWN ) 4168 { 4169 if ( event.keyCode == CAutoComplete.KEY_UP ) 4170 { 4171 if ( $CurSuggestion.length ) 4172 $NewSuggestion = $CurSuggestion.prev(); 4173 if ( !$NewSuggestion.length ) 4174 $NewSuggestion = this.m_$Popup.children( 'div:last-child' ); 4175 } 4176 else 4177 { 4178 if ( $CurSuggestion.length ) 4179 $NewSuggestion = $CurSuggestion.next(); 4180 if ( !$NewSuggestion.length ) 4181 $NewSuggestion = this.m_$Popup.children( 'div:first-child' ); 4182 } 4183 4184 if ( $NewSuggestion.length ) 4185 { 4186 $CurSuggestion.removeClass( 'focus' ); 4187 $NewSuggestion.addClass( 'focus' ); 4188 } 4189 4190 event.preventDefault(); 4191 } 4192} 4193 4194CAutoComplete.prototype.OnInputKeyUp = function( event ) 4195{ 4196 if ( this.m_bSuppressNextKeyUp && event.keyCode == CAutoComplete.KEY_ENTER ) 4197 { 4198 this.m_bSuppressNextKeyUp = false; 4199 event.preventDefault(); 4200 } 4201} 4202 4203 4204 4205function GetCurrentScrollPercentage() 4206{ 4207 var s = $J(window).scrollTop(); 4208 var d = $J(document).height(); 4209 var c = $J(window).height(); 4210 var scrollPercent = (s / (d-c)) * 100; 4211 return scrollPercent; 4212} 4213 4214// @elemID id of the element 4215// @fixedOffsetTop offset from the top when fixed 4216// @bScrollWithPageIfTooTall if the element is taller than the page, then it will "scroll" with the page if this is true 4217// @docHeightOffset if bScrollWithPageIfTooTall is set to true, then this is how much the document height is reduced by (recommend this to be 130 for the typical footer) 4218function FixedElementOnScrollWrapper ( elemID, fixedOffsetTop, bScrollWithPageIfTooTall, docHeightOffset, params ) 4219{ 4220 params = $J.extend( { 4221 fixedClass: null 4222 }, params ); 4223 4224 this.fixedElement = $( elemID ); 4225 this.$FixedElement = $JFromIDOrElement( elemID ); 4226 this.fixedOffsetTop = typeof fixedOffsetTop != "undefined" ? fixedOffsetTop : 0; 4227 this.bScrollWithPageIfTooTall = typeof bScrollWithPageIfTooTall != "undefined" ? bScrollWithPageIfTooTall : false;
4228 this.docHeightOffset = typeof docHeightOffset != "undefined" ? docHeightOffset : 0; 4229 this.homePosn = { x: this.fixedElement.cumulativeOffset()[0], y: this.fixedElement.cumulativeOffset()[1] }; 4230 this.fixedClass = params.fixedClass; 4231 4232 4233 this.$FixedElementPadding = $J('<div/>', {'class': 'FixedElementOnScrollWrapper_padding', 'id': 'ScrollWrapperPadding_' + elemID } ).hide(); 4234 this.fixedElement.insert( { before: this.$FixedElementPadding[0] } ); 4235 4236 var _this = this; 4237 $J(window).on( 'scroll.FixedElementOnScrollWrapper hashchange.FixedElementOnScrollWrapper', function() { _this.handleScroll() } ); 4238 $J(window).on( 'resize.FixedElementOnScrollWrapper', function() { _this.handleScroll( true /* force recalc */ ) } ); 4239 this.handleScroll(); 4240} 4241 4242FixedElementOnScrollWrapper.prototype.BIsFixed = function() 4243{ 4244 if ( this.fixedClass ) 4245 return this.$FixedElement.hasClass( this.fixedClass ); 4246 else 4247 return this.$FixedElement.css( 'position' ) == 'fixed'; 4248} 4249 4250FixedElementOnScrollWrapper.prototype.handleScroll = function( bForceRecalc ) 4251{ 4252 this.scrollOffset = document.viewport.getScrollOffsets().top; 4253 var offsetTop = this.fixedOffsetTop + GetResponsiveHeaderFixedOffsetAdjustment(); 4254 4255 if ( bForceRecalc && !this.BIsFixed() ) 4256 this.homePosn = { x: this.fixedElement.cumulativeOffset()[0], y: this.fixedElement.cumulativeOffset()[1] }; 4257 4258 if ( this.scrollOffset > ( this.homePosn.y - offsetTop ) ) 4259 { 4260 if ( !this.BIsFixed() || bForceRecalc ) 4261 { 4262 if ( this.fixedClass ) 4263 this.$FixedElement.addClass( this.fixedClass ); 4264 else 4265 this.$FixedElement.css( 'position', 'fixed' ); 4266 4267 this.fixedElement.style.top = offsetTop + 'px'; 4268 this.fixedElement.style.left = this.homePosn.x; 4269 4270 // jquery show() sets display to block, which prevents css from hiding this element if needed. 4271 this.$FixedElementPadding.css( 'display', '' ); 4272 this.$FixedElementPadding.css( 'height', this.fixedElement.getHeight() + 'px' ); 4273 } 4274 4275 if ( this.bScrollWithPageIfTooTall ) 4276 { 4277 // this forces the element to scroll off the page, but there's enough that isn't on the page, "scroll" this guy percentage-wise 4278 var elemHeight = this.fixedElement.getHeight() + offsetTop; 4279 if ( elemHeight > document.viewport.getHeight() ) 4280 { 4281 var currentScrollPercentage = GetCurrentScrollPercentage(); 4282 var heightDiff = elemHeight - ( document.viewport.getHeight() - this.docHeightOffset ); 4283 offsetTop -= Math.floor( heightDiff * currentScrollPercentage / 100 ); 4284 this.fixedElement.style.top = offsetTop + 'px'; 4285 } 4286 } 4287 } 4288 else 4289 { 4290 if ( this.BIsFixed() ) 4291 { 4292 if ( this.fixedClass ) 4293 this.$FixedElement.removeClass( this.fixedClass ); 4294 else 4295 this.$FixedElement.css( 'position', '' ); 4296 4297 this.$FixedElement.css( 'top', '' ).css( 'left', '' ); 4298 this.$FixedElementPadding.css( 'display', 'none' ); 4299 } 4300 } 4301} 4302 4303// general text suggestion control 4304 4305// fnSuggestForTerm will be called with two values, the current string to get suggestions for, 4306// and the callback to invoke with the new values. You should always invoke the callback per call 4307// to fnSuggestForTerm, but can delay due to ajax if needed. 4308 4309// supported options: 4310// bDoEmptySearchOnFirstFocus - run a search for "" (or whatever is in the box) when the text input is first interacted with 4311function CTextInputSuggest( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass, opts ) 4312{ 4313 this.Init( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass, opts ); 4314} 4315 4316CTextInputSuggest.prototype.Init = function( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass, opts ) 4317{ 4318 opts = opts ? opts : {}; 4319 if( !strCssClass ) 4320 strCssClass = 'popup_block_new'; 4321 this.m_bHaveSuggestions = false; 4322 this.m_$Input = $InputElement; 4323 this.m_fnSuggestForTerm = fnSuggestForTerm; 4324 this.m_fnOnSuggest = fnOnSuggest || function( term ) {}; 4325 this.m_strLastVal = opts.bDoEmptySearchOnFirstFocus ? undefined : ''; // '' is the value in the input when it's empty 4326 this.m_align = 'left'; 4327 this.m_valign = 'bottom'; 4328 4329 this.m_$Focus = $J(); 4330 this.m_strLastFocusVal = null; 4331 4332 this.m_nNextRequestID = 1; 4333 this.m_nRequestIDRecvd = 0; 4334 4335 4336 this.m_$SuggestionsCtn = $J('<div/>', {'class': strCssClass, style: 'display: none;' } ); 4337 this.m_$Suggestions = $J('<div/>', {'class': 'popup_body popup_menu' } ); 4338 this.m_$SuggestionsCtn.append( this.m_$Suggestions ); 4339 4340 this.m_$SuggestionsCtn.hide(); 4341 $J(document.body).append( this.m_$SuggestionsCtn ); 4342 4343 var zIndex = 200; //normal popup zindex 4344 this.m_$Input.parents().each( function() { 4345 var zIndexParent = $J(this).css('zIndex'); 4346 if ( zIndexParent != 'auto' && zIndexParent != 0 ) 4347 { 4348 zIndex = zIndexParent; 4349 } 4350 }); 4351 this.m_$SuggestionsCtn.css( 'zIndex', zIndex + 20 ); 4352 4353 var _this = this; 4354 this.m_$Input.on( 'keyup.CTextInputSuggest click.CTextInputSuggest', function( event ) { _this.OnTextChanged( event ) } ); 4355 this.m_$Input.on( 'paste.CTextInputSuggest cut.CTextInputSuggest', function() { window.setTimeout( function() { _this.OnTextChanged() }, 1 ); } ); 4356 this.m_$Input.on( 'keydown.CTextInputSuggest', function( event ) { _this.OnKeyDown( event ) } ); 4357 this.m_$Input.on( 'focus.CTextInputSuggest', function() { _this.ShowSuggestions() } ); 4358 this.m_$Input.on( 'blur.CTextInputSuggest', function() { _this.HideSuggestions() } ); 4359} 4360 4361CTextInputSuggest.prototype.SetAlignment = function( align, valign ) 4362{ 4363 this.m_align = align || 'left'; 4364 this.m_valign = valign || 'bottom'; 4365}; 4366 4367CTextInputSuggest.prototype.SetSuggestionsContainerId = function( strSuggestionsCtnId ) 4368{ 4369 this.m_$SuggestionsCtn.attr( 'id', strSuggestionsCtnId ); 4370}; 4371 4372CTextInputSuggest.prototype.ShowSuggestions = function() 4373{ 4374 if ( !this.m_$SuggestionsCtn.find(':visible').length ) 4375 { 4376 if ( this.m_bHaveSuggestions ) 4377 { 4378 AlignMenu(this.m_$Input[0], this.m_$SuggestionsCtn[0], this.m_align, this.m_valign, true); 4379 this.m_$SuggestionsCtn.fadeIn('fast'); 4380 } 4381 else if ( this.m_strLastVal === undefined ) 4382 { 4383 // run an initial query. This will set m_strLastVal to empty string (or whatever else is
4383in the box, 4384 // if something came from browser history) 4385 this.OnTextChanged( undefined ); 4386 } 4387 } 4388}; 4389 4390CTextInputSuggest.prototype.HideSuggestions = function() 4391{ 4392 if ( this.m_bHaveSuggestions ) 4393 this.m_$SuggestionsCtn.fadeOut( 'fast' ); 4394 else 4395 this.m_$SuggestionsCtn.hide(); 4396}; 4397 4398CTextInputSuggest.prototype.OnSuggestionSelected = function( $Suggestion ) 4399{ 4400 this.m_$Input.val( $Suggestion.text() ); 4401 4402 this.m_bHaveSuggestions = false; 4403 this.m_$Focus = $J(); 4404 this.HideSuggestions(); 4405 4406 this.m_fnOnSuggest( $Suggestion.text() ); 4407}; 4408 4409CTextInputSuggest.prototype.SetSuggestions = function( rgSuggestions ) 4410{ 4411 var strLastFocus = this.m_strLastFocusVal; 4412 4413 this.m_$Suggestions.empty(); 4414 4415 this.m_$Focus = $J(); 4416 this.m_strLastFocus = null; 4417 4418 if ( rgSuggestions && rgSuggestions.length ) 4419 { 4420 var _this = this; 4421 for ( var i = 0; i < rgSuggestions.length; i++ ) 4422 { 4423 var $Suggestion = $J('<div/>', {'class': 'suggestion_item popup_menu_item' } ); 4424 $Suggestion.text( rgSuggestions[i] ); 4425 $Suggestion.click( $J.proxy( this.OnSuggestionSelected, this, $Suggestion ) ); 4426 $Suggestion.mouseenter( $J.proxy( this.SetFocus, this, $Suggestion ) ); 4427 4428 this.m_$Suggestions.append( $Suggestion ); 4429 4430 if ( rgSuggestions[i] == strLastFocus ) 4431 this.SetFocus( $Suggestion ); 4432 } 4433 this.m_bHaveSuggestions = true; 4434 this.ShowSuggestions(); 4435 } 4436 else 4437 { 4438 this.m_bHaveSuggestions = false; 4439 this.HideSuggestions(); 4440 } 4441}; 4442 4443CTextInputSuggest.prototype.OnTextChanged = function( event ) 4444{ 4445 if ( event && ( event.which == 13 || event.which == 27 ) ) 4446 return; 4447 4448 var value = this.m_$Input.val().trim(); 4449 if ( value !== this.m_strLastVal ) 4450 { 4451 var _this = this; 4452 var nRequestID = this.m_nNextRequestID++; 4453 this.m_fnSuggestForTerm( value, function( rgSuggestions ) { 4454 // we've already recieved a more recent response, this is out-of-order 4455 if ( _this.m_nRequestIDRecvd > nRequestID ) 4456 return; 4457 4458 _this.m_nRequestIDRecvd = nRequestID; 4459 _this.SetSuggestions( rgSuggestions ); 4460 } ); 4461 4462 this.m_strLastVal = value; 4463 } 4464}; 4465 4466CTextInputSuggest.prototype.OnKeyDown = function( event ) 4467{ 4468 if ( event.which == 27 ) 4469 { 4470 this.HideSuggestions(); 4471 } 4472 else if ( this.m_bHaveSuggestions ) 4473 { 4474 var $NewSuggestion = null; 4475 if ( event.which == 13 ) 4476 { 4477 if ( this.m_$Focus.length && this.m_bHaveSuggestions ) 4478 { 4479 this.OnSuggestionSelected( this.m_$Focus ); 4480 event.stopPropagation(); 4481 event.preventDefault(); 4482 } 4483 } 4484 else if ( event.which == 38 /* up arrow */ ) 4485 { 4486 event.preventDefault(); 4487 if ( this.m_$Focus.length ) 4488 $NewSuggestion = this.m_$Focus.prev(); 4489 if ( !$NewSuggestion ) 4490 $NewSuggestion = this.m_$Suggestions.children().last(); 4491 } 4492 else if ( event.which == 40 /* down arrow */ ) 4493 { 4494 event.preventDefault(); 4495 if ( this.m_$Focus.length ) 4496 $NewSuggestion = this.m_$Focus.next(); 4497 if ( !$NewSuggestion ) 4498 $NewSuggestion = this.m_$Suggestions.children().first(); 4499 } 4500 4501 if ( $NewSuggestion ) 4502 this.SetFocus( $NewSuggestion ); 4503 } 4504}; 4505 4506CTextInputSuggest.prototype.SetFocus = function( $Suggestion ) 4507{ 4508 this.m_$Focus.removeClass( 'focus' ); 4509 this.m_$Focus = $Suggestion; 4510 this.m_$Focus.addClass( 'focus' ); 4511 this.m_strLastFocusVal = $Suggestion.text(); 4512}; 4513 4514CTextInputSuggest.prototype.Destroy = function() 4515{ 4516 this.m_$SuggestionsCtn.remove(); 4517 this.m_$Input.off( '.CTextInputSuggest' ); 4518}; 4519 4520/** 4521 * Similar to CTextInputSuggest, but uses associative arrays instead of just text; useful for when we may have more than one 4522 * item with the same text name, or when you want to use HTML in the item name instead of just plaintext 4523 * 4524 * Each suggestion should include a 'key', and one of the following: 4525 * text - Suggestion text (escaped, sets textContents) 4526 * html - Raw suggestion html (NOT ESCAPED, sets innerHTML) 4527 * 4528 * @param $InputElement 4529 * @param fnSuggestForTerm 4530 * @param fnOnSuggest 4531 * @constructor 4532 */ 4533function CIndexedInputSuggest( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass ) 4534{ 4535 this.Init( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass ); 4536} 4537 4538CIndexedInputSuggest.prototype = Object.create(CTextInputSuggest.prototype);; 4539 4540CIndexedInputSuggest.prototype.OnSuggestionSelected = function( $Suggestion ) 4541{ 4542 this.m_$Input.val( $Suggestion.text() ); 4543 4544 this.m_bHaveSuggestions = false;
4545 this.m_$Focus = $J(); 4546 this.HideSuggestions(); 4547 4548 this.m_fnOnSuggest( $Suggestion.data('suggest-key'), $Suggestion.text() ); 4549}; 4550 4551CIndexedInputSuggest.prototype.SetSuggestions = function( rgSuggestions ) 4552{ 4553 var strLastFocus = this.m_strLastFocusVal; 4554 4555 this.m_$Suggestions.empty(); 4556 4557 this.m_$Focus = $J(); 4558 this.m_strLastFocus = null; 4559 4560 if ( rgSuggestions && rgSuggestions.length ) 4561 { 4562 var _this = this; 4563 for ( var i = 0; i < rgSuggestions.length; i++ ) 4564 { 4565 4566 var $Suggestion = $J('<div/>', {'class': 'suggestion_item popup_menu_item' } ); 4567 if( rgSuggestions[i].text ) 4568 $Suggestion.text( rgSuggestions[i].text ); 4569 else if( rgSuggestions[i].html ) 4570 $Suggestion.html( rgSuggestions[i].html ); 4571 4572 $Suggestion.data('suggest-key', rgSuggestions[i].key ) 4573 4574 $Suggestion.click( $J.proxy( this.OnSuggestionSelected, this, $Suggestion ) ); 4575 $Suggestion.mouseenter( $J.proxy( this.SetFocus, this, $Suggestion ) ); 4576 4577 this.m_$Suggestions.append( $Suggestion ); 4578 4579 if ( rgSuggestions[i] == strLastFocus ) 4580 this.SetFocus( $Suggestion ); 4581 } 4582 this.m_bHaveSuggestions = true; 4583 this.ShowSuggestions(); 4584 } 4585 else 4586 { 4587 this.m_bHaveSuggestions = false; 4588 this.HideSuggestions(); 4589 } 4590}; 4591 4592 4593/** 4594 * Similar to CTextInputSuggest, but applies styling using the app_type value of each suggestion 4595 * 4596 * Each suggestion should include the suggestion text, and the app_type 4597 * 4598 * @param $InputElement 4599 * @param fnSuggestForTerm 4600 * @param fnOnSuggest 4601 * @constructor 4602 */ 4603function CAppTypeInputSuggest( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass ) 4604{ 4605 this.Init( $InputElement, fnSuggestForTerm, fnOnSuggest, strCssClass, { bDoEmptySearchOnFirstFocus: true } ); 4606} 4607 4608CAppTypeInputSuggest.prototype = Object.create( CTextInputSuggest.prototype ); 4609 4610CAppTypeInputSuggest.prototype.SetSuggestions = function( rgSuggestions ) 4611{ 4612 var strLastFocus = this.m_strLastFocusVal; 4613 4614 this.m_$Suggestions.empty(); 4615 4616 this.m_$Focus = $J(); 4617 this.m_strLastFocus = null; 4618 4619 if ( rgSuggestions && rgSuggestions.length ) 4620 { 4621 var _this = this; 4622 for ( var i = 0; i < rgSuggestions.length; i++ ) 4623 { 4624 var resultClass = 'suggestion_item popup_menu_item'; 4625 var suggestionText = rgSuggestions[i][0]; 4626 4627 if ( typeof rgSuggestions[i][1] !== 'undefined' ) 4628 { 4629 switch ( parseInt( rgSuggestions[i][1] ) ) 4630 { 4631 case 1: 4632 resultClass += ' app_Game'; 4633 suggestionText = '[Game] ' + suggestionText; 4634 break; 4635 case 2: 4636 suggestionText = '[Application] ' + suggestionText; 4637 break; 4638 case 4: 4639 suggestionText = '[Tool] ' + suggestionText; 4640 break; 4641 case 8: 4642 resultClass += ' app_Demo'; 4643 suggestionText = '[Demo] ' + suggestionText; 4644 break; 4645 case 16: 4646 suggestionText = '[Media] ' + suggestionText; 4647 break; 4648 case 32: 4649 resultClass += ' app_DLC'; 4650 suggestionText = '[DLC] ' + suggestionText; 4651 break; 4652 case 256: 4653 suggestionText = '[Config] ' + suggestionText; 4654 break; 4655 case 2048: 4656 resultClass += ' app_Movie'; 4657 suggestionText = '[Video] ' + suggestionText; 4658 break; 4659 case 8192: 4660 resultClass += ' app_Music'; 4661 suggestionText = '[Music] ' + suggestionText; 4662 break; 4663 case 65536: 4664 resultClass += ' app_Beta'; 4665 suggestionText = '[Beta] ' + suggestionText; 4666 break; 4667 } 4668 } 4669 4670 var $Suggestion = $J('<div/>', {'class': resultClass } ); 4671 $Suggestion.text( suggestionText ); 4672 4673 $Suggestion.click( $J.proxy( this.OnSuggestionSelected, this, $Suggestion ) ); 4674 $Suggestion.mouseenter( $J.proxy( this.SetFocus, this, $Suggestion ) ); 4675 4676 this.m_$Suggestions.append( $Suggestion ); 4677 4678 if ( rgSuggestions[i] == strLastFocus ) 4679 this.SetFocus( $Suggestion ); 4680 } 4681 this.m_bHaveSuggestions = true; 4682 this.ShowSuggestions(); 4683 } 4684 else 4685 { 4686 this.m_bHaveSuggestions = false; 4687 this.HideSuggestions(); 4688 } 4689}; 4690 4691 4692function InitBBCodeVideos( bAllowAutoPlay ) 4693{ 4694 var videos = $J( "video" ); 4695 if ( videos.length != 0 ) 4696 { 4697 for ( var i = 0; i < videos.length; ++i ) 4698 { 4699 var video = videos[i]; 4700 var $video = $J( video ); 4701 4702 if ( $video.hasClass("bb_video_loop") ) 4703 { 4704 video.setAttribute('loop', 'loop'); 4705 } 4706 4707 if ( $video.hasClass("bb_video_controls") ) 4708 { 4709 video.setAttribute( 'controls', 'controls' ); 4710 } 4711 4712 if ( bAllowAutoPlay ) 4713 { 4714 $video.unbind('mouseenter mouseleave'); 4715 4716 video.preload = $video.hasClass("bb_video_preload") ? 'auto' : 'metadata'; 4717
4718 if ( $video.hasClass("bb_video_autoplay") ) 4719 { 4720 video.autoplay = 'autoplay'; 4721 } 4722 } 4723 else 4724 { 4725 video.preload = 'metadata'; 4726 $video.hover( function toggleControls() { 4727 if ( this.hasAttribute("controls") ) { 4728 this.removeAttribute("controls") 4729 } else { 4730 this.setAttribute("controls", "controls") 4731 } 4732 } ); 4733 } 4734 } 4735 } 4736} 4737 4738 4739/** 4740 * Generic interface for handling ajax-driven sub-pages. 4741 * 4742 * @param elTarget Element to dump content into 4743 * @param strBaseURL Base URL to attach nav params 4744 * @param strInstanceId Unique flag to look for when scanning for state change events. also determines our data selector 4745 * Only needed if you're using more than one CAjaxSubPageController on the same page. 4746 * @param strDefaultLocation Used when no navid is provided 4747 * @constructor 4748 */ 4749var CAjaxSubPageController = function( elTarget, strBaseURL, strInstanceId, strDefaultLocation ) 4750{ 4751 this.elTarget = elTarget; 4752 this.strBaseURL = strBaseURL; 4753 this.strStateID = strInstanceId || 'navid'; 4754 this.strDefaultLocation = strDefaultLocation || ''; 4755 4756 this.rgOriginalEvent = {'html':this.elTarget.innerHTML,'title':document.title, 'id': this.strStateID}; 4757 4758 window.addEventListener('popstate', this.OnWindowPopState.bind(this)); 4759 4760 this.InstrumentLinks( document ); 4761 4762 var _this = this; 4763 setTimeout( function(){ $J(_this.elTarget).children().trigger('load'); }, 1); 4764 4765 var strLocation = window.location.href.substring(strBaseURL.length); 4766 this.PaintLinks(strLocation || this.strDefaultLocation); 4767 4768 4769}; 4770 4771/** 4772 * Register click handlers. This also sets the href for browser link preview and fallback for non-click navigation 4773 * events, such as opening in a new tab/window via middle click. 4774 * @param elTarget Element to query from. 4775 * @constructor 4776 */ 4777CAjaxSubPageController.prototype.InstrumentLinks = function( elTarget ) 4778{ 4779 //var rgLinks = elTarget.querySelectorAll('[data-'+this.strStateID+']'); 4780 var rgLinks = $J('[data-'+this.strStateID+']', elTarget); 4781 for( var i=0; i<rgLinks.length; i++) 4782 { 4783 rgLinks[i].addEventListener('click', this.Navigate.bind(this, rgLinks[i].dataset[ this.strStateID ], rgLinks[i].dataset[ 'title' ] ) ); 4784 rgLinks[i].href = this.strBaseURL + rgLinks[i].dataset[ this.strStateID ]; 4785 } 4786}; 4787 4788/** 4789 * Adds 'active' class to link that was clicked or is currently active. 4790 * @constructor 4791 */ 4792CAjaxSubPageController.prototype.PaintLinks = function( strLocation ) 4793{ 4794 // Figure out which link we clicked and paint it with the correct class 4795 var rgLinks = document.querySelectorAll('[data-'+this.strStateID+']'); 4796 for( var i=0; i<rgLinks.length; i++) 4797 { 4798 if( rgLinks[i].dataset[ this.strStateID ] == strLocation || rgLinks[i].dataset[ this.strStateID ] + '/' == strLocation ) 4799 rgLinks[i].classList.add('active'); 4800 else 4801 rgLinks[i].classList.remove('active'); 4802 } 4803}; 4804 4805/** 4806 * Call to navigate the sub-frame. 4807 * 4808 * @param strLocation Assumed to be strBaseURL + strLocation. Trailing slash should be on strBaseURL already. 4809 * @param strpageTitle Optional: Replace page title with this new value. 4810 * @constructor 4811 */ 4812CAjaxSubPageController.prototype.Navigate = function( strLocation, strPageTitle, event ) 4813{ 4814 var _this = this; 4815 var strURL = this.strBaseURL + strLocation; 4816 4817 this.elTarget.classList.add('loading'); 4818 4819 this.PaintLinks( strLocation ); 4820 4821 // Trigger the "saveform" event which we may have bound to do things. 4822 $J( 'form', this.elTarget ).trigger('saveform'); 4823 4824 $J.ajax({ 4825 url: strURL, 4826 dataType: "html", 4827 cache: true, /* Let the browser decide caching rules */ 4828 data: { 'ajax': 1 } 4829 }).done(function( result ) { 4830 4831 var elNewContents = $J( result ); 4832 4833 $J(_this.elTarget).empty(); 4834 $J(_this.elTarget).append( elNewContents ); 4835 4836 4837 _this.elTarget.classList.remove('loading'); 4838 4839 $J( elNewContents ).trigger('load'); 4840 4841 _this.InstrumentLinks( _this.elTarget ); 4842 if( strPageTitle ) 4843 document.title = strPageTitle; 4844 4845 $J('.tooltip', _this.elTarget).v_tooltip(); 4846 4847 window.history.pushState({'html':result,'title':strPageTitle, 'id': _this.strStateID}, '', strURL ); 4848 }); 4849 4850 if( event ) 4851 event.preventDefault(); 4852 4853}; 4854 4855/** 4856 * Internal event handler for the "back" button. 4857 * @param Event event 4858 * @constructor 4859 */ 4860CAjaxSubPageController.prototype.OnWindowPopState = function( event ) 4861{ 4862 // Revert to our original state if there's nothing in the stack. 4863 4864 if(event.state == null) 4865 { 4866 event.state = 5; 4867 } 4868 4869 var state = event.state || this.rgOriginalEvent; 4870
4871 if( state && state.id == this.strStateID) 4872 { 4873 var strLocation = window.location.href.substring(this.strBaseURL.length); 4874 this.PaintLinks( strLocation ); 4875 4876 var elNewContents = $J(state.html); 4877 $J(this.elTarget).empty(); 4878 $J(this.elTarget).append(elNewContents); 4879 if( state.title ) 4880 document.title = state.title; 4881 4882 $J( elNewContents ).trigger('load'); 4883 this.InstrumentLinks( elNewContents ); 4884 } 4885}; 4886 4887function SetupAnimateOnHoverImages() 4888{ 4889 var $Images = $J( '[data-animate-on-hover-src]', document ); 4890 if ( $Images.length ) 4891 { 4892 for ( var i = 0; i < $Images.length; ++i ) 4893 { 4894 var img = $J( $Images[i] ); 4895 img.data( 'static-src', img.attr( 'src') ); 4896 img.hover( 4897 function() { 4898 var thisImage = $J( this ); 4899 thisImage.attr( 'src', thisImage.data( 'animate-on-hover-src' ) ); 4900 }, 4901 function() { 4902 var thisImage = $J( this ); 4903 thisImage.attr( 'src', thisImage.data( 'static-src' ) ); 4904 } 4905 ); 4906 } 4907 } 4908} 4909 4910function BindTooltips(selector, rgOptions) 4911{ 4912 // Standard tooltips 4913 var $TextTooltips = $J( '[data-tooltip-text]', selector); 4914 if ( $TextTooltips.length ) 4915 $TextTooltips.v_tooltip( { 'tooltipClass': rgOptions.tooltipCSSClass, 'dataName': 'tooltipText', 'defaultType': 'text', 'replaceExisting': false, 'responsiveMode': window.UseSmallScreenMode && window.UseSmallScreenMode() } ); 4916 4917 var $HTMLTooltips = $J( '[data-tooltip-html]', selector); 4918 if ( $HTMLTooltips.length ) 4919 $HTMLTooltips.v_tooltip( { 'tooltipClass': rgOptions.tooltipCSSClass, 'dataName': 'tooltipHtml', 'defaultType': 'html', 'replaceExisting': false, 'responsiveMode': window.UseSmallScreenMode && window.UseSmallScreenMode() } ); 4920} 4921 4922function ShowTooltipMenuAsPopup( toolDiv ) 4923{ 4924 var $Popup = $J( toolDiv ); 4925 // detach this element and when the dialog closes re-attach to the document body 4926 $Popup.detach(); 4927 var originalPopupPosition = $Popup.css( 'position' ); 4928 $Popup.css( 'position', 'static' ); // clear possible absolute positioning 4929 $Popup.show(); 4930 4931 var dialog = ShowDialog( '', $Popup ).always( 4932 function() { 4933 // save it away again for later 4934 $Popup.hide(); 4935 $Popup.css( 'position', originalPopupPosition ); // restore positioning 4936 $J( document.body ).append( $Popup ); 4937 } 4938 ); 4939 return dialog; 4940} 4941 4942/** 4943 * Binds standard tooltips for the current site. This function also binds a mutationobserver to bind any future elements without additional work 4944 * @param strCSSClass 4945 * @constructor 4946 */ 4947var g_TooltipMutationObserver; 4948var g_bTooltipMutationObserverDisabled = false; 4949function SetupTooltips( rgOptions ) 4950{ 4951 function InnerSetupTooltips() { 4952 4953 BindTooltips(document, rgOptions); 4954 4955 if (g_TooltipMutationObserver || g_bTooltipMutationObserverDisabled) 4956 return; 4957 4958 try 4959 { 4960 var config = { 4961 attributes: true, // not clear if we actually support this, as we only look at addedNodes 4962 childList: true, 4963 subtree: true, 4964 attributeFilter: ["data-tooltip-html", "data-tooltip-text"] 4965 }; 4966 4967 var callback = function (mutationsList) 4968 { 4969 for (var i = 0; i < mutationsList.length; i++) 4970 { 4971 var mutation = mutationsList[i]; 4972 if (mutation.addedNodes && mutation.addedNodes.length) 4973 BindTooltips(mutation.addedNodes, rgOptions); 4974 } 4975 }; 4976 4977 // Create an observer instance linked to the callback function 4978 g_TooltipMutationObserver = new MutationObserver(callback); 4979 4980 // Start observing the target node for configured mutations 4981 g_TooltipMutationObserver.observe(document, config); 4982 } 4983 catch (e) 4984 { 4985 // Swallow exceptions for browsers that don't support mutationobservers 4986 } 4987 } 4988 4989 InnerSetupTooltips(); 4990 $J( window ).on( 'Responsive_SmallScreenModeToggled', InnerSetupTooltips ); 4991} 4992 4993// for perf sensitive pages 4994function DisableTooltipMutationObserver() 4995{ 4996 g_bTooltipMutationObserverDisabled = true; 4997 4998 if ( g_TooltipMutationObserver ) 4999 { 5000 g_TooltipMutationObserver.disconnect(); 5001 g_TooltipMutationObserver = null; 5002 } 5003} 5004 5005function ViewTitlesWithDescriptors( descid ) 5006{ 5007 var waitDialog = ShowBlockingWaitDialog( 'Example Products', 'Please wait while we find some example products...' ); 5008 5009 $J.get( 'https://store.steampowered.com/search/results/', 5010 { 5011 'filter' : 'examplesforcontentdescriptors', 5012 'ignore_preferences' : 1, 5013 'category1' : '992,994,998', 'descids[]' : descid, 5014 'json' : 1, 5015 } 5016 ).done( function( response ) { 5017 waitDialog.Dismiss(); 5018 5019 var content = $J( "<div>" ); 5020 content.append( $J( "<div>", { class: 'content_descriptors_examples_desc', text: response.desc } ) ); 5021 5022 if ( response.items.length != 0 ) 5023 { 5024 for ( var i = 0; i < response.items.length && i < 10; ++i ) 5025 { 5026 var item = response.items[i]; 5027 var elem = $J( "<div>", { class: 'content_descriptors_example_app' } ); 5028 elem.append( $J( "<img>", { class: 'app_logo', src: item.logo } ) ); 5029 elem.append( $J( "<div>", { class: 'app_name', text: item.name } ) ); 5030 content.append( elem ); 5031 } 5032 } 5033 else 5034 {
5035 content.append( $J( "<div>", { class: "no_items", text: 'No Products Found' } ) ); 5036 } 5037 5038 var dialog = ShowAlertDialog( 'Example Products', content ); 5039 } ); 5040} 5041 5042 5043 5044// Element appearance montior. Designed to replace jQuery.appear 5045 5046var CAppearMonitor = function( fnOnAppear, bCheckAnyIntersection ){ 5047 this.rgMonitoredElements = []; 5048 this.bRunning = false; 5049 this.unTimerFrequency = 500; 5050 this.bRegisteredWindowEvent = false; 5051 this.fnOnAppear = fnOnAppear; 5052 this.bCheckAnyIntersection = bCheckAnyIntersection || false; 5053}; 5054 5055CAppearMonitor.prototype.RegisterScrollEvent = function( elTarget ) 5056{ 5057 var instance = this; 5058 5059 $J(elTarget).on('scroll', function() 5060 { 5061 if( instance.bRunning || instance.rgMonitoredElements.length == 0 ) 5062 return; 5063 5064 instance.bRunning = true; 5065 setTimeout( instance.CheckVisibilityInternal.bind(instance), instance.unTimerFrequency ); 5066 5067 }); 5068} 5069 5070CAppearMonitor.prototype.CheckVisibility = function() 5071{ 5072 // when items are added they will schedule an immediate check. Delay for a bit 5073 if ( this.m_iCheckVisibilityRequestInterval ) 5074 { 5075 window.clearTimeout( this.m_iCheckVisibilityRequestInterval ); 5076 } 5077 5078 this.m_iCheckVisibilityRequestInterval = setTimeout( this.CheckVisibilityInternal.bind(this), this.unTimerFrequency ); 5079}; 5080 5081CAppearMonitor.prototype.CheckVisibilityInternal = function() 5082{ 5083 // Walk backwards so we can slice while we iterate 5084 for( var i=this.rgMonitoredElements.length-1; i>=0; i--) 5085 { 5086 var rgMonitored = this.rgMonitoredElements[i]; 5087 if( this.bIsElementVisible( rgMonitored.element ) ) 5088 { 5089 this.rgMonitoredElements.splice(i,1); 5090 this.fnOnAppear( rgMonitored.element ); 5091 } else if( !document.body.contains( rgMonitored.element ) ) 5092 { 5093 this.rgMonitoredElements.splice(i,1); 5094 } 5095 } 5096 5097 this.bRunning = false; 5098 this.m_iCheckVisibilityRequestInterval = undefined; 5099}; 5100 5101CAppearMonitor.prototype.bIsElementVisible = function( elElement ) 5102{ 5103 if( !elElement ) 5104 return; 5105 5106 // Check physical position vs viewport. This is likely the fastest early-out. 5107 var rectElement = elElement.getBoundingClientRect(); 5108 5109 if ( this.bCheckAnyIntersection ) 5110 { 5111 if ( rectElement.left > window.innerWidth || rectElement.right < 0 ) 5112 return false; 5113 5114 if ( rectElement.top > window.innerHeight || rectElement.bottom < 0 ) 5115 return false; 5116 } 5117 else 5118 { 5119 if ( !( rectElement.top >= 0 && rectElement.left >= 0 5120 && rectElement.bottom <= window.innerHeight && rectElement.right <= window.innerWidth ) ) 5121 return false; 5122 } 5123 5124 // Ask jQuery to compute visibility, since it knows all the edge cases. 5125 if( !$J(elElement).is(':visible')) 5126 return false; 5127 5128 return true; 5129 5130}; 5131 5132CAppearMonitor.prototype.RegisterElement = function( elTarget ) 5133{ 5134 if( $J(elTarget).data('manual-tracking' ) ) 5135 return; 5136 5137 this.rgMonitoredElements.push({ 5138 element: elTarget 5139 }); 5140 5141 // Register the window scroll event ONLY after we have something to monitor 5142 if( !this.bRegisteredWindowEvent) 5143 this.RegisterScrollEvent( window ); 5144}; 5145 5146CAppearMonitor.prototype.TrackAppearanceIfVisible = function( elTarget ) 5147{ 5148 var _this = this; 5149 window.setTimeout( function() { 5150 // Ensure we're actually in the viewport (Carousel may be scrolling out of view and calling this) 5151 // this triggers layout, so we delay this check a little bit to give the page time to settle whatever 5152 // change may currently be in-flight 5153 if( !_this.bIsElementVisible( elTarget ) ) 5154 return; 5155 5156 _this.TrackAppearance( elTarget ); 5157 }, 10); 5158}; 5159 5160CAppearMonitor.prototype.TrackAppearance = function( elTarget ) 5161{ 5162 // Find our element, splice it out if we were tracking it 5163 for( var i=this.rgMonitoredElements.length-1; i>=0; i--) 5164 { 5165 var rgMonitored = this.rgMonitoredElements[i]; 5166 if( rgMonitored.element === elTarget ) 5167 { 5168 this.rgMonitoredElements.splice(i,1); 5169 } 5170 } 5171 // Trigger it, even if we weren't actually tracking it 5172 this.fnOnAppear( elTarget ); 5173}; 5174 5175CAppearMonitor.prototype.ClearElements = function() 5176{ 5177 this.rgMonitoredElements = []; 5178} 5179 5180// gamepad navigation wrappers, the implementations of these are stored in legacy_web. 5181// once legacy web initializes, these functions will be replaced by the real thing. In the mean time, 5182// we will queue any commands and send them when ready. 5183var g_rgQueuedGamepadCommands = []; 5184var g_rgOnReadyCallbacks = []; 5185var g_bGamepadNavReady = false;
5186function RunWhenGamepadNavReady( callback ) 5187{ 5188 if ( g_bGamepadNavReady ) 5189 callback(); 5190 else 5191 g_rgOnReadyCallbacks.push( callback ); 5192} 5193window.addEventListener( 'vgp_gamepadnavready', function() { 5194 g_rgQueuedGamepadCommands.forEach( function( command ) { window[command.name].apply( window, command.args ); } ); 5195 g_rgQueuedGamepadCommands = []; 5196 5197 g_rgOnReadyCallbacks.forEach( function( fn ) { fn(); } ); 5198 g_rgOnReadyCallbacks = []; 5199}); 5200 5201function InstrumentFocusElements( element ) 5202{ 5203 g_rgQueuedGamepadCommands.push( { name: 'InstrumentFocusElements', args: arguments } ); 5204} 5205function ForceUpdateFocusElements( element ) 5206{ 5207 g_rgQueuedGamepadCommands.push( { name: 'ForceUpdateFocusElements', args: arguments } ); 5208} 5209function GPNavFocusChild( element ) 5210{ 5211 g_rgQueuedGamepadCommands.push( { name: 'GPNavFocusChild', args: arguments } ); 5212} 5213function GPOnShowingModalWindow( element ) 5214{ 5215 g_rgQueuedGamepadCommands.push( { name: 'GPOnShowingModalWindow', args: arguments } ); 5216} 5217function GPShowVirtualKeyboard( bShowKeyboard ) 5218{ 5219 g_rgQueuedGamepadCommands.push( { name: 'GPShowVirtualKeyboard', args: arguments } ); 5220} 5221function GPNavUpdateActionDescriptions( element, actionDescriptions ) 5222{ 5223 g_rgQueuedGamepadCommands.push( { name: 'GPNavUpdateActionDescriptions', args: arguments } ); 5224} 5225 5226var SetGPFocusRestoreTimeout = function(){}; // no op unless InitializeGPFocusRestoreTimeout is called 5227var nGPFocusRestoreTimeoutID = -1; 5228function InitializeGPFocusRestoreTimeout( bUseWindowOnload = true ) 5229{ 5230 window.history.replaceState( $J.extend( {}, window.history.state, { notify_focus_restore_ready: true } ), "" ); 5231 SetGPFocusRestoreTimeout = function( delay = 200 ) 5232 { 5233 if ( nGPFocusRestoreTimeoutID == 0 || ( !window.UseTabletScreenMode || !window.UseTabletScreenMode() ) ) 5234 return; 5235 5236 if ( nGPFocusRestoreTimeoutID !== -1 ) 5237 window.clearTimeout( nGPFocusRestoreTimeoutID ); 5238 5239 this.nGPFocusRestoreTimeoutID = window.setTimeout( function(){ 5240 nGPFocusRestoreTimeoutID = 0; 5241 dispatchEvent( new Event( 'focus_restore_ready' ) ); 5242 }, delay ); 5243 } 5244 5245 if ( bUseWindowOnload ) 5246 window.addEventListener( "load", function(){ SetGPFocusRestoreTimeout(); } ); 5247} 5248 5249function HandleOverlayWindowPinnedView( bPinned, bShowPinnedView ) 5250{ 5251 $J( 'body' ).toggleClass( 'OverlayWindowPinned', bPinned ); 5252 $J( 'body' ).toggleClass( 'OverlayWindowPinnedView', bShowPinnedView ); 5253} 5254 5255var g_oReactNativeSetHeader; 5256function InitReactNativeSetHeaderData( rgInitialReactNativeHeaderData, fnPostData ) 5257{ 5258 g_oReactNativeSetHeader = { 5259 rgData: rgInitialReactNativeHeaderData, 5260 fnPostData: fnPostData 5261 }; 5262 UpdateReactNativeClientHeaderData(); 5263} 5264 5265function SetReactNativeHeaderCartItemCount( nCartItemCount ) 5266{ 5267 if ( !g_oReactNativeSetHeader ) 5268 return; // not in react native client 5269 5270 if ( nCartItemCount == g_oReactNativeSetHeader.rgData['cart_item_count'] ) 5271 return; 5272 5273 if ( nCartItemCount ) 5274 g_oReactNativeSetHeader.rgData['cart_item_count'] = nCartItemCount; 5275 else 5276 delete g_oReactNativeSetHeader.rgData['cart_item_count']; 5277 5278 UpdateReactNativeClientHeaderData(); 5279} 5280 5281function UpdateReactNativeClientHeaderData() 5282{ 5283 if ( g_oReactNativeSetHeader ) 5284 { 5285 g_oReactNativeSetHeader.fnPostData( g_oReactNativeSetHeader.rgData ); 5286 } 5287} 5288
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.