PageSourceSearch

https://keziahjones.com/js/shared-global.js

js keziahjones.com collected 2026-09-26 20:32:29 UTC 158,468 bytes, 5,379 lines download raw bytes

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

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.