PageSourceSearch

https://keziahjones.com/js/game.js

js keziahjones.com collected 2026-09-26 20:32:37 UTC 101,631 bytes, 2,638 lines download raw bytes

1"use strict";
2
3
4function ShowEmbedWidget( )
5{
6	$J('#widget_create').show();
7	$J('#widget_finished').hide();
8
9	var $Content = $J('#EmbedModal');
10	$Content.detach();
11	$Content.show();
12
13	var deferred = new jQuery.Deferred();
14	var fnOK = function() { deferred.resolve(); };
15
16	var Modal = _BuildDialog( "\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e27\u0e34\u0e14\u0e40\u0e08\u0e47\u0e15\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e19\u0e33\u0e44\u0e1b\u0e1d\u0e31\u0e07\u0e15\u0e31\u0e27", $Content, [], fnOK, {} );
17	deferred.always( function() { Modal.Dismiss(); } );
18	Modal.Show();
19
20	// attach the deferred's events to the modal
21	deferred.promise( Modal );
22
23	Modal.always(
24		function() {
25			// save it away again for later
26			$Content.hide();
27			$J(document.body).append( $Content );
28		}
29	);
30}
31
32function CreateWidget( nAppId )
33{
34	$J('#widget_create').hide();
35
36	var nSubId = $J('input[name=w_subid]').val();
37	if( !nSubId )
38		nSubId = $J('input:radio[name=w_rsubid]:checked').val();
39
40	var strDesc = $J('textarea[name=w_text]').val();
41
42	var strWidgetURL;
43	if( nSubId )
44		strWidgetURL = 'https://store.steampowered.com/widget/' + nAppId + '/' + nSubId + '/';
45	else
46		strWidgetURL = 'https://store.steampowered.com/widget/' + nAppId + '/';
47
48	if( strDesc )
49		strWidgetURL = strWidgetURL + '?t=' + encodeURIComponent( strDesc );
50
51	var strWidgetCode = '<iframe src="' + strWidgetURL + '" frameborder="0" width="646" height="190"></iframe>';
52
53	var $iframe = $J(strWidgetCode);
54
55	$J('#widget_container').empty();
56	$J('#widget_container').append($iframe);
57	$J('#widget_code').val( strWidgetCode );
58
59	$J('#widget_finished').show();
60}
61
62// appid_for_follow can be different than store_appid in cases such as when viewing a standalone demo page where we want the follow button to follow the parent app
63function InitQueueControls( store_appid, appid_for_follow, next_in_queue_appid, snr )
64{
65	var $FollowBtn = $J('.queue_control_button.queue_btn_follow .queue_btn_inactive');
66	var $UnFollowBtn = $J('.queue_control_button.queue_btn_follow .queue_btn_active');
67	var $IgnoreBtn = $J('.queue_control_button.queue_btn_ignore .queue_btn_inactive');
68	var $UnIgnoreBtn = $J('.queue_control_button.queue_btn_ignore .queue_btn_active');
69
70	var $IgnoreMenuBtn = $J('#queue_ignore_menu_arrow');
71	var $IgnoreMenu = $J('#queue_btn_ignore_menu');
72	var $IgnoreMenuContent = $J('#ignore_menu_flyout > div');
73	var $IgnoreMenuOption_NotInterested = $J('#queue_ignore_menu_option_not_interested' );
74	var $IgnoreMenuOption_OwnedElsewhere = $J('#queue_ignore_menu_option_owned_elsewhere' );
75
76	var funcUpdateIgnoredReasonClasses = function() {
77		$IgnoreMenu.removeClass( "not_interested owned_elsewhere" );
78		var curIgnoredReason = $IgnoreMenu.data( "ignoredreason" );
79		if ( curIgnoredReason !== undefined )
80		{
81			$IgnoreMenuBtn.removeClass( "queue_btn_inactive" );
82			$IgnoreMenuBtn.addClass( "queue_btn_active" );
83			switch( curIgnoredReason )
84			{
85				case 0:
86					$IgnoreMenu.addClass( "not_interested" );
87					break;
88				case 2:
89					$IgnoreMenu.addClass( "owned_elsewhere" );
90					break;
91			}
92		}
93	};
94	funcUpdateIgnoredReasonClasses();
95
96	var funcIgnoreGame = function( ignore_reason ) {
97		$J.post( 'https://store.steampowered.com/recommended/ignorerecommendation/', {
98			sessionid: g_sessionID,
99			appid: store_appid,
100			snr: snr,
101			ignore_reason: ignore_reason
102		}).done( function( data ) {
103			const bWasFocused = document.activeElement === $IgnoreBtn[0];
104			$IgnoreBtn.hide();
105			$UnIgnoreBtn.show();
106			$IgnoreMenuBtn.attr( 'aria-expanded', false );
107			if ( bWasFocused )
108			{
109				$UnIgnoreBtn.focus();
110			}
111
112			$IgnoreMenu.data( "ignoredreason", ignore_reason );
113			funcUpdateIgnoredReasonClasses();
114
115			GDynamicStore.InvalidateCache();
116			if ( data && data.nSaleTaskCompleted ) { NewStickerPackModal( 'ทำเครื่องหมายบางสิ่งเป็นไม่สนใจ' ); } // SummerSale2017
117		}).fail( function( jqXHR ) {
118			ShowAlertDialog( 'ละเว้นแอป', 'ตรวจพบปัญหาในการบันทึกการเปลี่ยนแปลงของคุณ โปรดลองใหม่อีกครั้งภายหลัง' );
119		});
120	};
121
122	$IgnoreBtn.click( function() {
123		funcIgnoreGame( 0 );
124	} );
125	$IgnoreMenuOption_NotInterested.click( function() {
126		var curIgnoredReason = $IgnoreMenu.data( "ignoredreason" );
127		if ( curIgnoredReason === 0 )
128		{
129			$UnIgnoreBtn.click();
130			$IgnoreMenuBtn.attr( 'aria-expanded', false );
131		}
132		else
133		{
134			funcIgnoreGame( 0 );
135		}
136	} );
137	$IgnoreMenuOption_OwnedElsewhere.click( function() {
138		var curIgnoredReason = $IgnoreMenu.data( "ignoredreason" );
139		if ( curIgnoredReason === 2 )
140		{
141			$UnIgnoreBtn.click();
142			$IgnoreMenuBtn.attr( 'aria-expanded', false );
143		}
144		else
145		{
146			funcIgnoreGame( 2 );
147		}
148	} );
149
150	$IgnoreMenuBtn.click( function() {
151		const bIsExpanded = $IgnoreMenuBtn.attr( 'aria-expanded' ) == "true";
152		$IgnoreMenuBtn.attr( 'aria-expanded', !bIsExpanded );
153		if ( !bIsExpanded )
154		{
155			$IgnoreMenuContent[0].children[0].focus();
156		}
157	} );
158	$IgnoreMenuContent.focusout( function(e) {
159		if ( !$IgnoreMenuContent[0].contains( e.relatedTarget ) )
160		{
161			$IgnoreMenuBtn.attr( 'aria-expanded', false );
162		}
163	} );
164
165	$UnIgnoreBtn.click( function() {
166		$J.post( 'https://store.steampowered.com/recommended/ignorerecommendation/', {
167			sessionid: g_sessionID,
168			appid: store_appid,
169			snr: snr,
170			remove: 1
171		}).done( function() {
172			const bWasFocused = document.activeElement === $UnIgnoreBtn[0];
173			$IgnoreBtn.show();
174			$UnIgnoreBtn.hide();
175			if ( bWasFocused )
176			{
177				$IgnoreBtn.focus();
178			}
179			$IgnoreMenu.removeClass( "not_interested owned_elsewhere" );
180			$IgnoreMenuBtn.removeClass( "queue_btn_active" );
181			$IgnoreMenuBtn.addClass( "queue_btn_inactive" );
182			GDynamicStore.InvalidateCache();
183		}).fail( function() {
184			ShowAlertDialog( 'ละเว้นแอป', 'ตรวจพบปัญหาในการบันทึกการเปลี่ยนแปลงของคุณ โปรดลองใหม่อีกครั้งภายหลัง' );
185		});
186	});
187
188	$FollowBtn.click( function() {
189		$J.post( 'https://store.steampowered.com/explore/followgame/', {
190			sessionid: g_sessionID,
191			appid: appid_for_follow
192		}).done( function() {
193			const bWasFocused = document.activeElement === $FollowBtn[0];
194			$FollowBtn.hide();
195			$UnFollowBtn.show();
196			if ( bWasFocused )
197			{
198				$UnFollowBtn.focus();
199			}
200		}).fail( function() {
201			ShowAlertDialog( 'ติดตาม', 'ตรวจพบปัญหาในการบันทึกการเปลี่ยนแปลงของคุณ โปรดลองใหม่อีกครั้งภายหลัง' );
202		});
203	});
204
205	$UnFollowBtn.click( function() {
206		$J.post( 'https://store.steampowered.com/explore/followgame/', {
207			sessionid: g_sessionID,
208			appid: appid_for_follow,
209			unfollow: 1
210		}).done( function() {
211			const bWasFocused = document.activeElement === $UnFollowBtn[0];
212			$FollowBtn.show();
213			$UnFollowBtn.hide();
214			if ( bWasFocused )
215			{
216				$FollowBtn.focus();
217			}
218		}).fail( function() {
219			ShowAlertDialog( 'ติดตาม', 'ตรวจพบปัญหาในการบันทึกการเปลี่ยนแปลงของคุณ โปรดลองใหม่อีกครั้งภายหลัง' );
220		});
221	});
222
223	// discovery queue settings dialog
224	var bQueueIsValid = true;
225	var $NextInQueueBtn = $J('.btn_next_in_queue_trigger');
226
227	$NextInQueueBtn.click( function() {
228		if ( bQueueIsValid )
229			$J('#next_in_queue_form').submit();
230		else
231			window.location = 'https://store.steampowered.com/explore/startnew';
232	});
233
234	$J('a.dq_settings_link').click( function() {
235		CDiscoveryQueue.ShowCustomizeDialog( function( data ) {
236			if ( data && data.queue && ( data.queue.indexOf( store_appid ) == -1 ||
237				( next_in_queue_appid && data.queue.indexOf( next_in_queue_appid ) == -1 ) ) )
238			{
239				ShowConfirmDialog( 'ปรับแต่งคิวค้นพบของคุณ', 'ทางเราได้สร้างคิวค้นพบใหม่โดยการเพิ่มการปรับแต่งของคุณ คุณต้องการทำอะไร?',
240					'เริ่มสำรวจคิวใหม่','อยู่ที่นี่'
241				).done( function() {
242					window.location = 'https://store.steampowered.com/explore/startnew/' + g_eDiscoveryQueueType + '/';
243				});
244				bQueueIsValid = false;
245			}
246			else
247			{
248				bQueueIsValid = true;
249			}
250		}, g_eDiscoveryQueueType );
251	} );
252}
253
254/** OK button handler for collapsed `game_page_autocollapse` sections. Triggers expansion of the content. */
255function HandleAutoCollapseOKButton( event )
256{
257	var $Container = $J( event.currentTarget );
258	if ( $Container.hasClass( 'collapsed' ) )
259	{
260		var $Content = $Container.find( '.game_page_autocollapse' );
261		$Content.trigger('gamepage_autocollapse_expand');
262		return true;
263	}
264
265	return false;
266}
267
268
269function InitAutocollapse()
270{
271	$J('.game_page_autocollapse').each( function() {
272		var content = this;
273		var $Content = $J(content);
274		$Content.wrap( $J('<div/>', {'class': 'game_page_autocollapse_ctn' } ) );
275
276		var $Container = $Content.parent();
277
278		var $ReadMore = $J('<div/>', {'class': 'game_page_autocollapse_readmore' }).text( 'อ่านเพิ่มเติม' );
279		var $Fade = $J('<div/>', {'class': 'game_page_autocollapse_fade' } ).append( $ReadMore );
280		$Container.append( $Fade );
281
282		var nInterval = 0;
283		var nMaxHeight = parseInt( $Content.css('max-height') );
284		var bMaxHeightSet = true;
285
286		$Content.on( 'gamepage_autocollapse_expand', function() {
287			if ( $Container.hasClass( 'collapsed' ) )
288			{
289				// Stop suppressing gamepad focus on the expandable content now that it is expanded
290				$Content.data( 'gpFocusDisabled', false );
291				$Container.removeClass( 'collapsed' );
292				$Container.addClass( 'expanded' );
293
294				if ( bMaxHeightSet )
295				{
296					$Content.animate( {'max-height': content.scrollHeight + 20 + 'px'}, 'fast', null, function() { $Content.css('max-height', 'none' ); } );
297				}
298				window.clearInterval( nInterval );
299			}
300		});
301
302		$ReadMore.click( function() { $Content.trigger('gamepage_autocollapse_expand'); } );
303
304		var fnCheckHeight = function ()	{
305			if ( content.scrollHeight > nMaxHeight + 30 )
306			{
307				$Content.css( 'max-height', nMaxHeight + 'px' );
308				$Container.addClass( 'collapsed' );
309				window.clearInterval( nInterval );
310				bMaxHeightSet = true;
311
312				// The content div becomes a panel with focus temporarily disabled. This allows the container Panel to get focus and handle
313				// expansion via its OK button activation. Once expanded the content panel lets focus pass through to its children (if there are any).
314				if ( $Content.attr( 'data-panel' ) === undefined )
315					$Content.attr( 'data-panel', '[]' );
316
317				$Content.data( 'gpFocusDisabled', true );
318				$Container.attr( 'data-panel', '{"onOKButton":"HandleAutoCollapseOKButton( event )","focusableIfEmpty":true}' );
319			}
320			else if ( bMaxHeightSet )
321			{
322				$Content.css( 'max-height', 'none' );
323				bMaxHeightSet = false;
324			}
325		};
326
327		nInterval = window.setInterval( fnCheckHeight, 250 );
328		fnCheckHeight();
329
330	});
331}
332
333function AddScrollToAccessoriesBlock()
334{
335	function scrollToAccessoriesIfNeeded()
336	{
337		if ( window.location.hash == "#accessories" )
338		{
339			document.querySelector( "#accessory_block" ).scrollIntoView( {
340				behavior: 'smooth',
341				block: 'center',
342				inline: 'center',
343			} );
344
345			document.querySelector( "#accessory_block" ).classList.add( "highlighted" );
346		}
347		else
348		{
349			document.querySelector( "#accessory_block" ).classList.remove( "highlighted" );
350		}
351	}
352
353	window.addEventListener( "hashchange", scrollToAccessoriesIfNeeded, false );
354	window.addEventListener( "load", scrollToAccessoriesIfNeeded, false );
355}
356
357function ShowEULA( elLink )
358{
359	if ( window.UseGamepadScreenMode && window.UseGamepadScreenMode() )
360	{
361		// it's a better user experience on Deck if we navigate to the EULA instead of opening a new window
362		window.location = elLink.href;
363	}
364	else
365	{
366		var win = window.open( elLink.href,'eula','height=584,width=648,resize=yes,scrollbars=yes');
367		win && win.focus();
368	}
369}
370
371
372// formerly user_reviews_store.js
373
374var g_recommendationContents = [ 'friend', 'summary', 'recent_short', 'all', 'positive', 'negative', 'recent', 'funny' ];
375
376
377function OnRecommendationVotedUp( recommendationid )
378{
379	for ( var i = 0; i < g_recommendationContents.length; ++i )
380	{
381		$J( '#RecommendationVoteUpBtn' + g_recommendationContents[i] + recommendationid ).addClass( 'btn_active' );
382		$J( '#RecommendationVoteDownBtn' + g_recommendationContents[i] + recommendationid ).removeClass( "btn_active" );
383		$J( '#RecommendationVoteTagBtn' + g_recommendationContents[i] + recommendationid + '_' + 1 ).removeClass( 'btn_active' );
384	}
385}
386
387function OnRecommendationVotedDown( recommendationid )
388{
389	for ( var i = 0; i < g_recommendationContents.length; ++i )
390	{
391		$J( '#RecommendationVoteUpBtn' + g_recommendationContents[i] + recommendationid ).removeClass( 'btn_active' );
392		$J( '#RecommendationVoteDownBtn' + g_recommendationContents[i] + recommendationid ).addClass( "btn_active" );
393		$J( '#RecommendationVoteTagBtn' + g_recommendationContents[i] + recommendationid + '_' + 1 ).removeClass( 'btn_active' );
394	}
395}
396
397function OnRecommendationVotedTag( recommendationid, tagID, bRateUp )
398{
399	for ( var i = 0; i < g_recommendationContents.length; ++i )
400	{
401		if ( bRateUp )
402		{
403			$J( '#RecommendationVoteTagBtn' + g_recommendationContents[i] + recommendationid + '_' + tagID ).addClass( 'btn_active' );
404			$J( '#RecommendationVoteUpBtn' + g_recommendationContents[i] + recommendationid ).removeClass( 'btn_active' );
405			$J( '#RecommendationVoteDownBtn' + g_recommendationContents[i] + recommendationid ).removeClass( "btn_active" );
406		}
407		else
408		{
409			$J( '#RecommendationVoteTagBtn' + g_recommendationContents[i] + recommendationid + '_' + tagID ).removeClass( 'btn_active' );
410		}
411	}
412}
413
414function OnRecommendationAward( recommendationid, award )
415{
416	for ( var i = 0; i < g_recommendationContents.length; ++i )
417	{
418		var review = $J( "#ReviewContent" + g_recommendationContents[i] + recommendationid );
419		var rewardsCtn = review.find( ".review_award_ctn" );
420		if ( rewardsCtn.length != 0 )
421		{
422			var bFoundExisting = false;
423			var rewards = rewardsCtn.find( ".review_award" );
424			for ( var j = 0; j < rewards.length; ++j )
425			{
426				var reward = $J( rewards[j] );
427				if ( reward.data( "reaction" ) == award )
428				{
429					bFoundExisting = true;
430
431					var count = parseInt( reward.data( "reactioncount" ) );
432					var countElem = reward.find( ".review_award_count" );
433					countElem.text( count + 1 );
434
435					reward.data( "reactioncount", count + 1 );
436					break;
437				}
438			}
439
440			if ( !bFoundExisting )
441			{
442				var reward = $J( "<div>", { class: "review_award" } );
443				var img = $J( "<img>", { class: "review_award_icon tooltip", src: "https://store.fastly.steamstatic.com/public/images/loyalty/reactions/still/" + award + ".png" } );
444				reward.append( img );
445
446				var countElem = $J( "<span>", { class: "review_award_count", text: "1" } );
447				reward.append( countElem );
448
449				reward.data( "reaction", award );
450				reward.data( "reactioncount", 1 );
451
452				rewardsCtn.append( reward );
453				review.find( ".vote_info" ).show();
454			}
455		}
456	}
457}
458
459function RequestCurrentUserRecommendationVotes( recommendationIDs )
460{
461	if ( recommendationIDs.length == 0 )
462	{
463		return;
464	}
465
466	$J.post( 'https://store.steampowered.com//userreviews/ajaxgetvotes/', {
467			'recommendationids' : recommendationIDs
468		}
469	).done( function( response ) {
470			if ( response.success == 1 )
471			{
472				var votes = response.votes;
473				for ( var i = 0; i < votes.length; ++i )
474				{
475					var vote = votes[i];
476					if ( vote.voted_up )
477					{
478						OnRecommendationVotedUp( vote.recommendationid );
479					}
480					else if ( vote.voted_down )
481					{
482						OnRecommendationVotedDown( vote.recommendationid );
483					}
484					if ( vote.voted_funny )
485					{
486						OnRecommendationVotedTag( vote.recommendationid, 1, true );
487					}
488				}
489			}
490		} );
491}
492
493function UserReviewVoteUp( bLoggedIn, strLoginURL, id )
494{
495	if ( !bLoggedIn )
496	{
497		var dialog = ShowConfirmDialog( 'ข้อผิดพลาด', 'คุณต้องเข้าสู่ระบบเพื่อกระทำการนั้น', 'เข้าสู่ระบบ' );
498		dialog.done( function() {
499			top.location.href = strLoginURL;
500		} );
501		return;
502	}
503
504	UserReview_Rate( id, true, 'https://store.steampowered.com/',
505		function( rgResults ) {
506			if ( rgResults.nSaleTaskCompleted ) { NewStickerPackModal( 'ทำเครื่องหมายบทวิจารณ์ว่าเป็นประโยชน์ ... หรือไม่เป็น');}
507			OnRecommendationVotedUp( id );
508		}
509	);
510}
511
512function UserReviewVoteDown( bLoggedIn, strLoginURL, id )
513{
514	if ( !bLoggedIn )
515	{
516		var dialog = ShowConfirmDialog( 'ข้อผิดพลาด', 'คุณต้องเข้าสู่ระบบเพื่อกระทำการนั้น', 'เข้าสู่ระบบ' );
517		dialog.done( function() {
518			top.location.href = strLoginURL;
519		} );
520		return;
521	}
522
523	UserReview_Rate( id, false, 'https://store.steampowered.com/',
524		function( rgResults ) {
525			if ( rgResults.nSaleTaskCompleted ) { NewStickerPackModal( 'ทำเครื่องหมายบทวิจารณ์ว่าเป็นประโยชน์ ... หรือไม่เป็น');}
526			OnRecommendationVotedDown( id );
527		}
528	);
529}
530
531function UserReviewVoteTag( bLoggedIn, strLoginURL, id, tagID, elemID )
532{
533	if ( !bLoggedIn )
534	{
535		var dialog = ShowConfirmDialog( 'ข้อผิดพลาด', 'คุณต้องเข้าสู่ระบบเพื่อกระทำการนั้น', 'เข้าสู่ระบบ' );
536		dialog.done( function() {
537			top.location.href = strLoginURL;
538		} );
539		return;
540	}
541
542	var elem = $J( '#' + elemID );
543	var bRateUp = !elem.hasClass( 'btn_active' );
544	UserReview_VoteTag( id, tagID, bRateUp, 'https://store.steampowered.com/',
545		function( rgResults ) {
546			OnRecommendationVotedTag( id, tagID, bRateUp );
547		}
548	);
549}
550
551function UserReviewShowMore( id, context )
552{
553	$J('#ReviewContent'+context+id).parent().removeClass('partial');
554	$J('#ReviewContent'+context+id).parent().addClass('expanded');
555
556	// GamepadUI: Enable links in the review body as focus targets once
557	// the review is expanded.
558	$J('#ReviewContent'+context+id).find( 'div.content a' ).data( 'gpFocusDisabled', false );
559}
560
561function UpdateDateFilterText( date_filter_text )
562{
563	var dateFilterText = $J( "#review_selected_histogram_date_range_text" );
564	dateFilterText.text( date_filter_text );
565	dateFilterText.show();
566}
567
568function LoadMoreReviews( appid, cursor, dayRange, startDate, endDate, context )
569{
570	$J( "#ViewAllReviews" + context ).remove();
571	$J( "#LoadMoreReviews" + context ).remove();
572	$J( "#LoadingMoreReviews" + context ).show();
573
574	var container = $J( "#Reviews_" + context );
575
576	var reviewType = $J('input[name="review_type"]:checked').val();
577	var purchaseType = $J('input[name="purchase_type"]:checked').val();
578	var language = $J('input[name="review_language"]:checked').val();
579	var filterOfftopicActivity = $J( "#reviews_offtopic_activity_checkbox" ).is( ":checked" ) ? 1 : 0;
580	var dateRangeType = $J('input[name="review_date_range"]:checked').val();
581	var summaryNumPositiveReviews = $J( "#review_summary_num_positive_reviews" ).val();
582	var summaryNumReviews = $J( "#review_summary_num_reviews" ).val();
583	var playtimeFilterMin = $J( "#app_reviews_playtime_range_min" ).val();
584	var playtimeFilterMax = $J( "#app_reviews_playtime_range_max" ).val();
585	var playtimeType = $J( 'input[name="review_playtime_type"]:checked' ).val();
586	var topics = [];
587	$J( 'input[name="review_topic"]:checked' ).each( ( index, elem ) =>
588	{
589		topics.push( $J( elem ).val() );
590	});
591	// hardware
592	let hardwareOS = $J( 'input[name="review_hardware_os"]:checked' ).val();
593	let hardwareCPU = $J( 'input[name="review_hardware_cpu_name"]:checked' ).val();
594	let hardwareGPU = $J( 'input[name="review_hardware_gfx_adapter_description"]:checked' ).val();
595	let hardwareDeviceType = $J( 'input[name="review_hardware_gaming_device_type"]:checked' ).val();
596
597	var filteredReviewScore = $J( "#user_reviews_filter_score" );
598	filteredReviewScore.removeClass( "visible" );
599
600	$J.get( 'https://store.steampowered.com/appreviews/' + appid,{
601				'use_review_quality': $J('input[name="use_review_quality"]').is( ":checked" ) ? 1 : 0,
602				'cursor' : cursor,
603		'day_range' : dayRange,
604		'start_date' : startDate,
605		'end_date' : endDate,
606		'date_range_type' : dateRangeType,
607		'filter' : context,
608		'language' : language,
609		'l' : 'thai',
610		'review_type' : reviewType,
611		'purchase_type' : purchaseType,
612		'playtime_filter_min' : playtimeFilterMin,
613		'playtime_filter_max' : playtimeFilterMax,
614		'playtime_type' : playtimeType,
615		'topics' : topics,
616		'filter_offtopic_activity' : filterOfftopicActivity,
617		'summary_num_positive_reviews' : summaryNumPositiveReviews,
618		'summary_num_reviews' : summaryNumReviews,
619		'hardware_os' : hardwareOS,
620		'hardware_cpu' : hardwareCPU,
621		'hardware_gpu' : hardwareGPU,
622		'hardware_device_type' : hardwareDeviceType,
623	}).done( function( data ) {
624
625		RecordAJAXPageView( this.url );
626
627		if ( data.success == 1 )
628		{
629			$J( "#Reviews_loading" ).hide();
630			$J( "#Reviews_" + context ).show();
631
632			if ( cursor == "" || cursor == "*" )
633			{
634				var filteredReviewScore = $J( "#user_reviews_filter_score" );
635				if ( data.review_score )
636				{
637					filteredReviewScore.addClass( "visible" );
638					filteredReviewScore.html( data.review_score );
639				}
640				else
641				{
642					filteredReviewScore.removeClass( "visible" );
643				}
644			}
645			$J( "#LoadingMoreReviews" + context ).remove();
646
647			// remove duplicates
648			var recommendationIDs = [];
649			var temp = $J('<div></div>');
650			temp.append( data.html );
651			for ( var i = 0; i < data.recommendationids.length; ++i )
652			{
653				var recommendationid = data.recommendationids[i];
654				var elemID = "#ReviewContent" + context + recommendationid;
655				if ( $J( elemID ).length != 0 )
656				{
657					temp.find( elemID ).parent().remove();
658				}
659				else
660				{
661					recommendationIDs.push( recommendationid );
662				}
663			}
664
665			if ( data.date_filter_text )
666			{
667				UpdateDateFilterText( data.date_filter_text );
668			}
669
670			// all dupes, request more
671			if ( data.recommendationids.length != 0 && recommendationIDs.length == 0 && data.cursor != '*' )
672			{
673				LoadMoreReviews( appid, data.cursor, data.dayrange, data.start_date, data.end_date, context );
674			}
675			else if ( recommendationIDs.length != 0 )
676			{
677				container.append( temp.children() );
678				CollapseLongReviews();
679				RequestCurrentUserRecommendationVotes( recommendationIDs );
680			}
681
682			// if customer is on a mobile size screen make content changes in the reviews section
683			ReparentReviewsForSmallScreens();
684		}
685		else
686		{
687			ShowAlertDialog( 'ข้อผิดพลาด', 'ตรวจพบข้อผิดพลาดในการพยายามประมวลผลคำร้องของคุณ:  ' + data.error );
688			$J( "#Reviews_" + context ).show();
689			$J( "#LoadingMoreReviews" + context ).remove();
690		}
691	} );
692}
693
694function SelectReviews( appid, context, reviewDayRange, startDate, endDate, forceClear )
695{
696	$J( "#ReviewsTab_summary" ).removeClass( "active" );
697	$J( "#ReviewsTab_all" ).removeClass( "active" );
698	$J( "#ReviewsTab_recent" ).removeClass( "active" );
699	$J( "#ReviewsTab_positive" ).removeClass( "active" );
700	$J( "#ReviewsTab_negative" ).removeClass( "active" );
701	$J( "#ReviewsTab_funny" ).removeClass( "active" );
702	$J( "#ReviewsTab_" + context ).addClass( "active" );
703
704	$J( "#Reviews_summary" ).hide();
705	$J( "#Reviews_all" ).hide();
706	$J( "#Reviews_recent" ).hide();
707	$J( "#Reviews_positive" ).hide();
708	$J( "#Reviews_negative" ).hide();
709	$J( "#Reviews_funny" ).hide();
710	$J( "#Reviews_loading" ).show();
711
712	var container = $J( "#Reviews_" + context );
713	if ( forceClear )
714	{
715		container.empty()
716	}
717	if ( container.children().length == 0 )
718	{
719		LoadMoreReviews( appid, "*", reviewDayRange, startDate, endDate, context );
720	}
721}
722
723function FilterReviewsToGraph( bCountAllReviews, startDate, endDate )
724{
725	if ( bCountAllReviews )
726	{
727		$J('#purchase_type_all').attr( 'checked', true );
728	}
729	else
730	{
731		$J('#purchase_type_steam').attr( 'checked', !bCountAllReviews );
732	}
733
734	$J('#review_start_date').val( startDate );
735	$J('#review_end_date').val( endDate );
736	if ( !$J('#review_date_range_histogram').attr( 'checked' ) && !$J('#review_date_range_exclude_histogram').attr( 'checked') )
737	{
738		$J('#review_date_range_histogram').attr('checked', true);
739	}
740	$J('#review_date_range_histogram').attr( 'disabled', false );
741	$J('#review_date_range_exclude_histogram').attr( 'disabled', false );
742	ShowFilteredReviews();
743}
744
745function FilterReviewsGraph( bCountAllReviews, startDate, endDate, bExclude )
746{
747	if ( bCountAllReviews )
748	{
749		$J('#purchase_type_all').attr( 'checked', true );
750	}
751	else
752	{
753		$J('#purchase_type_steam').attr( 'checked', !bCountAllReviews );
754	}
755
756	$J('#review_start_date').val( startDate );
757	$J('#review_end_date').val( endDate );
758
759	$J('#review_date_range_histogram').attr( 'disabled', false );
760	$J('#review_date_range_exclude_histogram').attr( 'disabled', false );
761
762	if ( bExclude )
763	{
764		$J('#review_date_range_exclude_histogram').attr( 'checked', true );
765	}
766	else
767	{
768		$J('#review_date_range_histogram').attr( 'checked', true );
769	}
770
771	ShowFilteredReviews();
772}
773
774function ClearReviewTypeFilter()
775{
776	$J('#review_type_all').attr( 'checked', true );
777	ShowFilteredReviews();
778}
779
780function ClearReviewPurchaseTypeFilter()
781{
782	$J('#purchase_type_all').attr( 'checked', true );
783	ShowFilteredReviews();
784}
785
786function ClearReviewLanguageFilter()
787{
788	$J('#review_language_all').attr( 'checked', true );
789	ShowFilteredReviews();
790}
791
792function ClearReviewDateRangeFilter()
793{
794	$J('#review_date_range_all').attr( 'checked', true );
795	ClearReviewDateFilter();
796}
797
798function ClearHardwareFilter( category )
799{
800	$J( 'input[name="review_' + category + '"][value="all"]' ).prop('checked', true);
801	ShowFilteredReviews();
802}
803
804function EditUserReviewScorePreference()
805{
806	if ( g_AccountID == 0 )
807	{
808		ShowAlertDialog( 'ข้อผิดพลาด', 'คุณต้องเข้าสู่ระบบเพื่อกระทำการนั้น' );
809		return;
810	}
811
812	$J.post(
813		'https://store.steampowered.com/account/edituserreviewscorepreference/', { sessionid: g_sessionID }
814	).done( function( response ) {
815		var dialogContent = $J( response.html );
816		var dialog = ShowAlertDialog( 'การตั้งค่าคะแนนบทวิจารณ์', dialogContent, 'ยกเลิก' );
817	});
818}
819
820function SetUserReviewScorePreference( pref )
821{
822	if ( g_AccountID == 0 )
823	{
824		ShowAlertDialog( 'ข้อผิดพลาด', 'คุณต้องเข้าสู่ระบบเพื่อกระทำการนั้น' );
825		return;
826	}
827
828	var rgData = {
829		preference: pref,
830		sessionid : g_sessionID
831	};
832	$J.post( 'https://store.steampowered.com/account/saveuserreviewscorepreference', rgData ).done(
833		function( json )
834		{
835			var h = window.location.href.substr( 0, window.location.href.indexOf('#') );
836			window.location.href = h + '#app_reviews_hash';
837			window.location.reload( true );
838		}
839	).fail(
840		function( json )
841		{
842			ShowAlertDialog( "\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14", "\u0e01\u0e32\u0e23\u0e1b\u0e23\u0e3
8421\u0e1a\u0e41\u0e15\u0e48\u0e07\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e1a\u0e31\u0e19\u0e17\u0e36\u0e01 \u0e42\u0e1b\u0e23\u0e14\u0e25\u0e2d\u0e07\u0e43\u0e2b\u0e21\u0e48\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07\u0e20\u0e32\u0e22\u0e2b\u0e25\u0e31\u0e07" );
843		}
844	);
845}
846
847function IntervalDistance( min1, max1, min2, max2 )
848{
849	return Math.max( 0, Math.max( min2 - max1, min1 - max2 ) );
850}
851
852function DrawPastEvents( flotRollup, rgPastEvents )
853{
854	var ctx = flotRollup.getCanvas().getContext("2d");
855	var rollupPoints = flotRollup.getData();
856	var seriesPositive = rollupPoints[0];
857	var axes = flotRollup.getAxes();
858	var dateOptions = { day: 'numeric', month: 'long', year: 'numeric', timeZone: "UTC" };
859
860	for ( var i = 0; i < rgPastEvents.length; ++i )
861	{
862		var event = rgPastEvents[i];
863		var midDate = ( event.start_date + ( event.end_date - event.start_date ) / 2 ) * 1000;
864
865		var o = flotRollup.pointOffset( { x: midDate, y: axes.yaxis.max } );
866
867		var topY = seriesPositive.yaxis.p2c( event.recommendations_up );
868
869		var eventIconID = flotRollup.getPlaceholder()[0].id + "_review_anomaly_" + event.start_date;
870		var eventIcon = $J( "#" + eventIconID );
871		var newStyle = {
872			left: (o.left - 6) + 'px',
873			top: (o.top - 10) + 'px'
874		};
875
876		if ( eventIcon.length == 0 )
877		{
878			eventIcon = $J( '<div/>', { id: eventIconID, class: 'review_event_graph_icon', style: JSON.stringify( newStyle ), html: '*' } );
879			eventIcon.data( 'startDate', event.start_date );
880			eventIcon.data( 'endDate', event.end_date );
881			var startDate = new Date( event.start_date * 1000 );
882			var endDate = new Date( event.end_date * 1000 );
883			var strToolTip = 'ตรวจพบกิจกรรมบทวิจารณ์นอกเรื่อง: <br><br>' +	startDate.toLocaleDateString( undefined, dateOptions ) + " - " + endDate.toLocaleDateString( undefined, dateOptions ) + '<br><br>คลิกสำหรับรายละเอียดเพิ่มเติม';
884			eventIcon.data( 'tooltipContent', strToolTip );
885			eventIcon.v_tooltip();
886			flotRollup.getPlaceholder().append( eventIcon );
887		}
888		else
889		{
890			eventIcon.css( newStyle );
891		}
892
893		o.top += 10;
894
895		ctx.beginPath();
896		ctx.moveTo(o.left-1, o.top);
897		ctx.lineTo(o.left, topY);
898		ctx.lineTo(o.left+1, o.top);
899		ctx.lineTo(o.left-1, o.top);
900		ctx.fillStyle = "#fff";
901		ctx.fill();
902	}
903}
904
905function AddOrUpdatePastEvent( rgPastEvents, event )
906{
907	for ( var i = 0; i < rgPastEvents.length; ++i )
908	{
909		if ( rgPastEvents[i].start_date == event.start_date )
910		{
911			rgPastEvents[i].recommendations_up = Math.max( rgPastEvents[i].recommendations_up, event.recommendations_up );
912			return;
913		}
914	}
915
916	rgPastEvents.push( { start_date: event.start_date, end_date: event.end_date, recommendations_up: event.recommendations_up } );
917}
918
919function BuildReviewHistogram()
920{
921	if( $J( "#review_histograms_container" ).length == 0 )
922		return;
923
924	var appid = $J( "#review_appid" ).val();
925
926	var review_score_preference = 0;
927	if ( typeof GDynamicStore != 'undefined' )
928	{
929		review_score_preference = ( !GDynamicStore.s_preferences['review_score_preference'] ? 0 : GDynamicStore.s_preferences['review_score_preference'] );
930	}
931
932	$J.get( 'https://store.steampowered.com/appreviewhistogram/' + appid, { l: 'thai', review_score_preference: review_score_preference }
933	).done( function( data ) {
934
935		$J( "#review_histograms_container" ).addClass( "has_data" );
936
937		// remove so we can draw to the canvas
938		$J( "#review_histograms_container" ).removeClass( "collapsed" );
939		$J( "#reviews_filter_options" ).removeClass( "graph_collapsed" );
940
941		var bCountAllReviews = data.count_all_reviews;
942		var bExpandGraph = data.expand_graph;
943		var numTotalDays = ( data.results.end_date - data.results.start_date ) / 86400;
944		var numReviewsRecent = 0;
945		for ( var i = 0; i < data.results.recent.length; ++i )
946		{
947			var recentDay = data.results.recent[i];
948			numReviewsRecent += recentDay.recommendations_up + recentDay.recommendations_down;
949		}
950
951		if ( numTotalDays < 7 )
952		{
953			$J( "#app_reviews_hash" ).addClass( "graph_hidden" );
954			$J( "#review_histogram_rollup_container" ).hide();
955			$J( "#review_histogram_rollup_section" ).addClass( "recent" );
956			$J( "#review_histogram_rollup_section" ).addClass( "hidden" );
957			$J( "#review_histogram_recent_section" ).hide();
958			return;
959		}
960		else if ( numReviewsRecent == 0 )
961		{
962			data.results.recent = null;
963			$J( "#review_histogram_rollup_section" ).addClass( "recent" );
964			$J( "#review_histogram_recent_section" ).hide();
965		}
966		else if ( numTotalDays < 30 )
967		{
968			$J( "#review_histogram_rollup_section" ).addClass( "recent" );
969			$J( "#review_histogram_recent_section" ).hide();
970
971			data.results.rollups = data.results.recent;
972			data.results.recent = null;
973			data.results.rollup_type = 'day';
974		}
975
976		var maxPositive = 0;
977		var chartDataPositive = [];
978		var chartDataNegative = [];
979		var chartDataAll = [];
980
981		var chartDataPositiveEvent = [];
982		var chartDataNegativeEvent = [];
983		var chartDataAllEvent = [];
984		var rgPastEventsRollup = [];
985		var rgPastEventsRecent = [];
986
987		for ( var i = 0; i < data.results.rollups.length; ++i )
988		{
989			var rollup = data.results.rollups[i];
990			maxPositive = Math.max( rollup.recommendations_up, maxPositive );
991			var barDataUp = [ rollup.date * 1000, rollup.recommendations_up ];
992			var barDataDown = [ rollup.date * 1000, -rollup.recommendations_down ];
993			chartDataPositive.push( barDataUp );
994			chartDataNegative.push( barDataDown );
995			chartDataAll.push( [ rollup.recommendations_up, rollup.recommendations_down ] );
996
997			if ( data.past_events && data.past_events.length != 0 )
998			{
999				for ( var j = 0; j < data.past_events.length; ++j )
1000				{
1001					var event = data.past_events[j];
1002					var rollupDateEnd = new Date( rollup.date * 1000 );
1003					if ( data.results.rollup_type == 'week' )
1004					{
1005						rollupDateEnd = new Date( ( rollup.date + 86400*7 - 1 ) * 1000 );
1006					}
1007					else if ( data.results.rollup_type == 'month' )
1008					{
1009						rollupDateEnd.setMonth( rollupDateEnd.getMonth() + 1 );
1010					}
1011					if ( IntervalDistance( event.start_date, event.end_date, rollup.date, rollupDateEnd.getTime() / 1000 ) == 0 )
1012					{
1013						chartDataPositiveEvent.push( barDataUp );
1014						chartDataNegativeEvent.push( barDataDown );
1015						chartDataAllEvent.push( [ rollup.recommendations_up, rollup.recommendations_down ] );
1016
1017						AddOrUpdatePastEvent( rgPastEventsRollup, { start_date: event.start_date, end_date : event.end_date, recommendations_up: rollup.recommendations_up } );
1018						break;
1019					}
1020				}
1021			}
1022		}
1023		var seriesRollup = [
1024			{ label: "แง่บวก", color: "#66c0f4", fillColor: "#66c0f4", data: chartDataPositive, dataAll: chartDataAll },
1025			{ label: "แง่ลบ", color: "#A34C25", fillColor: "#A34C25", data: chartDataNegative, dataAll: chartDataAll },
1026			{ label: "แง่บวก", color: "#FFFFFF", fillColor: "#FFFFFF", data: chartDataPositiveEvent, dataAll: chartDataAllEvent },
1027			{ label: "แง่ลบ", color: "#FFFFFF", fillColor: "#FFFFFF", data: chartDataNegativeEvent, dataAll: chartDataAllEvent },
1028		];
1029
1030		var seriesRecent = null;
1031		if ( data.results.recent )
1032		{
1033			chartDataPositive = [];
1034			chartDataNegative = [];
1035			chartDataAll = []
1036			chartDataPositiveEvent = [];
1037			chartDataNegativeEvent = [];
1038			chartDataAllEvent = []
1039			for ( var i = 0; i < data.results.recent.length; ++i )
1040			{
1041				var recentDay = data.results.recent[i];
1042				var barDataUp = [ recentDay.date * 1000, recentDay.recommendations_up ];
1043				var barDataDown = [ recentDay.date * 1000, -recentDay.recommendations_down ];
1044				chartDataPositive.push( barDataUp );
1045				chartDataNegative.push( barDataDown );
1046				chartDataAll.push( [ recentDay.recommendations_up, recentDay.recommendations_down ] );
1047				if ( data.past_events && data.past_events.length != 0 )
1048				{
1049					for ( var j = 0; j < data.past_events.length; ++j )
1050					{
1051						var event = data.past_events[j];
1052						if ( IntervalDistance( event.start_date, event.end_date, recentDay.date, recentDay.date + 86400 - 1 ) == 0 )
1053						{
1054							chartDataPositiveEvent.push( barDataUp );
1055							chartDataNegativeEvent.push( barDataDown );
1056							chartDataAllEvent.push( [ recentDay.recommendations_up, recentDay.recommendations_down ] );
1057
1058							AddOrUpdatePastEvent( rgPastEventsRecent, { start_date: event.start_date, end_date : event.end_date, recommendations_up: recentDay.recommendations_up } );
1059							break;
1060						}
1061					}
1062				}
1063			}
1064			seriesRecent = [
1065				{ color: "#66c0f4", label: "แง่บวก", data: chartDataPositive, dataAll: chartDataAll },
1066				{ color: "#A34C25", label: "แง่ลบ", data: chartDataNegative, dataAll: chartDataAll },
1067				{ label: "แง่บวก", color: "#FFFFFF", fillColor: "#FFFFFF", data: chartDataPositiveEvent, dataAll: chartDataAllEvent },
1068				{ label: "แง่ลบ", color: "#FFFFFF", fillColor: "#FFFFFF", data: chartDataNegativeEvent, dataAll: chartDataAllEvent },
1069			];
1070		}
1071
1072		var options = {
1073			series: {
1074				stack: 0,
1075				bars: {
1076					show: 1,
1077					fill: 1,
1078					lineWidth: 0,
1079					barWidth: 86400*1000 * 0.5,
1080					align: "left"
1081				},
1082				lines: {
1083					show: 0
1084				},
1085				highlightColor: 'rgb(255,255,255)'
1086			},
1087			legend: {
1088				show: 0
1089			},
1090			xaxis: {
1091				mode: "time",
1092				timeformat: "%b %d",
1093				tickLength: 0
1094			},
1095			yaxis: {
1096				tickFormatter : function( val, axis ) {
1097					return ( val < 0 ) ? -val : val;
1098				},
1099				tickLength: 0,
1100				tickDecimals: 0,
1101				autoscaleMargin: 0,
1102			},
1103			grid: {
1104				hoverable: true,
1105				clickable: true,
1106				borderWidth: 0,
1107				margin: 0,
1108				mouseActiveRadius: 10,
1109				autoHighlight: true,
1110				markings: [ { yaxis: { from: 0, to: 0 }, color: "#4582A5" } ]
1111			},
1112			selection: {
1113				mode: "x",
1114				color: "#ffffff",
1115			}
1116		};
1117
1118		// week/month rollup
1119		var rollupOptions = $J.extend( true, {}, options );
1120		if ( data.results.rollup_type == 'week' )
1121		{
1122			if ( numTotalDays > 365 / 2 )
1123			{
1124				rollupOptions.xaxis.timeformat = "%b";
1125			}
1126			else
1127			{
1128				rollupOptions.xaxis.timeformat = "%b %d";
1129			}
1130			rollupOptions.series.bars.barWidth = 86400*1000 * 7 * 0.5;
1131		}
1132		else if ( data.results.rollup_type == 'month' )
1133		{
1134			rollupOptions.xaxis.timeformat = numTotalDays > 365 ? "%b %Y": "%b";
1135			rollupOptions.series.bars.barWidth = 86400*1000 * 30 * 0.5;
1136		}
1137
1138		// leave room for the asterisk
1139		if ( rgPastEventsRollup.length != 0 )
1140		{
1141			rollupOptions.yaxis.max = maxPositive * 1.1;
1142		}
1143
1144		var graphRollup =  $J( "#review_histogram_rollup" );
1145		var flotRollup = $J.plot( graphRollup, seriesRollup, rollupOptions );
1146
1147		if ( rgPastEventsRollup.length != 0 )
1148		{
1149			var funcDrawPastEvents = function() {
1150				DrawPastEvents( flotRollup, rgPastEventsRollup );
1151			};
1152			flotRollup.hooks.draw.push( funcDrawPastEvents );
1153			funcDrawPastEvents();
1154
1155			$J("#review_graph_canvas").resize( funcDrawPastEvents );
1156		}
1157
1158		// recent
1159		var graphRecent = null;
1160		if ( seriesRecent )
1161		{
1162			var recentOptions = $J.extend( true, {}, options );
1163			recentOptions.series.bars.barWidth = 86400*1000 * 0.5;
1164			recentOptions.xaxis.timeformat = "%b %d";
1165						recentOptions.yaxis.autoscaleMargin = rgPastEventsRecent.length != 0 ? 0.2 : null;
1166			var graphRecent =  $J( "#review_histogram_recent" );
1167			var flotRecent = $J.plot( graphRecent, seriesRecent, recentOptions );
1168
1169			var recentGraphStartDate = data.results.recent[0].date * 1000;
1170
1171			var rollupPoints = flotRollup.getData();
1172			var seriesPositive = rollupPoints[0];
1173			{
1174				var startX = seriesPositive.xaxis.p2c( recentGraphStartDate );
1175				var endPoint = seriesPositive.data[seriesPositive.data.length-1];
1176				var endX = seriesPositive.xaxis.p2c( endPoint[0] + rollupOptions.series.bars.barWidth );
1177				var highlightWidth = endX - startX;
1178
1179				// get min/max of the yaxis
1180				var axes = flotRollup.getAxes();
1181				var startY = seriesPositive.yaxis.p2c( axes.yaxis.max );
1182				var endY = seriesPositive.yaxis.p2c( axes.yaxis.min );
1183				var highlightHeight = endY - startY;
1184
1185				var funcDrawGraphOverlay = function() {
1186
1187					var c = document.getElementById("review_graph_canvas");
1188					var ctx = c.getContext("2d");
1189					// resize the canvas to the same size as the element, so our drawing doesn't look blurry
1190					c.width = $J(c).width();
1191					c.height = $J(c).height();
1192					ctx.clearRect(0, 0, c.width, c.height);
1193					// these should be 1-to-1 now, but for correctness, we need to
1194					var scaleX = c.width / c.offsetWidth;
1195					var scaleY = c.height / c.offsetHeight;
1196
1197					// draw rect on the graph
1198					ctx.fillStyle = 'rgba(148,217,255,0.2)';
1199					var offsets = flotRollup.getPlotOffset();
1200					var offsetLeft = ( offsets.left + startX ) + $J(flotRollup.getCanvas()).offsetParent().position().left;
1201					var offsetTop = ( offsets.top + startY ) + $J(flotRollup.getCanvas()).offsetParent().position().top;
1202					ctx.fillRect(offsetLeft * scaleX, offsetTop * scaleY, highlightWidth, highlightHeight);
1203
1204					// now draw the "pop-out" on our other canvas
1205					offsetLeft = ( offsets.left + endX ) + $J(flotRollup.getCanvas()).offsetParent().position().left;
1206					offsetTop = ( offsets.top + startY ) + $J(flotRollup.getCanvas()).offsetParent().position().top;
1207					var offsetBottom = ( offsets.top + endY ) + $J(flotRollup.getCanvas()).offsetParent().position().top;
1208					var recentSection = $J("#review_histogram_recent_section");
1209
1210					ctx.fillStyle = 'rgba(33,44,61,1)';
1211					ctx.beginPath();
1212					ctx.moveTo(offsetLeft * scaleX, offsetTop * scaleY);
1213					ctx.lineTo(recentSection.position().left * scaleX, recentSection.position().top * scaleY);
1214					ctx.lineTo(recentSection.position().left * scaleX, ( recentSection.position().top + recentSection.height() ) * scaleY);
1215					ctx.lineTo(offsetLeft * scaleX, offsetBottom * scaleY);
1216					ctx.lineTo(offsetLeft * scaleX, offsetTop * scaleY);
1217					ctx.fill();
1218				};
1219
1220				var funcDrawPastEvents = function() {
1221					DrawPastEvents( flotRecent, rgPastEventsRecent );
1222				};
1223				flotRecent.hooks.draw.push( funcDrawPastEvents );
1224				funcDrawPastEvents();
1225
1226				$J("#review_graph_canvas").resize( funcDrawGraphOverlay );
1227				$J("#review_graph_canvas").resize( funcDrawPastEvents );
1228				funcDrawGraphOverlay();
1229			}
1230		}
1231
1232		// tooltip
1233		$J(
1234			`<div id='review_histogram_tooltip'>
1235				<div class='date'></div>
1236				<div class='positive'><span></span>&nbsp;แง่บวก&nbsp;(<span class='percentage'></span>%)</div>
1237				<div class='negative'><span></span>&nbsp;
1237แง่ลบ</div>
1238			</div>`
1239		).appendTo("body");
1240		var funcTooltip = function (event, pos, item) {
1241			var tooltip = $J("#review_histogram_tooltip");
1242			if ( item )
1243			{
1244				var idx = item.dataIndex;
1245				var reviewData = item.series.dataAll[idx];
1246				var numPositiveReviews = reviewData[0];
1247				var numNegativeReviews = reviewData[1];
1248				var percentage = ( 100.0 * numPositiveReviews ) / Math.max( 1, numPositiveReviews + numNegativeReviews );
1249				var x = item.datapoint[0].toFixed(2);
1250				var y = item.datapoint[1].toFixed(2);
1251				var bNegativeReviews = y < 0;
1252				var yDelta = bNegativeReviews ? 10 : ( -20 - tooltip.height() );
1253				var xDelta = 5;
1254
1255				var date = new Date( parseInt(x) );
1256
1257				var dateOptions = { day: 'numeric', month: 'long', year: 'numeric', timeZone: "UTC" };
1258				var strDate = date.toLocaleDateString( undefined, dateOptions );
1259				tooltip.find( ".positive span:first-child" ).html( v_numberformat( numPositiveReviews ) );
1260				tooltip.find( ".negative span:first-child" ).html( v_numberformat( numNegativeReviews ) );
1261				tooltip.find( ".percentage" ).html( percentage.toFixed( 0 ) );
1262				tooltip.find( ".date" ).html( strDate );
1263				tooltip.css( {top: item.pageY+yDelta, left: item.pageX+xDelta} );
1264				tooltip.fadeIn( 10 );
1265			}
1266			else
1267			{
1268				tooltip.hide();
1269			}
1270		};
1271
1272		// click
1273		var funcClick = function( plot, rollupType ) {
1274			return function (event, pos, item) {
1275				// ignore for selection
1276				if ( plot.getSelection() )
1277				{
1278					return;
1279				}
1280
1281				if ( item )
1282				{
1283					var x = item.datapoint[0].toFixed(2);
1284					var y = item.datapoint[1].toFixed(2);
1285					var numReviews = parseInt( y );
1286					var bNegativeReviews = numReviews < 0;
1287
1288					var date = new Date( parseInt(x) );
1289					var startDate = date.getTime() / 1000;
1290					var endDate = startDate + 86400;
1291					if ( rollupType == 'week' )
1292					{
1293						endDate = startDate + 86400 * 7;
1294					}
1295					else if ( rollupType == 'month' )
1296					{
1297						var lastDayOfMonth = new Date( date.getUTCFullYear(), date.getUTCMonth() + 1, 0 );
1298						endDate = lastDayOfMonth.getTime() / 1000;
1299					}
1300
1301					$J( bNegativeReviews ? '#review_type_negative' : '#review_type_positive').attr( 'checked', true );
1302					FilterReviewsToGraph( bCountAllReviews, startDate, endDate );
1303
1304					flotRollup.clearSelection();
1305					if ( flotRecent )
1306					{
1307						flotRecent.clearSelection();
1308					}
1309				}
1310			}
1311		};
1312
1313		var funcSelected = function( plot ) {
1314			return function (event, ranges) {
1315
1316				var startDate = Math.floor( ranges.xaxis.from.toFixed(1) / ( 86400 * 1000 ) ) * 86400;
1317				var endDate = Math.ceil( ranges.xaxis.to.toFixed(1) / ( 86400 * 1000 ) ) * 86400;
1318
1319				$J( '#review_type_all' ).attr( 'checked', true );
1320				FilterReviewsToGraph( bCountAllReviews, startDate, endDate );
1321				if ( plot == flotRollup && flotRecent )
1322				{
1323					flotRecent.clearSelection();
1324				}
1325				else if ( plot == flotRecent )
1326				{
1327					flotRollup.clearSelection();
1328				}
1329			};
1330		};
1331
1332		var funcUnSelected = function ( event ) {
1333
1334		};
1335
1336		graphRollup.bind("plothover", funcTooltip);
1337		graphRollup.bind("plotclick", funcClick( flotRollup, data.results.rollup_type ) );
1338		graphRollup.bind("plotselected", funcSelected( flotRollup ));
1339		graphRollup.bind("plotunselected", funcUnSelected );
1340		if ( graphRecent )
1341		{
1342			graphRecent.bind("plothover", funcTooltip);
1343			graphRecent.bind("plotclick", funcClick( flotRecent, 'day' ) );
1344			graphRecent.bind("plotselected", funcSelected( flotRecent ) );
1345			graphRecent.bind("plotunselected", funcUnSelected );
1346		}
1347
1348		var funcViewReviewsDuringEvent = function( bCountAllReviews, startDate, endDate ) {
1349			$J( '#review_type_all' ).attr( 'checked', true );
1350			$J('#reviews_offtopic_activity_checkbox').attr( 'checked', false );
1351			FilterReviewsGraph( bCountAllReviews, startDate, endDate, false );
1352			if ( flotRecent )
1353			{
1354				flotRecent.clearSelection();
1355			}
1356			flotRollup.clearSelection();
1357		};
1358
1359		var funcViewReviewsExcludingEvent = function( bCountAllReviews, startDate, endDate ) {
1360			$J( '#review_type_all' ).attr( 'checked', true );
1361			$J('#reviews_offtopic_activity_checkbox').attr( 'checked', false );
1362			FilterReviewsGraph( bCountAllReviews, startDate, endDate, true );
1363			if ( flotRecent )
1364			{
1365				flotRecent.clearSelection();
1366			}
1367			flotRollup.clearSelection();
1368		};
1369
1370		// recent events
1371		if ( bExpandGraph )
1372		{
1373			var container = $J( "#review_recent_events_container" );
1374			container.show();
1375
1376			flotRollup.draw();
1377			if ( flotRecent )
1378			{
1379				flotRecent.draw();
1380			}
1381		}
1382		else
1383		{
1384			$J( "#review_histograms_container" ).addClass( "collapsed" );
1385			$J( "#reviews_filter_options" ).addClass( "graph_collapsed" );
1386		}
1387
1388		$J( ".review_event_graph_icon" ).each( function( index, value ) {
1389			var $elem = $J( value );
1390			$elem.click( function() {
1391				var startDate = $J( this ).data( 'startDate' );
1392				var endDate = $J( this ).data( 'endDate' );
1393
1394				var startDateObj = new Date( startDate * 1000 );
1395				var endDateObj = new Date( endDate * 1000 );
1396				var dateOptions = { day: 'numeric', month: 'long', year: 'numeric', timeZone: "UTC" };
1397				var strDateRange = startDateObj.toLocaleDateString( undefined, dateOptions ) + " - " + endDateObj.toLocaleDateString( undefined, dateOptions );
1398
1399				var dialogContent = $J( "<div/>", { class: "review_anomalous_events_dialog" } );
1400				dialogContent.append( $J( "<div/>", { class: 'review_anomalous_events_icon', html: '*' } ) );
1401				dialogContent.append( $J( "<div/>", { class: 'review_anomalous_events_title', html: strDateRange } ) );
1402				dialogContent.append( $J( "<div/>", { style: "clear: left" } ) );
1403				dialogContent.append( $J( "<div/>", { html: '<p>ช่วงเวลานี้ถูกทำเครื่องหมายว่า มีชุดบทวิจารณ์ที่ไม่ปกติ ซึ่งเราเชื่อว่าส่วนใหญ่ไม่เกี่ยวข้องกับเนื้อหาที่คุณต้องการรับจากผลิตภัณฑ์นั้น บทวิจารณ์ภายในช่วงเวลานี้จะถูกละเว้นจากคะแนนบทวิจารณ์ด้วยค่าเริ่มต้น</p><p>คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับวิธีการเลือกช่วงเวลาเหล่านี้โดยการอ่านบน <a href="https://steamcommunity.com/games/593110/announcements/detail/1808664240333155775">โพสต์บล็อก Steam ของเรา</a></p><p>หากคุณต้องการข้อมูลเชิงลึก คุณยังคงสามารถอ่านบทวิจารณ์ภายในช่วงเวลานี้ หรือแก้ไขการปรับแต่งของคุณเพื่อให้รวมช่วงเวลาของบทวิจารณ์นอกเรื่องในคะแนนบทวิจารณ์</p>' } ) );
1404				var dialog = ShowConfirmDialog( 'กิจกรรมบทวิจารณ์นอกเรื่อง', dialogContent, 'อ่านบทวิจารณ์', 'ยกเลิก', 'แก้ไขการปรับแต่ง' );
1405				dialog.done( function( type ) {
1406					if ( type == 'OK' )
1407					{
1408						funcViewReviewsDuringEvent( bCountAllReviews, startDate, endDate );
1409					}
1410					else if ( type == 'SECONDARY' )
1411					{
1412						EditUserReviewScorePreference();
1413					}
1414				} );
1415			} );
1416		} );
1417
1418	} );
1419}
1420
1421function SetReviewsGraphVisibility( bVisible )
1422{
1423	const previousFocusedElement = document.activeElement;
1424	$J( "#review_histograms_container" ).toggleClass( "collapsed", !bVisible );
1425	$J( "#reviews_filter_options" ).toggleClass( "graph_collapsed", !bVisible );
1426
1427	const showButton = document.getElementById( "review_show_graph_button" );
1428	const hideButton = document.getElementById( "review_hide_graph_button" );
1429	if ( previousFocusedElement === showButton )
1430	{
1431		hideButton.focus();
1432	}
1433	else if(previousFocusedElement === hideButton )
1434	{
1435		showButton.focus();
1436	}
1437}
1438
1439function ClearReviewDateFilter()
1440{
1441	$J('#review_start_date').val( -1 );
1442	$J('#review_end_date').val( -1 );
1443	$J('#review_date_range_histogram').attr( 'disabled', true );
1444	$J('#review_date_range_exclude_histogram').attr( 'disabled', true );
1445	$J( "#review_selected_histogram_date_range_text" ).hide();
1446	ShowFilteredReviews();
1447}
1448
1449function OnLoadReviews()
1450{
1451	BuildReviewHistogram();
1452	ShowFilteredReviews();
1453}
1454
1455function LocalizeDateRange( startDate, endDate )
1456{
1457	let options = { month: 'short', day: 'numeric', year: 'numeric' };
1458	return new Date( startDate * 1000 ).toLocaleDateString( undefined, options ) + ' - ' + new Date( endDate * 1000 ).toLocaleDateString( undefined, options );
1459}
1460
1461function UpdateHardwareFilter( category )
1462{
1463	var hardwareValue = $J( 'input[name="review_' + category + '"]:checked' ).val();
1464	var elemFilter = $J( "#reviews_filter_" + category );
1465	if ( !hardwareValue || hardwareValue == 'all' )
1466	{
1467		elemFilter.hide();
1468	}
1469	else
1470	{
1471		elemFilter.find( "span:first-child" ).text( hardwareValue );
1472		elemFilter.show();
1473	}
1474}
1475
1476function UpdateActiveFilters()
1477{
1478	var bAnyActiveFilters = false;
1479	var startDate = $J( "#review_start_date" ).val();
1480	var endDate = $J( "#review_end_date" ).val();
1481
1482	// type
1483	if ( $J( "#review_type_positive" ).attr( "checked" ) )
1484	{
1485		bAnyActiveFilters = true;
1486		$J( "#reviews_filter_type" ).show();
1487		$J( "#reviews_filter_type" ).text( 'แง่บวก' );
1488	}
1489	else if ( $J( "#review_type_negative" ).attr( "checked" ) )
1490	{
1491		bAnyActiveFilters = true;
1492		$J( "#reviews_filter_type" ).show();
1493		$J( "#reviews_filter_type" ).text( 'แง่ลบ' );
1494	}
1495	else
1496	{
1497		$J( "#reviews_filter_type" ).hide();
1498	}
1499
1500	// purchase type
1501	if ( $J( "#purchase_type_steam" ).attr( "checked" ) )
1502	{
1503		bAnyActiveFilters = true;
1504		$J( "#reviews_filter_purchase_type" ).show();
1505		$J( "#reviews_filter_purchase_type" ).text( 'ผู้สั่งซื้อบน Steam' );
1506	}
1507	else if ( $J( "#purchase_type_non_steam" ).attr( "checked" ) )
1508	{
1509		bAnyActiveFilters = true;
1510		$J( "#reviews_filter_purchase_type" ).show();
1511		$J( "#reviews_filter_purchase_type" ).text( 'ไม่ได้สั่งซื้อบน Steam' );
1512	}
1513	else
1514	{
1515		$J( "#reviews_filter_purchase_type" ).hide();
1516	}
1517
1518	// language
1519	var selectedLanguage = $J('input[name="review_language"]:checked').val();
1520	if ( $J( "#review_language_mine" ).attr( "checked" ) )
1521	{
1522		bAnyActiveFilters = true;
1523		$J( "#reviews_filter_language" ).show();
1524		$J( "#reviews_filter_language" ).text( 'ภาษาของคุณ' );
1525	}
1526	else if ( false )
1527	{
1528		bAnyActiveFilters = true;
1529		$J( "#reviews_filter_language" ).show();
1530		$J( "#reviews_filter_language" ).text( 'จีนตัวย่อ' );
1531	}
1532	else if ( selectedLanguage != 'all' )
1533	{
1534		$J( "#reviews_filter_language" ).show();
1535		var strDisplay = $J('input[name="review_language"]:checked').data( 'language' );
1536		$J( "#reviews_filter_language" ).text( strDisplay );
1537	}
1538	else
1539	{
1540		$J( "#reviews_filter_language" ).hide();
1541	}
1542
1543	// graph
1544	if ( $J( "#review_date_range_histogram" ).attr( "checked" ) )
1545	{
1546		bAnyActiveFilters = true;
1547		$J( "#reviews_filter_graph" ).show();
1548		$J( "#review_selected_histogram_date_range_prefix" ).text( 'ดูเฉพาะ ' );
1549
1550		UpdateDateFilterText( LocalizeDateRange( startDate, endDate ) );
1551	}
1552	else if ( $J( "#review_date_range_exclude_histogram" ).attr( "checked" ) )
1553	{
1554		bAnyActiveFilters = true;
1555		$J( "#reviews_filter_graph" ).show();
1556		$J( "#review_selected_histogram_date_range_prefix" ).text( 'ละเว้น ' );
1557
1558		UpdateDateFilterText( LocalizeDateRange( startDate, endDate ) );
1559	}
1560	else
1561	{
1562		$J( "#reviews_filter_graph" ).hide();
1563	}
1564
1565	// off-topic review activity
1566	if ( $J( "#user_reviews_offtopic_activity_menu" ).is( ":visible" ) && $J( "#reviews_offtopic_activity_checkbox" ).attr( "checked" ) )
1567	{
1568		bAnyActiveFilters = true;
1569		$J( "#reviews_filter_offtopic_activity" ).show();
1570	}
1571	else
1572	{
1573		$J( "#reviews_filter_offtopic_activity" ).hide();
1574	}
1575
1576	// playtime
1577	var playtimeFilterMin = $J( "#app_reviews_playtime_range_min" ).val();
1578	var playtimeFilterMax = $J( "#app_reviews_playtime_range_max" ).val();
1579	if ( playtimeFilterMin != 0 || playtimeFilterMax != 0 )
1580	{
1581		var elemPlaytimeDesc = $J( "#review_playtime_preset_text" );
1582		if ( playtimeFilterMax == 0 )
1583		{
1584			if ( playtimeFilterMin == 1 )
1585			{
1586				elemPlaytimeDesc.text( 'มากกว่า 1 ชั่วโมง' );
1587			}
1588			else
1589			{
1590				elemPlaytimeDesc.text( 'มากกว่า %2$s ชั่วโมง'.replace( "%2$s", playtimeFilterMin ) );
1591			}
1592		}
1593		else
1594		{
1595			elemPlaytimeDesc.text( '%1$s ชั่วโมง ถึง %2$s ชั่วโมง'.replace( "%1$s", playtimeFilterMin ).replace( "%2$s", playtimeFilterMax ) );
1596		}
1597
1598		bAnyActiveFilters = true;
1599		$J( "#reviews_filter_playtime" ).show();
1600	}
1601	else
1602	{
1603		$J( "#reviews_filter_playtime" ).hide();
1604	}
1605
1606	if ( $J( "#review_playtime_type_deck" ).attr( "checked" ) )
1607	{
1608		$J( "#reviews_filter_deck_playtime" ).show();
1609	}
1610	else
1611	{
1612		$J( "#reviews_filter_deck_playtime" ).hide();
1613	}
1614
1615	UpdateHardwareFilter( 'hardware_os' );
1616	UpdateHardwareFilter( 'hardware_cpu_name' );
1617	UpdateHardwareFilter( 'hardware_gfx_adapter_description' );
1618	UpdateHardwareFilter( 'hardware_gaming_device_type' );
1619
1620	// topic
1621	$J( ".review_topic_filter" ).hide();
1622	$J( 'input[name="review_topic"]:checked' ).each( ( index, elem ) =>
1623	{
1624		$J( "#review_topic_filter" + $J( elem ).val() ).show();
1625	} );
1626
1627	$J( "#reviews_filter_title" ).toggle( bAnyActiveFilters );
1628}
1629
1630function ShowFilteredReviews()
1631{
1632	const params = new URLSearchParams( window.location.search );
1633	const bLoadReviews = !!g_bUseOldReviewDisplay;
1634
1635	UpdateActiveFilters();
1636
1637	if ( $J( "#review_appid" ).length == 0 )
1638		return;
1639
1640	var appid = $J( "#review_appid" ).val();
1641	var context = $J( "input[name=review_context]:checked" ).val();
1642	var defaultDayRange = $J( "#review_default_day_range" ).val();
1643	var startDate = $J( "#review_start_date" ).val();
1644	var endDate = $J( "#review_end_date" ).val();
1645
1646	// Notify React of the change to filters. If we haven't rendered React because
1647	// we're forcing old-style display, this will still post the message but
1648	// nobody's listening.
1649	let reviewType = $J( 'input[name="review_type"]:checked' ).val();
1650	let purchaseType = $J( 'input[name="purchase_type"]:checked' ).val();
1651	let language = $J( 'input[name="review_language"]:checked' ).val();
1652	let filterOfftopicActivity = $J( "#reviews_offtopic_activity_checkbox" ).is( ":checked" ) ? 1 : 0;
1653	let dateRangeType = $J( 'input[name="review_date_range"]:checked' ).val();
1654	let summaryNumPositiveReviews = $J( "#review_summary_num_positive_reviews" ).val();
1655	let summaryNumReviews = $J( "#review_summary_num_reviews" ).val();
1656	let playtimeFilterMin = $J( "#app_reviews_playtime_range_min" ).val();
1657	let playtimeFilterMax = $J( "#app_reviews_playtime_range_max" ).val();
1658	let playtimeType = $J( 'input[name="review_playtime_type"]:checked' ).val();
1659
1660	// hardware
1661	let hardwareOS = $J( 'input[name="review_hardware_os"]:checked' ).val();
1662	let hardwareCPU = $J( 'input[name="review_hardware_cpu_name"]:checked' ).val();
1663	let hardwareGPU = $J( 'input[name="review_hardware_gfx_adapter_description"]:checked' ).val();
1664	let hardwareDeviceType = $J( 'input[name="review_hardware_gaming_device_type"]:checked' ).val();
1665
1666	let topics = [];
1667	$J( 'input[name="review_topic"]:checked' ).each( ( index, elem ) =>
1668	{
1669		topics.push( $J( elem ).val() );
1670	});
1671
1672	var useReviewQuality = $J( 'input[name="use_review_quality"]' ).is( ":checked" ) ? 1 : 0;
1673
1674	window.postMessage( JSON.stringify( {
1675		message: 'ReviewFilterChange',
1676		args: {
1677			review_type: reviewType,
1678			filter: context,
1679			day_range: defaultDayRange,
1680			start_date: startDate,
1681			end_date: endDate,
1682			purchase_type: purchaseType,
1683			language,
1684			filter_offtopic_activity: filterOfftopicActivity,
1685			date_range_type: dateRangeType,
1686			summary_num_positive_reviews: summaryNumPositiveReviews,
1687			summary_num_reviews: summaryNumReviews,
1688			playtime_filter_min: playtimeFilterMin,
1689			playtime_filter_max: playtimeFilterMax,
1690			playtime_type: playtimeType,
1691			use_review_quality: useReviewQuality,
1692			hardware_os: hardwareOS,
1693			hardware_cpu: hardwareCPU,
1694			hardware_gpu: hardwareGPU,
1695			hardware_device_type: hardwareDeviceType,
1696			topics,
1697		}
1698	} ) );
1699
1700	if ( bLoadReviews )
1701	{
1702		SelectReviews( appid, context, defaultDayRange, startDate, endDate, true );
1703	}
1704}
1705
1706function ChangeReviewPurchaseTypeFilter()
1707{
1708	var purchaseType = $J('input[name="purchase_type"]:checked').val();
1709	V_SetCookie( "review_purchase_type_filter", purchaseType, purchaseType == 'all' ? 1 : 14, "app/" );
1710	ShowFilteredReviews();
1711}
1712
1713function OnReviewPlaytimeFilterSliderChanged( hourMin, hourMax, maxHours )
1714{
1715	if ( hourMin == 0 && hourMax == maxHours )
1716	{
1717		$J('input[name="review_playtime_preset"][value="0"]').attr( 'checked', true );
1718	}
1719	else
1720	{
1721		$J('input[name="review_playtime_preset"]').attr( 'checked', false );
1722	}
1723
1724	ShowFilteredReviews();
1725}
1726
1727function SelectPlaytimeFilterPreset( playtimeMinHours )
1728{
1729	var maxHours = 100;
1730	var maxSeconds = maxHours * 60 * 60;
1731
1732	$J("#app_reviews_playtime_slider").slider('values', 0, playtimeMinHours * 60 * 60 );
1733	$J("#app_reviews_playtime_slider").slider('values', 1, maxSeconds );
1734
1735	UpdatePlaytimeFilterValues( playtimeMinHours, maxHours );
1736
1737	ShowFilteredReviews();
1738}
1739
1740function ChangedOfftopicReviewActivityFilter()
1741{
1742	ShowFilteredReviews();
1743}
1744
1745function ClearOfftopicReviewActivityFilter()
1746{
1747	$J('#reviews_offtopic_activity_checkbox').attr( 'checked', false );
1748	ShowFilteredReviews();
1749}
1750
1751function ClearReviewPlaytimeFilter()
1752{
1753	$J('#review_playtime_preset_0').attr( 'checked', true );
1754	SelectPlaytimeFilterPreset( 0 );
1755}
1756
1757function ClearDeckPlaytimeFilter()
1758{
1759	$J('#review_playtime_type_all').attr( 'checked', true );
1760	SelectPlaytimeFilterPreset( 0 );
1761}
1762
1763function SetReviewTopicFilter( strTopicId, bSet )
1764{
1765	$J('input[name="review_topic"][value="' + strTopicId + '"]').prop( 'checked', bSet ); // check the menu item (or not)
1766	$J('input[name="review_topic"][value="' + strTopicId + '"]').parent().show(); // make sure we're showing the menu item
1767	ShowFilteredReviews();
1768}
1769
1770function HighlightReviewTopic( strParentElementId, nTopicId, bHighlight )
1771{
1772	CSS.highlights.delete("TopicHighlight");
1773
1774	if ( bHighlight )
1775	{
1776		const parent = $J( "#" + strParentElementId );
1777		const nParentBottom = parent[0].getBoundingClientRect().bottom;
1778
1779		let bHiddenElement = false;
1780		let jqFragments = parent.find( "span.generic_highlight" ).filter( ( _, span ) =>
1781		{
1782			try
1783			{
1784				const rTypes = $J( span ).data( "type" );
1785				const bMatching = rTypes.includes( nTopicId );
1786				if ( bMatching && span.getBoundingClientRect().top > nParentBottom )
1787					bHiddenElement = true;
1788				return bMatching;
1789			}
1790			catch
1791			{
1792				return false;
1793			}
1794		} );
1795
1796		const ranges = $J( jqFragments ).map( ( _, node ) =>
1797		{
1798			const range = document.createRange();
1799			range.selectNodeContents( node );
1800			return range;
1801		} ).get();
1802
1803		if ( bHiddenElement )
1804		{
1805			const range = document.createRange();
1806			range.selectNodeContents( parent.find( ".view_more" )[0] );
1807			ranges.push( range )
1808		}
1809		const highlight = new Highlight( ...ranges );
1810		CSS.highlights.set( "TopicHighlight", highlight );
1811	}
1812}
1813
1814function CollapseLongReviews()
1815{
1816	$J('.review_box').each( function(j, i){
1817		if( $J(i).outerHeight() > 400 )
1818		{
1819			if ( !$J(i).hasClass('expanded') )
1820			{
1821				$J(i).addClass('partial');
1822
1823				// In gamepadui links inside the review content should not be focusable if the review
1824				// is collapsed. This prevents focus going to links the user cannot see.
1825				$J(i).find( 'div.content a' ).data( 'gpFocusDisabled', true );
1826			}
1827		}
1828	});
1829}
1830
1831function UpdatePlaytimeFilterValues( hourMin, hourMax )
1832{
1833	var maxHours = 100;
1834	$J( "#app_reviews_playtime_range_min" ).val( hourMin );
1835	$J( "#app_reviews_playtime_range_max" ).val( hourMax == maxHours ? 0 : hourMax );
1836
1837	var strHoursMinNumber = v_numberformat( hourMin, 0 );
1838	var strHourMin = hourMin > 0 ? '%1$s ชั่วโมง'.replace( /%1\$s/g, strHoursMinNumber ) : 'ไม่มีต่ำสุด';
1839	$J( "#app_reviews_playtime_range_text_min" ).text( strHourMin );
1840
1841	var strHoursMaxNumber = v_numberformat( hourMax, 0 );
1842	var strHourMax = hourMax > 0 && hourMax != maxHours ? '%1$s ชั่วโมง'.replace( /%1\$s/g, strHoursMaxNumber ) : 'ไม่มีสูงสุด';
1843	$J( "#app_reviews_playtime_range_text_max" ).text( strHourMax );
1844
1845
1846	const handles = [...document.querySelectorAll( "#app_reviews_playtime_slider .ui-slider-handle" )];
1847	if ( handles.length > 0 )
1848	{
1849		handles[0].ariaValueNow = hourMin;
1850		handles[0].ariaValueText = strHourMin;
1851		handles[1].ariaValueNow = hourMax;
1852		handles[1].ariaValueText = strHourMax;
1853	}
1854}
1855
1856
1857function InitPlaytimeFilterSlider()
1858{
1859	// Element doesn't exist playtime filters is empty
1860	if ( $J( "#app_reviews_playtime_slider" ).length === 0 )
1861		return;
1862
1863	var maxHours = 100;
1864	var maxSeconds = maxHours * 60 * 60;
1865	const slider = $J( "#app_reviews_playtime_slider" ).slider({
1866		range: true,
1867		min: 0,
1868		max: maxSeconds,
1869		values: [0, maxSeconds],
1870		step: 3600,
1871		slide: function (event, ui) {
1872			var minSecs = ui.values[0];
1873			var maxSecs = ui.values[1];
1874
1875			var hourMin = parseInt( minSecs / ( 60 * 60 ), 10 );
1876			var hourMax = parseInt( maxSecs / ( 60 * 60 ), 10 );
1877
1878			UpdatePlaytimeFilterValues( hourMin, hourMax );
1879		},
1880		change: function( event, ui ) {
1881			if ( event.originalEvent )
1882			{
1883				var minSecs = ui.values[0];
1884				var maxSecs = ui.values[1];
1885
1886				var hourMin = parseInt( minSecs / ( 60 * 60 ), 10 );
1887				var hourMax = parseInt( maxSecs / ( 60 * 60 ), 10 );
1888
1889				OnReviewPlaytimeFilterSliderChanged( hourMin, hourMax, maxHours );
1890			}
1891		}
1892	});
1893	UpdatePlaytimeFilterValues( 0, maxHours );
1894	const handles = [...slider[0].querySelectorAll(".ui-slider-handle")];
1895	for ( const handle of handles )
1896	{
1897		handle.role = "slider";
1898		handle.ariaValueMin = 0;
1899		handle.ariaValueMax = maxHours;
1900	}
1901	handles[0].ariaLabel = 'จำนวนชั่วโมงต่ำสุดที่เล่น';
1902	handles[1].ariaLabel = 'จำนวนชั่วโมงสูงสุดที่เล่น';
1903}
1904
1905//formerly app_reporting.js
1906
1907var gReportedApp = false;
1908function ShowReportDialog( nAppId )
1909{
1910	if ( gReportedApp )
1911	{
1912		return;
1913	}
1914
1915	var content = $J('<div/>', {'class': 'app_report_dialog' } );
1916
1917	content.append( $J('<div/>', {'class': 'app_report_dialog_intro' } ).text('โปรดเลือกเหตุผลว่าทำไมคุณถึงทำการรายงานผลิตภัณฑ์นี้') );
1918
1919		var rgReportOptions = {"1":"\u0e09\u0e49\u0e2d\u0e42\u0e01\u0e07 - <span class=\"sub\">\u0e0b\u0e2d\u0e1f\u0e15\u0e4c\u0e41\u0e27\u0e23\u0e4c\u0e19\u0e35\u0e49\u0e1e\u0e22\u0e32\u0e22\u0e32\u0e21\u0e2b\u0e25\u0e2d\u0e01\u0e25\u0e27\u0e07\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e40\u0e01\u0e47\u0e1a\u0e23\u0e27\u0e1a\u0e23\u0e27\u0e21\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e2a\u0e33\u0e04\u0e31\u0e0d \u0e15\u0e31\u0e27\u0e2d\u0e22\u0e48\u0e32\u0e07\u0e40\u0e0a\u0e48\u0e19 \u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07 Steam \u0e2b\u0e23\u0e37\u0e2d\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e17\u0e32\u0e07\u0e01\u0e32\u0e23\u0e40\u0e07\u0e34\u0e19 (\u0e40\u0e0a\u0e48\u0e19 \u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e1a\u0e31\u0e15\u0e23\u0e40\u0e04\u0e23\u0e14\u0e34\u0e15)<\/span>","2":"\u0e40\u0e1b\u0e47\u0e19\u0e2d\u0e31\u0e19\u0e15\u0e23\u0e32\u0e22 - <span class=\"sub\">\u0e0b\u0e2d\u0e1f\u0e15\u0e4c\u0e41\u0e27\u0e23\u0e4c\u0e19\u0e35\u0e49\u0e14\u0e31\u0e14\u0e41\u0e1b\u0e25\u0e07\u0e41\u0e01\u0e49\u0e44\u0e02\u0e04\u0e2d\u0e21\u0e1e\u0e34\u0e27\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e02\u0e2d\u0e07\u0e1c\u0e39\u0e49\u0e0b\u0e37\u0e49\u0e2d\u0e43\u0e19\u0e17\u0e32\u0e07\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e04\u0e32\u0e14\u0e04\u0e34\u0e14\u0e2b\u0e23\u0e37\u0e2d\u0e40\u0e1b\u0e47\u0e19\u0e2d\u0e31\u0e19\u0e15\u0e23\u0e32\u0e22 (\u0e40\u0e0a\u0e48\u0e19 \u0e40\u0e1b\u0e47\u0e19\u0e21\u0e31\u0e25\u0e41\u0e27\u0e23\u0e4c\u0e2b\u0e23\u0e37\u0e2d\u0e44\u0e27\u0e23\u0e31\u0e2a)<\/span>","5":"\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e0d\u0e48 - <span class=\"sub\">\u0e1a\u0e23\u0e23\u0e08\u0e38\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e0d\u0e48\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e17\u0e33\u0e40\u0e04\u0e23\u0e37\u0e48\u0e2d\u0e07\u0e2b\u0e21\u0e32\u0e22\u0e2d\u0e22\u0e48\u0e32\u0e07\u0e40\u0e2b\u0e21\u0e32\u0e30\u0e2a\u0e21\u0e41\u0e25\u0e30\u0e44\u0e21\u0e48\u0e21\u0e35\u0e01\u0e32\u0e23\u0e01\u0e23\u0e2d\u0e07\u0e2d\u0e32\u0e22\u0e38<\/span>","6":"\u0e2b\u0e21\u0e34\u0e48\u0e19\u0e1b\u0e23\u0e30\u0e21\u0e32\u0e17 - <span class=\"sub\">\u0e1a\u0e23\u0e23\u0e08\u0e38\u0e02\u0e49\u0e2d\u0e04\u0e27\u0e32\u0e21\u0e17\u0e35\u0e48\u0e2b\u0e21\u0e34\u0e48\u0e19\u0e1b\u0e23\u0e30\u0e21\u0e32\u0e17\u0e2b\u0e23\u0e37\u0e2d\u0e43\u0e2a\u0e48\u0e23\u0e49\u0e32\u0e22\u0e1b\u0e49\u0e32\u0e22\u0e2a\u0e35<\/span>","8":"\u0e01\u0e32\u0e23\u0e41\u0e2a\u0e27\u0e07\u0e2b\u0e32\u0e1b\u0e23\u0e30\u0e42\u0e22\u0e0a\u0e19\u0e4c\u0e08\u0e32\u0e01\u0e40\u0e14\u0e47\u0e01 - <span class=\"sub\">\u0e1a\u0e23\u0e23\u0e08\u0e38\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e17\u0e35\u0e48\u0e41\u0e2a\u0e27\u0e07\u0e2b\u0e32\u0e1b\u0e23\u0e30\u0e42\u0e22\u0e0a\u0e19\u0e4c\u0e08\u0e32\u0e01\u0e40\u0e14\u0e47\u0e01\u0e43\u0e19\u0e17\u0e32\u0e07\u0e43\u0e14\u0e17\u0e32\u0e07\u0e2b\u0e19\u0e36\u0e48\u0e07<\/span>","11":"\u0e01\u0e32\u0e23\u0e25\u0e30\u0e40\u0e21\u0e34\u0e14\u0e01\u0e0e\u0e2b\u0e21\u0e32\u0e22 - <span class=\"sub\">\u0e1a\u0e23\u0e23\u0e08\u0e38\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e17\u0e35\u0e48\u0e25\u0e30\u0e40\u0e21\u0e34\u0e14\u0e01\u0e0e\u0e2b\u0e21\u0e32\u0e22\u0e43\u0e19\u0e40\u0e02\u0e15\u0e2d\u0e33\u0e19\u0e32\u0e08\u0e28\u0e32\u0e25\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13<\/span>","13":"\u0e44\u0e21\u0e48\u0e2a\u0e21\u0e1a\u0e39\u0e23\u0e13\u0e4c - <span class=\"sub\">\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e32\u0e27\u0e19\u0e4c\u0e42\u0e2b\u0e25\u0e14 \u0e40\u0e23\u0e34\u0e48\u0e21\u0e40\u0e1b\u0e34\u0e14 \u0e2b\u0e23\u0e37\u0e2d\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e44\u0e14\u0e49\u0e2d\u0e22\u0e48\u0e32\u0e07\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 \u0e41\u0e21\u0e49\u0e27\u0e48\u0e32\u0e40\u0e04\u0e23\u0e37\u0e48\u0e2d\u0e07\u0e19\u0e31\u0e49\u0e19\u0e1c\u0e48\u0e32\u0e19\u0e04\u0e27\u0e32\u0e21\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e23\u0e30\u0e1a\u0e1a\u0e02\u0e3
19191\u0e49\u0e19\u0e15\u0e48\u0e33\u0e41\u0e25\u0e49\u0e27<\/span>"};
1920	var rgReportOptionElements = [];
1921	for ( var eReportType in rgReportOptions )
1922	{
1923		var $ReportOption = $J('<div/>', {'class': 'app_report_dialog_option' } );
1924		$ReportOption.append( $J('<div/>', {'class': 'app_report_dialog_option_input' }).append( $J('<input/>', {'type': 'radio', 'name':'report_type', 'value': eReportType, 'id': 'report_type_' + eReportType } ) ) );
1925		$ReportOption.append( $J('<div/>', {'class': 'app_report_dialog_option_text' }).append( $J('<label/>', {'for': 'report_type_' + eReportType }).html( rgReportOptions[eReportType] ) ) );
1926		rgReportOptionElements.push( $ReportOption );
1927	}
1928
1929	for ( var j, x, i = rgReportOptionElements.length; i; j = parseInt(Math.random() * i), x = rgReportOptionElements[--i], rgReportOptionElements[i] = rgReportOptionElements[j], rgReportOptionElements[j] = x );
1930	for ( var i = 0; i < rgReportOptionElements.length; ++i )
1931	{
1932		content.append( rgReportOptionElements[i] );
1933	}
1934
1935	var textArea = $J('<textarea/>', { 'class': 'app_report_dialog_reason grey_bevel fullwidth', 'id' : 'ReportReason',  'name' : 'report_reason' } );
1936	var maxReasonLength = 512;
1937	textArea.attr( 'maxlength', maxReasonLength );
1938	textArea.bind( "keyup change",
1939		function()
1940		{
1941			var str = $J(this).val()
1942			var mx = parseInt($J(this).attr('maxlength'))
1943			if (str.length > mx)
1944			{
1945				$J(this).val(str.substr(0, mx))
1946				return false;
1947			}
1948		}
1949	);
1950	content.append( $J('<div/>', {'class': 'app_report_dialog_intro' }
1950 ).text('คุณสามารถกรอกข้อมูลเพิ่มเติมที่คุณรู้สึกว่ามีความเกี่ยวข้องได้ที่นี่: ') );
1951	content.append( textArea );
1952
1953	content.append( $J('<div/>', {'class': 'app_report_dialog_dmca' } ).html('หากคุณต้องการจะรายงานการละเมิดลิขสิทธิ์ และคุณเป็นเจ้าของสิขสิทธิ์นี้ โปรดดำเนินการต่อที่แบบฟอร์มแจ้งการร้องเรียน DMCA เกี่ยวกับการละเมิดลิขสิทธิ์ <a href="https://steamcommunity.com/dmca/create/">ที่นี่</a>') );
1954	content.append( $J('<div/>', {'class': 'app_report_dialog_dmca' } ).html('หากคุณต้องการยื่นคำร้องเรียนเกี่ยวกับเครื่องหมายการค้า โปรดกรอกแบบฟอร์ม<a href="https://steamcommunity.com/trademark/createtrademarkcomplaint/">ที่นี่</a>') );
1955
1956	var dialog = ShowConfirmDialog( 'รายงานผลิตภัณฑ์นี้', content, 'รายงาน');
1957
1958	dialog.done( function() {
1959		var eReportTypeSelected = content.find( 'input[type=radio]:checked' ).val();
1960		if ( eReportTypeSelected )
1961		{
1962			$J.post(
1963				'https://store.steampowered.com/appreport/' + nAppId + '/report/',
1964				{
1965					'report_type' : eReportTypeSelected,
1966					'report_reason' : content.find( 'textarea' ).val(),
1967					'sessionid': g_sessionID
1968				}
1969			).done( function( json ) {
1970					gReportedApp = true;
1971					$J( "#ReportAppBtn").addClass( 'btn_active' );
1972				}
1973			).fail( function( jqXHR ) {
1974					var json = jqXHR.responseText.evalJSON();
1975					if ( json.success == 29 )
1976					{
1977						ShowAlertDialog( 'ข้อผิดพลาด', 'คุณได้รายงานผลิตภัณฑ์นี้เรียบร้อยแล้ว!' );
1978						gReportedApp = true;
1979						$J( "#ReportAppBtn").addClass( 'btn_active' );
1980					}
1981					else
1982					{
1983						ShowAlertDialog( 'ข้อผิดพลาด', 'ตรวจพบปัญหาในการบันทึกรายงานของคุณ โปรดลองใหม่อีกครั้งภายหลัง' );
1984					}
1985
1986				} );
1987		}
1988		else
1989		{
1990			ShowAlertDialog( 'ข้อผิดพลาด', 'คุณต้องเลือกเหตุผลที่คุณรายงานผลิตภัณฑ์นี้!' );
1991		}
1992	});
1993}
1994
1995function ShowNavigatorShare()
1996{
1997	if ( typeof navigator === 'object' && typeof navigator.share === 'function' )
1998	{
1999		navigator.share( { url: window.location.href } );
2000	}
2001	else if ( 'ReactNativeWebView' in window && typeof window.ReactNativeWebView === 'object' && typeof window.ReactNativeWebView.postMessage === 'function' )
2002	{
2003		window.ReactNativeWebView.postMessage( JSON.stringify( { event_name: 'share', link: window.location.href } ) );
2004	}
2005}
2006
2007function ShowGotSteamModal( strSteamURL, strAppName, strPlayLaunchVerb )
2008{
2009		var $ModalContent = $J("<dialog class=\"gotsteamModal\">\n\t<div class=\"got_steam_ctn\">\n\t<div class=\"got_steam_box\">\n\t\t<h1>\u0e14\u0e32\u0e27\u0e19\u0e4c\u0e42\u0e2b\u0e25\u0e14 Steam \u0e2b\u0e23\u0e37\u0e2d\u0e22\u0e31\u0e07?<\/h1>\n\t\t<p>\u0e04\u0e38\u0e13\u0e08\u0e33\u0e40\u0e1b\u0e47\u0e19\u0e15\u0e49\u0e2d\u0e07\u0e15\u0e34\u0e14\u0e15\u0e31\u0e49\u0e07<a href=\"https:\/\/store.steampowered.com\/about\/\">\u0e41\u0e2d\u0e1b\u0e1e\u0e25\u0e34\u0e40\u0e04\u0e0a\u0e31\u0e19 Steam \u0e1a\u0e19\u0e40\u0e14\u0e2a\u0e01\u0e4c\u0e17\u0e47\u0e2d\u0e1b<\/a> \u0e01\u0e48\u0e2d\u0e19\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e08\u0e30\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e15\u0e34\u0e14\u0e15\u0e31\u0e49\u0e07\u0e41\u0e25\u0e30\u0e40\u0e23\u0e34\u0e48\u0e21 <strong class=\"gotSteam_AppName\"><\/strong> \u0e44\u0e14\u0e49 \u0e04\u0e38\u0e13\u0e44\u0e14\u0e49\u0e15\u0e34\u0e14\u0e15\u0e31\u0e49\u0e07 Steam \u0e1a\u0e19\u0e04\u0e2d\u0e21\u0e1e\u0e34\u0e27\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e19\u0e35\u0e49\u0e41\u0e25\u0e49\u0e27\u0e43\u0e0a\u0e48\u0e44\u0e2b\u0e21?<\/p>\n\t\t<div class=\"gotsteam_buttons\">\n\t\t\t<a class=\"gotSteam_SteamURL btn_blue leftbtn\" href=\"\">\n\t\t\t\t<h3>\u0e43\u0e0a\u0e48 Steam \u0e15\u0e34\u0e14\u0e15\u0e31\u0e49\u0e07\u0e2d\u0e22\u0e39\u0e48\u0e41\u0e25\u0e49\u0e27<\/h3>\n\t\t\t\t<h5 class=\"gotsteam_action\"><\/h5>\n\t\t\t<\/a>
2009\n\t\t\t<a href=\"https:\/\/store.steampowered.com\/about\/\" class=\"btn_blue\">\n\t\t\t\t<h3>\u0e44\u0e21\u0e48 \u0e09\u0e31\u0e19\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23 Steam<\/h3>\n\t\t\t\t<h5>\u0e14\u0e32\u0e27\u0e19\u0e4c\u0e42\u0e2b\u0e25\u0e14\u0e41\u0e25\u0e30\u0e2d\u0e48\u0e32\u0e19\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a Steam<\/h5>\n\t\t\t<\/a>\n\t\t\t<div style=\"clear: left;\"><\/div>\n\t\t<\/div>\n\t\t<div class=\"got_steam_low_block\">\n\t\t\t<div class=\"gotsteam_steam_ico\"><img src=\"https:\/\/store.fastly.steamstatic.com\/public\/images\/v6\/steam_ico.png\" width=\"40\" height=\"40\" border=\"0\" \/><\/div>\n\t\t\tSteam \u0e04\u0e37\u0e2d\u0e41\u0e1e\u0e25\u0e15\u0e1f\u0e2d\u0e23\u0e4c\u0e21\u0e40\u0e01\u0e21\u0e1a\u0e19\u0e40\u0e14\u0e2a\u0e01\u0e4c\u0e17\u0e47\u0e2d\u0e1b\u0e23\u0e30\u0e14\u0e31\u0e1a\u0e1e\u0e23\u0e35\u0e40\u0e21\u0e35\u0e22\u0e23\u0e4c \u0e2a\u0e21\u0e31\u0e04\u0e23\u0e1f\u0e23\u0e35\u0e41\u0e25\u0e30\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e07\u0e48\u0e32\u0e22 <a href=\"https:\/\/store.steampowered.com\/about\/\">\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 Steam<\/a>\n\t\t<\/div><\/div>\n\t<\/div>\n<\/dialog>");
2010	$ModalContent.find('.gotSteam_AppName').text( strAppName );
2011	$ModalContent.find('.gotsteam_action').text( strPlayLaunchVerb );
2012	$ModalContent.find( '.gotSteam_SteamURL').attr( 'href', strSteamURL );
2013	var Modal = new CModal( $ModalContent );
2014	Modal.Show();
2015	//ShowDialog( 'Got Steam?', $ModalContent );
2016}
2017
2018function ChangeSeason( el, season )
2019{
2020	// switch active season indicator
2021	$J( '.series_seasons .season_selector .season_name' ).removeClass( 'active' );
2022	$J( el ).addClass( 'active' );
2023
2024	// switch which episodes are shown
2025	$J( '.season_episode_list_wrapper .season_episode_list' ).removeClass( 'active' );
2026	$J( '.season_episode_list_wrapper .season_episode_list[data-season="' + season + '"]' ).addClass( 'active' );
2027
2028	// load the images
2029	$J( '.season_episode_list.active .episode_image_col img').each( function( index, value )
2030	{
2031		$J( this ).attr( 'src', $J( this ).data( 'src' ) );
2032	});
2033}
2034
2035function ShowRecommendedMoreInfoModal()
2036{
2037	var strTemplate = "<div class=\"recommended_more_info_modal\">\n\t\t\t\t\t\t\t\t<p class=\"intro\">\u0e40\u0e23\u0e32\u0e44\u0e14\u0e49\u0e1e\u0e1a\u0e1a\u0e32\u0e07\u0e2a\u0e34\u0e48\u0e07\u0e17\u0e35\u0e48\u0e14\u0e39\u0e44\u0e21\u0e48\u0e40\u0e2b\u0e21\u0e37\u0e2d\u0e19\u0e2a\u0e34\u0e48\u0e07\u0e2d\u0e37\u0e48\u0e19\u0e43\u0e14\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e44\u0e14\u0e49\u0e40\u0e04\u0e22\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e21\u0e32\u0e01\u0e48\u0e2d\u0e19\u0e2b\u0e19\u0e49\u0e32\u0e19\u0e35\u0e49 Steam \u0e08\u0e30\u0e40\u0e23\u0e35\u0e22\u0e19\u0e23\u0e39\u0e49\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e1b\u0e23\u0e31\u0e1a\u0e41\u0e15\u0e48\u0e07\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e08\u0e32\u0e01\u0e40\u0e01\u0e21\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e40\u0e25\u0e48\u0e19 \u0e20\u0e32\u0e1e\u0e22\u0e19\u0e15\u0e23\u0e4c\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e23\u0e31\u0e1a\u0e0a\u0e21 \u0e41\u0e25\u0e30\u0e0b\u0e2d\u0e1f\u0e15\u0e4c\u0e41\u0e27\u0e23\u0e4c\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e1a\u0e19 Steam \u0e40\u0e23\u0e32\u0e08\u0e30\u0e43\u0e0a\u0e49\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e2b\u0e25\u0e48\u0e32\u0e19\u0e35\u0e49\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e1b\u0e23\u0e31\u0e1a\u0e41\u0e15\u0e48\u0e07\u0e04\u0e33\u0e41\u0e19\u0e30\u0e19\u0e33\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e43\u0e2b\u0e49\u0e40\u0e2b\u0e21\u0e32\u0e30\u0e01\u0e31\u0e1a\u0e23\u0e2a\u0e19\u0e34\u0e22\u0e21\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13<\/p>\n\t\t\t\t\t\t\t\t<h2>\u0e0a\u0e2d\u0e1a\u0e1c\u0e25\u0e34\u0e15\u0e20\u0e31\u0e13\u0e11\u0e4c\u0e19\u0e35\u0e49?<\/h2>\n\t\t\t\t\t\t\t\t<p>\u0e2b\u0e32\u0e01\u0e04\u0e38\u0e13\u0e0a\u0e2d\u0e1a\u0e1c\u0e25\u0e34\u0e15\u0e20\u0e31\u0e13\u0e11\u0e4c\u0e19\u0e35\u0e49\u0e41\u0e25\u0e30\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e08\u0e30\u0e2a\u0e31\u0e48\u0e07\u0e0b\u0e37\u0e49\u0e2d\u0e41\u0e25\u0e30\u0e40\u0e25\u0e48\u0e19 \u0e40\u0e23\u0e32\u0e08\u0e30\u0e19\u0e33\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e19\u0e35\u0e49\u0e44\u0e1b\u0e1b\u0e23\u0e3
20371\u0e1a\u0e43\u0e0a\u0e49\u0e01\u0e31\u0e1a\u0e1a\u0e31\u0e0d\u0e0a\u0e35\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e14\u0e33\u0e40\u0e19\u0e34\u0e19\u0e01\u0e32\u0e23\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e04\u0e33\u0e41\u0e19\u0e30\u0e19\u0e33\u0e43\u0e19\u0e2d\u0e19\u0e32\u0e04\u0e15<\/p>\n\t\t\t\t\t\t\t\t<h2>\u0e44\u0e21\u0e48\u0e2a\u0e19\u0e43\u0e08\u0e43\u0e19\u0e1c\u0e25\u0e34\u0e15\u0e20\u0e31\u0e13\u0e11\u0e4c\u0e25\u0e31\u0e01\u0e29\u0e13\u0e30\u0e19\u0e35\u0e49?<\/h2>\n\t\t\t\t\t\t\t\t<p>\u0e04\u0e38\u0e13\u0e22\u0e31\u0e07\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e04\u0e48\u0e32\u0e01\u0e32\u0e23\u0e1b\u0e23\u0e31\u0e1a\u0e41\u0e15\u0e48\u0e07\u0e23\u0e49\u0e32\u0e19\u0e04\u0e49\u0e32\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e1a\u0e2d\u0e01 Steam \u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e17\u0e47\u0e01\u0e2b\u0e23\u0e37\u0e2d\u0e1b\u0e23\u0e30\u0e40\u0e20\u0e17\u0e02\u0e2d\u0e07\u0e1c\u0e25\u0e34\u0e15\u0e20\u0e31\u0e13\u0e11\u0e4c\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e2a\u0e19\u0e43\u0e08\u0e44\u0e14\u0e49 <a href=\"https:\/\/store.steampowered.com\/account\/preferences\/\">\u0e01\u0e32\u0e23\u0e40\u0e22\u0e35\u0e48\u0e22\u0e21\u0e0a\u0e21\u0e01\u0e32\u0e23\u0e1b\u0e23\u0e31\u0e1a\u0e41\u0e15\u0e48\u0e07\u0e23\u0e49\u0e32\u0e19\u0e04\u0e49\u0e32<\/a><\/p>\n\t\t\t\t\t\t\t<\/div>";
2038	ShowAlertDialog( "\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e40\u0e15\u0e34\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e04\u0e33\u0e41\u0e19\u0e30\u0e19\u0e33", strTemplate);
2039
2040}
2041
2042function CollapseLongStrings( strSelector )
2043{
2044
2045	$J(strSelector).each(function(i, j){
2046		var $target = $J(j);
2047		if( j.scrollWidth >  $target.innerWidth() )
2048		{
2049			//$target.css({ 'overflow': 'hidden', 'white-space': 'nowrap', 'text-overflow': 'ellipsis' });
2050			var elMoreBtn = document.createElement('div');
2051
2052			elMoreBtn.classList.add('more_btn');
2053			elMoreBtn.textContent = '+';
2054			elMoreBtn.addEventListener('click', function($target, elButton, event){
2055
2056				$target.css({'overflow': 'visible', 'white-space': 'normal'});
2057				elButton.remove();
2058
2059			}.bind(null, $target, elMoreBtn ));
2060
2061			j.parentNode.appendChild(elMoreBtn);
2062		}
2063	})
2064}
2065
2066function JSReportProductAction( appId, pageAction, snr )
2067{
2068	$J.post( 'https://store.steampowered.com//ajaxreportproductaction/' + appId + '/', {
2069			'page_action' : pageAction,
2070			'snr' : snr
2071		}
2072	);
2073}
2074
2075function ToggleBannerContentVisibility( divContentID, divIconID )
2076{
2077	var $element = $J( divContentID );
2078	if ( $element !== null )
2079	{
2080		$element.slideToggle( 'fast' );
2081	}
2082
2083	var $icon = $J( divIconID )
2084	if ( $icon !== null )
2085	{
2086		$icon.toggleClass('expanded');
2087	}
2088};
2089
2090// The review detail section is simplified for mobile screens
2091// which requires some section moves here, and formatting changes in .css
2092function ReparentReviewsForSmallScreens()
2093{
2094	var bUseGamepadScreenMode = window.UseGamepadScreenMode && window.UseGamepadScreenMode();
2095	var fn_reparent = bUseGamepadScreenMode ? Responsive_ReparentItemsInGamepadMode : Responsive_ReparentItemsInMobileMode;
2096
2097	var $MoveReviewSections = $J('.user_reviews_container');
2098	$MoveReviewSections.each( function() {
2099
2100		// move content within each review detail section for small screen layout
2101		var $SummarySection = $J(this).find('[id^="ReviewContent"]');
2102		$SummarySection.each( function() {
2103
2104			var $LeftColContent = $J(this).find('.leftcol');
2105			var $NewLeftLocation = $J(this).find('.responsive_review_leftcol_
2105ctn');
2106			var $ReviewAwardCtn = $J(this).find('.review_award_ctn');
2107			var $NewAwardLocation = $J(this).find('.rightcol');
2108
2109			// if we don't have the required destination containers bail (this shouldn't happen)
2110			if ( $NewLeftLocation.length == 0 || $NewAwardLocation.length == 0 )
2111				return;
2112
2113			fn_reparent( '#' + $LeftColContent.attr('id'), $J( '#' + $NewLeftLocation.attr('id') ) );
2114			fn_reparent( '#' + $ReviewAwardCtn.attr('id'), $J( '#' + $NewAwardLocation.attr('id') ) );
2115		} );
2116	} );
2117}
2118
2119// Share dialog calls this to copy the store page link to the clipboard
2120function ShareDialogCopyToClipboard()
2121{
2122	// select the text and copy to clipboard
2123	var $res = false;
2124	var $linkText = document.getElementById('shareDialogLinkStoreLink');
2125	if ( $linkText )
2126	{
2127		// using .select() instead causes the copy command to fail on iOS 12
2128		$linkText.setSelectionRange(0, $linkText.value.length);
2129		$res = document.execCommand( "copy" );
2130	}
2131
2132	// report the result
2133	$J('#shareDialogResult').text( $res ? 'Link copied' : 'ล้มเหลวในการคัดลอกลิงก์ร้านค้าไปยังคลิปบอร์ด');
2134
2135	// clear the selection
2136	document.getSelection().removeAllRanges();
2137}
2138
2139// A very basic scroll helper which only supports vertical scrolling
2140// Added to translate gamepad navigation events into scroll up/down
2141function ScrollElement( elementID, nAmount )
2142{
2143	var element = $J( elementID )[0];
2144	if ( !element )
2145		return false;
2146
2147	if ( nAmount > 0 && element.scrollTop + element.clientHeight < element.scrollHeight )
2148		element.scrollBy( 0, nAmount );
2149	else if ( nAmount < 0 && element.scrollTop > 0 )
2150		element.scrollBy( 0, nAmount );
2151	else
2152		return false;
2153
2154	return true;
2155}
2156
2157// initially only used by tablet
2158function ShowEarlyAccessModal( contentID )
2159{
2160	var $Content = $J( contentID );
2161	$Content.detach();
2162	$Content.show();
2163
2164	ShowDialog( "\u0e0b\u0e2d\u0e1f\u0e15\u0e4c\u0e41\u0e27\u0e23\u0e4c\u0e23\u0e30\u0e2b\u0e27\u0e48\u0e32\u0e07\u0e01\u0e32\u0e23\u0e1e\u0e31\u0e12\u0e19\u0e32", $Content ).always(
2165		function() {
2166
2167			// save it away again for later
2168			$Content.hide();
2169			$J( document.body ).append( $Content );
2170		}
2171	);
2172}
2173
2174// applies layout changes for mobile and gamepad screens
2175function ReparentAppLandingPageForSmallScreens()
2176{
2177	// copy the review summary content to the review details section
2178	// CSS will set when to make these visible
2179	var $appReviewsAll = $J('#appReviewsAll_responsive').clone();
2180	$appReviewsAll.attr( 'id', 'appReviewsAll_Detail' );
2181	$appReviewsAll.css( 'display', 'none' );
2182	$appReviewsAll.appendTo('.reviews_info_ctn');
2183	var $appReviewsRecent = $J('#appReviewsRecent_responsive').clone();
2184	$appReviewsRecent.attr( 'id', 'appReviewsRecent_Detail' );
2185	$appReviewsRecent.css( 'display', 'none' );
2186	$appReviewsRecent.appendTo('.reviews_info_ctn');
2187
2188	// on iOS use the iOS share icon.  the default is Android.
2189	if ( navigator.userAgent.toLowerCase().indexOf( 'iphone' ) != -1 )
2190	{
2191		$J('#shareImg').attr('src', '../images/icon-share-ios.svg' );
2192	}
2193
2194	var bUseGamepadScreenMode = window.UseGamepadScreenMode && window.UseGamepadScreenMode();
2195
2196	ReparentPurchaseOptionsForGamepad( '#purchaseOptionsContent' );
2197
2198		var fn_reparent = bUseGamepadScreenMode ? Responsive_ReparentItemsInGamepadMode : Responsive_ReparentItemsInMobileMode;
2199
2200	if ( bUseGamepadScreenMode )
2201	{
2202		// move the page header image to the top of purchase options
2203		fn_reparent( $J('#page_header_img'), $J('#purchaseOptionsContent') );
2204	}
2205
2206	// move early access content into the purchase options parent
2207	if ( bUseGamepadScreenMode )
2208	{
2209		// on tablet we provide a learn more link which opens a dialog containing early access details
2210		fn_reparent( $J('#earlyAccessBody'), $J('#earlyAccessTabletDialogContent') );
2211		fn_reparent( $J('#earlyAccessTabletContent'), $J('#earlyAccessHeader_Gamepad') );
2212	}
2213	else
2214	{
2215		fn_reparent( '.early_access_header', $J('#purchaseOptionsContent') );
2216	}
2217
2218	// move purchase options into a new container for responsive UX
2219	if ( !bUseGamepadScreenMode )
2220	{
2221		fn_reparent( '#game_area_purchase', $J( '#purchaseOptionsContent' ) );
2222	}
2223	else
2224	{
2225		// Hide it
2226		$J( '#game_area_purchase' ).hide();
2227	}
2228
2229	// order the action buttons
2230	fn_reparent( '#reportBtn', $J('#rowBtnActions') );
2231	fn_reparent( '#queueBtnFollow', $J('#rowBtnActions') );
2232	fn_reparent( '#ignoreBtn', $J('#rowBtnActions') );
2233	fn_reparent( '#shareBtn', $J('#rowBtnActions') );
2234	fn_reparent( '#rowBtnActions', $J('#queueActionsCtn') );
2235
2236	// place discovery queue below the action buttons
2237	fn_reparent( '#nextInDiscoveryQueue', $J('#queueCtn') );
2238
2239	// place banners and game details into the links and info section
2240	fn_reparent( '#bannerAchievements', $J( '#appLinksAndInfo' ) );
2241	fn_reparent( '#bannerPointsShop', $J( '#appLinksAndInfo' ) );
2242	fn_reparent( '#bannerItemStore', $J( '#appLinksAndInfo' ) );
2243	fn_reparent( '#bannerCommunity', $J( '#appLinksAndInfo' ) );
2244
2245	// on tablet these go into a dropdown
2246	fn_reparent( '#appDetailsUnderlinedLinks', bUseGamepadScreenMode ? $J( '#appLinksAndInfo_TabletDropdownContent' ) : $J( '#appLinksAndInfo' ) );
2247
2248	// place the active review filter list in the review details section
2249	fn_reparent( '#reviews_active_filters', $J('.reviews_info_ctn') );
2250
2251	// populate the reviews settings popup
2252	fn_reparent( '#review_histograms_container', $J( '#reviewSettingsPopupContent' ) );
2253	fn_reparent( '#reviews_filter_options', $J( '#reviewSettingsPopupContent' ) );
2254
2255	// move some of the links and info content to the bottom
2256	// testing this - we may need to move this again
2257	fn_reparent( '#genresAndManufacturer', $J( '#appLinksAndInfo' ) );
2258
2259	// the window resize message handler is for layout adjustements that require more logic than reparenting based on screen size
2260	var defaultShareFlex = $J('#shareBtn').css('flex-grow');
2261	var defaultFollowFlex = $J('#queueBtnFollow').css('flex-grow');
2262	var defaultReportFlex = $J('#reportBtn').css('flex-grow');
2263	var defaultLanguageTableDisplay = $J('#languageTable').css('display');
2264
2265	var msgWatch = bUseGamepadScreenMode ? 'Responsive_GamepadScreenModeToggled' : 'Responsive_MobileScreenModeToggled';
2266	$J(window).on( msgWatch, function() {
2267
2268		var bUseNewUX = ( bUseGamepadScreenMode || ( window.UseMobileScreenMode && window.UseMobileScreenMode() ) );
2269
2270		// if one of the wishlist buttons are visible make the action buttons flex grow so the two rows of buttons match width.
2271		if ( bUseNewUX && ( $J('#add_to_wishlist_area').is(':visible')
2272			|| $J('#add_to_wishlist_area_success').is(':visible')
2273			|| $J('#add_to_wishlist_area_fail').is(':visible') ) )
2274		{
2275			$J('#shareBtn').css('flex-grow', '1');
2276			$J('#queueBtnFollow').css('flex-grow', '1');
2277			$J('#reportBtn').css('flex-grow', '1');
2278		}
2279		else
2280		{
2281			$J('#shareBtn').css('flex-grow', defaultShareFlex);
2282			$J('#queueBtnFollow').css('flex-grow', defaultFollowFlex);
2283			$J('#reportBtn').css('flex-grow', defaultReportFlex);
2284		}
2285
2286		// if we're in a small screen layout reveal all language options within the languageTable
2287		// don't bother trying to re-hide these if the user later resizes outside small screen
2288		if ( bUseNewUX )
2289		{
2290			var $LanguageOptions = $J('#languageTable').find('tr');
2291			$LanguageOptions.each( function() {
2292				$J(this).css('display','');
2293			} );
2294
2295			// hide the 'see all X languages" link since all languages are now visible
2296			$J('#languageTable').find('.all_languages').css('display', 'none');
2297		}
2298		else
2299		{
2300			$J('#reviews_active_filters').attr( 'role', 'group' );
2301		}
2302
2303		// hide the language table on small screens since a banner is clicked to reveal it
2304		//
2305		// putting a display:none in CSS for small screen sizes doesn't work well because the
2306		// CSS values don't override the display state set by the language banner click handler
2307		$J('#languageTable').css('display', bUseNewUX ? 'none' : defaultLanguageTableDisplay );
2308	});
2309	$J(window).trigger( msgWatch );
2310
2311	// the vertical space available to display purchase options can change when the page scrolls.  Check if we need to update the height.
2312	if ( bUseGamepadScreenMode )
2313		$J(window).on( 'scroll', UpdateGamepadPurchaseOptionsHeight );
2314}
2315
2316// Update the height of the sticky, scrollable purchase options container.
2317function UpdateGamepadPurchaseOptionsHeight()
2318{
2319	if ( window.UseGamepadScreenMode && window.UseGamepadScreenMode() )
2320	{
2321		let $purchaseContentHeight = parseInt( window.innerHeight ) - parseInt( GetResponsiveHeaderFixedOffsetAdjustment() ) - parseInt( $J('#purchaseOptionsContentTablet')[0].getBoundingClientRect().top );
2322		$purchaseContentHeight += 'px';
2323		let $stickyDiv = $J('#purchaseOptionsContentTablet');
2324		if ( $stickyDiv && $stickyDiv.css( 'height') != $purchaseContentHeight )
2325		{
2326			$stickyDiv.css( 'height', $purchaseContentHeight );
2327		}
2328	}
2329}
2330
2331function ReparentPurchaseOptionsForGamepad( idPurchaseOptions )
2332{
2333	// gamepad has its own purchase options container (shown on right side of screen)
2334	if ( window.UseGamepadScreenMode && window.UseGamepadScreenMode() )
2335	{
2336		Responsive_ReparentItemsInGamepadMode( idPurchaseOptions, $J('#purchaseOptionsContentTablet') );
2337
2338		$J('#purchaseOptionsContentTablet').css('top', parseInt( GetResponsiveHeaderFixedOffsetAdjustment() ) + 'px' );
2339		$J('#purchaseOptionsContentTablet').css('scrollTop', 0);
2340		UpdateGamepadPurchaseOptionsHeight();
2341	}
2342}
2343
2344
2345// Popup for customers to optin/out of the new mobile UX
2346var g_newMobileUXPopup = null;
2347function ShowUseNewMobileUXPopup()
2348{
2349	var $ModalContent = $J('#popupNewMobileUX');
2350	if ( 0 == $ModalContent.length )
2351		return;
2352
2353	g_newMobileUXPopup = new CModal( $ModalContent );
2354	g_newMobileUXPopup.Show();
2355}
2356
2357// check if customer selection for viewing the new mobile UX has changed
2358function OnNewMobileUXPopupClosed()
2359{
2360	if ( typeof g_newMobileUXPopup !== 'undefined' )
2361		g_newMobileUXPopup.Dismiss();
2362}
2363
2364// called by Mobile UX to show a settings dialog with Review filters
2365var g_reviewSettingsPopup = null;
2366function ShowReviewSettingsModal()
2367{
2368	// bail if we can't find the popup content, or it's not populated
2369	var $ModalContent = $J('#reviewSettingsPopupCtn');
2370	if ( 0 == $ModalContent.length || 0 == $ModalContent.find('#reviews_filter_options').length )
2371	{
2372		return;
2373	}
2374
2375	g_reviewSettingsPopup = new CModal( $ModalContent );
2376	g_reviewSettingsPopup.Show();
2377}
2378function CloseReviewSettingsModal()
2379{
2380	if ( typeof g_reviewSettingsPopup !== 'undefined' )
2381		g_reviewSettingsPopup.Dismiss();
2382}
2383
2384function BindFocusVideoOnTablet()
2385{
2386	$J( '.highlight_movie' ).on( "focusout", function( event ) {
2387		this.pause();
2388	} );
2389}
2390
2391function AddRightNavStickyPaddingOnTablet()
2392{
2393	const elSticky = document.querySelector('.purchase_options_content_tablet' );
2394	const observer = new IntersectionObserver(
2395		([e]) => e.target.classList.toggle('isCurrentlySticky', e.isIntersecting ),
2396		{threshold: [1]}
2397	);
2398	observer.observe( elSticky )
2399}
2400
2401function ToggleShowAllPackageContentsText( event )
2402{
2403	const elContainer = event.target.parentNode.parentNode;
2404	elContainer.classList.toggle( 'package_contents_collapsed');
2405}
2406
2407function SetupReviewFilterMenus()
2408{
2409	[...document.querySelectorAll( "#reviews_filter_options button[aria-expanded]" )].forEach( ( button ) => {
2410		const flyout = button.ariaControlsElements?.[0] ?? document.getElementById(button.getAttribute("aria-controls"));
2411		button.addEventListener( "click", () => {
2412			if ( button.ariaExpanded === "true" )
2413			{
2414				button.ariaExpanded = "false";
2415			}
2416			else {
2417				button.ariaExpanded = "true";
2418			}
2419
2420			const currentFocus = document.activeElement;
2421			let nextFocusTarget = flyout?.firstElementChild;
2422			const visited = new Set();
2423			while ( nextFocusTarget && nextFocusTarget !== flyout )
2424			{
2425				nextFocusTarget.focus();
2426				if ( document.activeElement !== currentFocus )
2427				{
2428					// successfully moved focus
2429					break;
2430				}
2431				visited.add( nextFocusTarget );
2432				if ( nextFocusTarget.firstElementChild && !visited.has( nextFocusTarget.firstElementChild ) )
2433				{
2434					nextFocusTarget = nextFocusTarget.firstElementChild;
2435				}
2436				else if ( nextFocusTarget.nextElementSibling && !visited.has( nextFocusTarget.nextElementSibling ) )
2437				{
2438					nextFocusTarget = nextFocusTarget.nextElementSibling;
2439				}
2440				else
2441				{
2442					nextFocusTarget = nextFocusTarget.parentElement;
2443				}
2444			}
2445		} );
2446
2447		flyout?.addEventListener( "focusout", ( e ) => {
2448			if ( !flyout.contains( e.relatedTarget ) )
2449			{
2450				button.ariaExpanded = "false";
2451			}
2452		} );
2453	} );
2454
2455	// $J( 'input[name="review_topic"]' ).not( '[always-show]' ).each( function()
2456	// {
2457	// 	$J(this).hide();
2458	// 	$J('label[for="' + this.id + '"]').hide();
2459	// } );
2460
2461}
2462
2463function CheckClientBootProtection( strType )
2464{
2465	let element = document.querySelector( `[data-bootprotection="${ strType }"]` );
2466	if ( !element )
2467		return;
2468
2469	if ( typeof SteamClient === "undefined" || !SteamClient.Compat || !SteamClient.Compat.CheckBootProtectionEnabled )
2470		return;
2471
2472	if ( strType != 'secureboot_tpm2' )
2473		return;
2474
2475	SteamClient.Compat.CheckBootProtectionEnabled( '2', true ).then( ( bEnabled ) => {
2476		if ( !bEnabled )
2477			element.style.display = 'block';
2478	} );
2479}
2480
2481// Go through images on the store page and add:
2482// - Click to play/pause for animated images.
2483// - Magnifying glass to zoom in when an image has a bigger version available.
2484$J(document).ready(function() {
2485	// TODO: add back initial paused state is based reduced motion preference. We were using
2486	// `window.matchMedia("(prefers-reduced-motion: reduce)").matches` but it turns out that this is set when users
2487	// don't expect it, such as if they have some animations turned off in Windows or other things. We're going to
2488	// add a setting to client and then we can plumb it through here, which should help.
2489	const bInitiallyPaused = false;
2490	let bPaused = bInitiallyPaused;
2491
2492	function addActionBar($el) {
2493		const $iconArea = $J(`<div class='bb_img_icons' />`);
2494		$el.append($iconArea);
2495		return $iconArea;
2496	}
2497
2498	function addPlayPauseIcon($outerDiv, $actionBar) {
2499		const strTooltip = "\u0e40\u0e1a\u0e23\u0e32\u0e27\u0e4c\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e40\u0e1b\u0e34\u0e14\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e01\u0e32\u0e23\u0e15\u0e31\u0e49\u0e07\u0e04\u0e48\u0e32\u0e01\u0e32\u0e23\u0e25\u0e14\u0e01\u0e32\u0e23\u0e40\u0e04\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e2b\u0e27\u0e1a\u0e19\u0e2b\u0e19\u0e49\u0e32\u0e08\u0e2d\u0e44\u0e27\u0e49 \u0e14\u0e31\u0e07\u0e19\u0e31\u0e49\u0e19\u0e20\u0e32\u0e1e\u0e40\u0e04\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e2b\u0e27\u0e19\u0e35\u0e49\u0e08\u0e36\u0e07\u0e2b\u0e22\u0e38\u0e14\u0e0a\u0e31\u0e48\u0e27\u0e04\u0e23\u0e32\u0e27\u0e15\u0e32\u0e21\u0e04\u0e48\u0e32\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19";
2500		const strPanel = 'data-panel="{&quot;focusable&quot;:true,&quot;clickOnActivate&quot;:true}" role="button"';
2501		const $btnPlayPause = $J(`<div class='bb_img_play_pause' ${strPanel} data-tooltip='${strTooltip}' data-tooltip-visible=${bInitiallyPaused}>
2502			<img width="18" height="18" draggable="false" />
2503		</div>`);
2504		$actionBar.append($btnPlayPause);
2505
2506		$btnPlayPause.on("click", function(e) {
2507			e.stopPropagation(); // so it doesn't double trigger outer click
2508			$outerDiv.trigger("click");
2509		});
2510
2511		return $btnPlayPause;
2512	}
2513
2514	function addExpandIcon($actionBar, $img, bigSrc) {
2515		const strPanel = 'data-panel="{&quot;focusable&quot;:true,&quot;clickOnActivate&quot;:true}" role="button"';
2516		const $btnExpand = $J(`<div class='bb_img_expand' ${strPanel}>🔎</div>`);
2517		$actionBar.append($btnExpand);
2518
2519		$btnExpand.on("click", function(e) {
2520			const newSrc = bigSrc && bigSrc.length ? bigSrc : $img.attr("src");
2521			window.open(newSrc, "_blank");
2522		});
2523
2524		return $btnExpand;
2525	}
2526
2527	function updatePlayPauseStyle($el, bPaused, bInitial) {
2528		$el.toggleClass("paused", bPaused);
2529		$el.toggleClass("playing", !bPaused);
2530		const $img = $el.find('img');
2531		$img.attr('src', bPaused ? "/public/images/ico/media_playback/play.svg" : "/public/images/ico/media_playback/pause.svg");
2532		$img.attr('alt', bPaused ? "\u0e40\u0e25\u0e48\u0e19\u0e41\u0e2d\u0e19\u0e34\u0e40\u0e21\u0e0a\u0e31\u0e19" : "\u0e2b\u0e22\u0e38\u0e14\u0e41\u0e2d\u0e19\u0e34\u0e40\u0e21\u0e0a\u0e31\u0e19\u0e0a\u0e31\u0e48\u0e27\u0e04\u0e23\u0e32\u0e27");
2533		if (!bInitial) {
2534			// hide the initial tooltip, that should only show if the image started paused and has never been played
2535			$el.removeAttr("data-tooltip-visible");
2536		}
2537	}
2538
2539	function updatePlayPauseOnAll()
2540	{
2541		bPaused = !bPaused;
2542
2543		$J(".bb_img_ctn").each(function() {
2544			const $outerDiv = $J(this);
2545
2546			const $pic = $outerDiv.find("picture");
2547			const $video = $outerDiv.find("video");
2548
2549			const bVideoWithSources = $video.length !== 0 && $video.find("source").length > 0;
2550
2551			const $btnPlayPause = $outerDiv.find('.bb_img_play_pause');
2552
2553			if ($pic.length > 0) {
2554				// handle click-to-play for picture elements
2555				const $source = $pic.find("source");
2556				const $img = $pic.find("img");
2557
2558				const pausedSrc = $img.attr("src");
2559				let animatedSrc = $source.attr("srcset");
2560
2561				// store the source's srcset the first time we come through here so we have it after we change it
2562				if (!$source.data('original-srcset')) {
2563					$source.data('original-srcset', animatedSrc);
2564				} else {
2565					animatedSrc = $source.data('original-srcset');
2566				}
2567
2568				// on first click, drop the media attribute so the source always applies
2569				if ($source.attr("media")) {
2570					$source.removeAttr("media");
2571				}
2572
2573				$source.attr("srcset", bPaused ? pausedSrc : animatedSrc);
2574				updatePlayPauseStyle($btnPlayPause, bPaused, false);
2575
2576				// force re-evaluation by replacing <img src> with a dummy and then back
2577				const temp = $img.attr("src");
2578				$img.attr("src", "").attr("src", temp);
2579			} else if (bVideoWithSources) {
2580				// handle click-to-play for video elements
2581				const elVideo = $video[0];
2582
2583				if (bPaused) {
2584					elVideo.pause();
2585				} else {
2586					elVideo.play();
2587				}
2588
2589				updatePlayPauseStyle($btnPlayPause, bPaused, false);
2590			}
2591		});
2592	}
2593
2594	$J(".bb_img_ctn").each(function() {
2595		const $outerDiv = $J(this);
2596
2597		const $pic = $outerDiv.find("picture");
2598		const $video = $outerDiv.find("video");
2599		const $img = $outerDiv.find("img");
2600
2601		const $actionBar = addActionBar($outerDiv);
2602
2603		// only imgs can have expand buttons
2604		let $btnExpand = null;
2605		const nColumnWidth = 780;
2606		if ($img.length !== 0 && ($img.data("big-src") !== undefined || $img.attr("width") > nColumnWidth * 1.25)) {
2607			$btnExpand = addExpandIcon($actionBar, $img, $img.data("big-src"));
2608		}
2609
2610		const bVideoWithSources = $video.length !== 0 && $video.find("source").length > 0;
2611
2612		// only picture and video elements have play/pause buttons
2613		let $btnPlayPause = null;
2614		if ($pic.length !== 0 || bVideoWithSources) {
2615			$btnPlayPause = addPlayPauseIcon($outerDiv, $actionBar);
2616			updatePlayPauseStyle($btnPlayPause, bPaused, true);
2617		}
2618
2619		if ($pic.length > 0) {
2620			// handle click-to-play for picture elements
2621			$outerDiv.on("click", function() {
2622				updatePlayPauseOnAll();
2623			});
2624		} else if (bVideoWithSources) {
2625			// handle click-to-play for video elements
2626			const elVideo = $video[0];
2627
2628			if (bPaused) {
2629				elVideo.removeAttribute("autoplay");
2630			}
2631
2632			$outerDiv.on("click", function() {
2633				updatePlayPauseOnAll();
2634			});
2635		}
2636	});
2637});
2638

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.