PageSourceSearch

https://www.steamvr.com/en/public/shared/javascript/shared_global.js?v=7x550RMBik-V&_cdn=fastly

js steamvr.com collected 2026-09-24 22:59:58 UTC 152,804 bytes, 5,288 lines download raw bytes

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, '&amp;' ).replace( /["]/g, '&quot;' ).replace( /[']/g, '&#39;' ).replace( /</g, '&lt;').replace( />/g, '&gt;');
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.