Create attractive VASL scenarios, with loads of useful information embedded to assist with game play. https://vasl-templates.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
vasl-templates/vasl_templates/webapp/static/scenarios.js

1475 lines
61 KiB

/* jshint esnext: true */
( function() { // nb: put the entire file into its own local namespace, global stuff gets added to window.
var gIsFirstSearch ;
var $gDialog, $gScenariosSelect, $gSearchQueryInputBox, $gScenarioCard, $gFooter ;
var $gImportControl, $gDownloadsButton, $gImportScenarioButton, $gConfirmImportButton, $gCancelImportButton, $gImportWarnings ;
// At time of writing, there are ~8600 scenarios, and loading them all into a select2 makes it a bit sluggish.
// The problem is when the user types the first 1 or 2 characters of the search query, which can result in
// thousands of results being loaded into the DOM. We work-around this by limiting the number of results
// shown for these very short query strings.
// An index is built of search results for very short query strings (e.g. "a" or "th"), with the additional
// requirement that the query string must appear at the start of a word (so "th" will match "the", but not "with").
// This index also means that we can return results for these short query strings quickly, since we don't
// have to scan through all the scenarios looking for matches.
// The only down-side is that the search results shown to the user may change radically when we switch
// from using the prefix index to a normal substring search, but we can live with that.
var gPrefixIndex = null ;
const PREFIX_SIZE = 3 ;
// --------------------------------------------------------------------
window.searchForScenario = function()
{
// initialize
var $dlg ;
var eventHandlers = new jQueryHandlers() ;
// NOTE: We have to get the scenario index before we can do anything.
getScenarioIndex( function( scenarios, isNewScenarios ) {
// show the dialog
$( "#scenario-search" ).dialog( {
title: "Search for scenarios",
dialogClass: "scenario-search",
modal: true,
closeOnEscape: false, // nb: handled in handle_escape()
width: $(window).width() * 0.8,
minWidth: 760,
height: $(window).height() * 0.8,
minHeight: 400,
position: { my: "center center", at: "center center", of: window },
create: function() {
initDialog( $(this), scenarios ) ;
// FUDGE! This works around a weird layout problem. The very first time the dialog opens,
// the search input box (the whole .select2-dropdown, actually) is too far left. The layout
// fixes itself on the first keypress, but we adjust the initial position here.
$(this).find( ".select2-dropdown" ).css( "left", 10 ) ;
},
open: function() {
// initialize
$dlg = $(this) ;
if ( isNewScenarios )
initSearchResults( $dlg, scenarios ) ;
// reset everything
$gSearchQueryInputBox.val( "" ) ;
$gDialog.find( ".select2-results__option" ).remove() ;
updateForSearchResults() ;
$gScenarioCard.empty() ;
// NOTE: We don't hide the footer since we want it to take up its space in the layout,
// so that the list of search results is the correct height.
$gFooter.css( "opacity", 0 ) ;
$gImportWarnings.empty().hide() ;
$gDownloadsButton.button( "disable" ) ;
$gImportScenarioButton.show() ;
$gConfirmImportButton.hide() ;
$gCancelImportButton.hide() ;
updateLayout() ;
gIsFirstSearch = true ;
gActiveScenaridCardRequest = null ;
gScenarioCardRequestTimerId = null ;
},
close: function() {
// clean up
eventHandlers.cleanUp() ;
},
resize: updateLayout,
} ) ;
} ) ;
} ;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function initDialog( $dlg, scenarios )
{
// initialize
$gDialog = $dlg ;
fixup_external_links( $dlg ) ;
$gScenarioCard = $dlg.find( ".scenario-card" ) ;
$gImportControl = $dlg.find( ".import-control" ) ;
$gDownloadsButton = $dlg.find( "button.downloads" ).button()
.on( "click", onDownloads ) ;
$gImportScenarioButton = $dlg.find( "button.import" ).button()
.on( "click", function() {
if ( ! is_scenario_dirty() )
onImportScenario() ;
else {
ask( "Import scenario",
"<p> Your scenario has been changed. <p> Do you want to import a new scenario, and lose your changes?", {
width: 470,
ok: onImportScenario,
} ) ;
}
} ) ;
$gConfirmImportButton = $dlg.find( "button.confirm-import" ).button()
.on( "click", onConfirmImportScenario ) ;
$gCancelImportButton = $dlg.find( "button.cancel-import" ).button()
.on( "click", onCancelImportScenario ) ;
$gImportWarnings = $dlg.find( ".import-control .warnings" ) ;
$gFooter = $dlg.find( ".footer" ) ;
// initialize the splitter
Split( [ $dlg.find( ".left" )[0], $dlg.find( ".right" )[0] ], {
sizes: [ 30, 70 ],
direction: "horizontal",
gutterSize: 3,
onDrag: updateLayout,
} ) ;
var $gripper = $( "<img src='" + gImagesBaseUrl + "/gripper-vert.png'>" ) ;
$dlg.find( ".gutter.gutter-horizontal" ).append( $gripper ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function initSearchResults( $dlg, scenarios )
{
// initialize the search results
initPrefixIndex( scenarios ) ;
var options = [] ;
scenarios.forEach( function( scenario ) {
options.push( {
id: scenario.scenario_id,
text: scenario.scenario_name, // nb: this will always have something
scenario: scenario,
} ) ;
} ) ;
sortScenarios( options ) ;
// load the search results
if ( $gScenariosSelect ) {
// clean up the previous select2
$gScenariosSelect.empty().select2( "destroy" ) ;
}
$gScenariosSelect = $dlg.find( ".scenarios select" ) ;
$gScenariosSelect.select2( {
data: options,
matcher: isMatchingItem, sorter: sortItems, templateResult: formatItem,
width: "100%",
closeOnSelect: false,
dropdownParent: $dlg.find( ".scenarios" ),
} ) ;
// stop the select2 droplist from closing up
$gScenariosSelect.select2( "open" ) ;
$gScenariosSelect.on( "select2:closing", function( evt ) {
stopEvent( evt ) ;
} ) ;
// keep the UI up-to-date as items are selected
$gScenariosSelect.on( "select2:select", function( evt ) {
onItemSelected( evt.params.data.id ) ;
} ) ;
// keep the UI up-to-date as items are selected
$gSearchQueryInputBox = $dlg.find( ".select2-search__field" ) ;
$gSearchQueryInputBox.on( "input", function() {
// FUDGE! select2 rebuilds the list of matching items, and selects the first one,
// but doesn't send us a "select" event for it - we do things manually here :-/
var $elem = $( ".select2-results__option--highlighted .search-result" ) ;
onItemSelected( $elem.attr( "data-id" ) ) ;
updateForSearchResults() ;
// FUDGE! Undo the positioning hack we did in the "create" handler.
$dlg.find( ".select2-dropdown" ).css( "left", 0 ) ;
} ) ;
// handle Up and Down key-presses
$gSearchQueryInputBox.on( "keydown", function( evt ) {
if ( evt.keyCode == $.ui.keyCode.UP || evt.keyCode == $.ui.keyCode.DOWN ) {
// NOTE: We don't want to refresh the scenario card if it's not necessary (e.g. after the user
// presses UP when already at the top of the list), since it causes flickering (because some
// elements in the scenario card fade in/out). We seem to get this event *after* the selection
// has already changed in the search result, so we compare the currently-selected item
// with what's currently showing in the scenario card to decide if anything's changed.
var currId = $dlg.find( ".scenario-card" ).data( "scenario" ).scenario_id ;
var $elem = $( ".select2-results__option--highlighted .search-result" ) ;
if ( $elem.attr( "data-id" ) != currId )
onItemSelected( $elem.attr( "data-id" ) ) ;
}
} ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function initPrefixIndex( scenarios )
{
function addEntry( key, scenario ) {
if ( gPrefixIndex[ key ] === undefined )
gPrefixIndex[ key ] = {} ;
gPrefixIndex[ key ][ scenario.scenario_id ] = true ;
}
// build the prefix index
gPrefixIndex = {} ;
scenarios.forEach( function( scenario ) {
// get the searchable text for the next scenario (and cache it)
scenario._searchText = makeSearchText( scenario ) ;
// add each word to the prefix index
var words = scenario._searchText.split( " " ) ;
words.forEach( function( word ) {
if ( word.length < 3 )
return ; // nb: ignore short words
for ( var i=1 ; i <= PREFIX_SIZE ; ++i )
addEntry( word.substring(0,i).toLowerCase(), scenario ) ;
} ) ;
} ) ;
}
// --------------------------------------------------------------------
function isMatchingItem( params, item )
{
// NOTE: This function is called by the select2 to decide if an item should be shown.
// check if an item should be shown
if ( ! params.term )
return null ; // nb: we don't show anything if there is no query string
var termLC = params.term.trim().toLowerCase() ;
if ( termLC.length <= PREFIX_SIZE ) {
// seaerch the prefix index
if ( gPrefixIndex[termLC] && gPrefixIndex[termLC][item.id] )
return item ;
} else {
// search for a matching substring
if ( item.scenario._searchText.indexOf( termLC ) !== -1 )
return item ;
}
return null ;
}
function makeSearchText( scenario )
{
// return the text that will be searched upon
var val = scenario.scenario_name.trim().toLowerCase() ;
val = val.replace( /\s{2,}/g, " " ) ;
return val ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function sortItems( items )
{
// NOTE: This function is called by the select2 to sort the items being shown.
// NOTE: We used to sort the items alphabetically here, but this could cause a new item to appear
// at the top of the list, which we want to be selected. It was ridiculously difficult to figure out
// how to select an item:
// $gScenariosSelect.select2( "trigger", "select", {
// data: { id: items[0].id }
// } ) ;
// but unfortunately, this slows things down a lot in Chrome (everything flies in Firefox).
// We really need to present the scenarios in alphabetical order (so that scenarios with the same name
// are grouped together), but we can achieve that same effect by loading them into the select2
// in alphabetical order.
return items ;
}
function sortScenarios( options )
{
// NOTE See sortItems() for why we load the scenarios in alphabetical order.
function getSortVal( text ) {
text = text.trim().toLowerCase() ;
if ( text[0] == '"' || text[0] == "'" )
text = text.substring( 1 ) ;
if ( text[0] == "\u00a1" || text[0] == "\u00bf" ) // nb: inverted ! and ?
text = text.substring( 1 ) ;
if ( text.substring( 0, 3 ) == "..." )
text = text.substring( 3 ) ;
return text ;
}
options.sort( function( lhs, rhs ) {
return getSortVal( lhs.text ).localeCompare( getSortVal( rhs.text ) ) ;
} ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function formatItem( opt )
{
// NOTE: This function is called by the select2 to format items being shown.
// initialize
if ( ! opt.id )
return opt.text ;
var scenario = opt.scenario ;
function addVal( val, className, prefix, postfix ) {
if ( val ) {
buf.push( "<span class='"+className+"'>" ) ;
if ( prefix )
buf.push( prefix ) ;
buf.push( val ) ;
if ( postfix )
buf.push( postfix ) ;
buf.push( "</span>" ) ;
}
}
// generate the search result
const nowrap = "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" ;
var buf = [ "<div class='search-result' data-id='", scenario.scenario_id, "'>" ] ;
buf.push( "<div>" ) ;
addVal( scenario.scenario_name ) ;
addVal( scenario.scenario_display_id, "scenario-id", " (", ")" ) ;
buf.push( "</div>" ) ;
if ( scenario.scenario_location ) {
buf.push( "<div style='" + nowrap + "'>" ) ;
addVal( scenario.scenario_location, "scenario-location" ) ;
addVal( scenario.scenario_date, "scenario-date", " (", ")" ) ;
buf.push( "</div>" ) ;
}
if ( scenario.publication_name ) {
buf.push( "<div style='" + nowrap + "'>" ) ;
addVal( scenario.publication_name, "publication-name" ) ;
addVal( scenario.publisher_name, "publisher-name", " (", ")" ) ;
if ( scenario.publication_date ) {
addVal( scenario.publication_date.substring(5,7) + "/" + scenario.publication_date.substring(0,4),
"publication-date", " (", ")"
) ;
}
buf.push( "</div>" ) ;
}
buf.push( "</div>" ) ;
// check if this is the first time we're showing search results
if ( gIsFirstSearch ) {
$gFooter.fadeTo( 5*1000, 0.8 ) ;
gIsFirstSearch = false ;
}
return $( buf.join("") ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function updateForSearchResults()
{
// check if there were any search results
$gDialog.find( ".select2-results__message" ).each( function() {
if ( $(this).text() == "No results found" ) {
// nope - update the UI
$(this).hide() ;
onItemSelected( null ) ;
}
} ) ;
// update the import control
var hasSearchResults = $gDialog.find( ".select2-results .search-result" ).length > 0 ;
$gImportScenarioButton.button( hasSearchResults ? "enable" : "disable" ) ;
$gImportControl.css( { "border-top-color": hasSearchResults ? "#666": "#ccc" } ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function onItemSelected( scenarioId )
{
// update the UI
onCancelImportScenario() ;
// load the specified scenario
if ( ! scenarioId ) {
$gScenarioCard.empty().data( "scenario", null ) ;
$gDownloadsButton.button( "disable" ) ;
return ;
}
// NOTE: We pass "auto-match" as the ROAR override, to tell the server to try to find
// a matching ROAR scenario.
loadScenarioCard( $gScenarioCard, scenarioId, true, null, "auto-match", false,
function( scenario ) {
if ( scenario.downloads && scenario.downloads.length > 0 )
$gDownloadsButton.button( "enable" ).data( "scenario", $gScenarioCard.data("scenario") ) ;
else
$gDownloadsButton.button( "disable" ) ;
// update the layout
updateLayout() ;
// NOTE: We set focus to the query input box so that UP/DOWN will work
// after clicking on a search result.
$gSearchQueryInputBox.focus() ;
},
function( xhr, status, errorMsg ) {
$gScenarioCard.html( "Can't get the scenario card:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
}
) ;
}
// --------------------------------------------------------------------
// It's quite easy for there to be multiple requests for scenario cards in progress at the same time
// e.g. if the user holds down the arrow keys to scroll through the search results. We try to optimize
// the process by ignoring all responses except for the most recently requested scenario card. We could
// count the number of active requests pending, but this will end up showing the wrong scenario card
// if the responses come back in a different order.
// So, we instead remember the ID of the most recently requested scenario card, and load that
// into the UI when it arrives. We will load the "wrong" response if the user requests, for example,
// scenarios 1, 2, 3, 2 (i.e. we will load the first "2" response, not the second one), but since
// these things never change, it doesn't actually matter.
var gActiveScenaridCardRequest = null ;
var gScenarioCardRequestTimerId ;
function loadScenarioCard( $target, scenarioId, briefMode, scenarioDateOverride, roarOverride, showRoarButtons, onDone, onError )
{
// NOTE: Loading scenario cards is usually quick, but it can occasionally take some time (especially
// if the query string has just been changed). We show a "loading" spinner if a response hasn't arrived
// within a short amount of time.
if ( ! gScenarioCardRequestTimerId ) {
gScenarioCardRequestTimerId = setTimeout( function() {
$target.html( "<img src='" + gImagesBaseUrl+"/loader.gif" + "'" +
" style='position:absolute;left:45%;top:45%;'>"
) ;
}, 500 ) ;
}
gActiveScenaridCardRequest = scenarioId ;
// initialize
// NOTE: We tag the scenario card with a seq# for the benefit of the test suite, so that it can tell
// when a scenario card has finished loading, and it's safe to read values out of the UI.
// It won't work under load, but we only need it for the simple case of a single request coming in,
// and waiting for it to load completely.
var seqNo = $target.attr( "data-seqNo" ) || 1 ;
// load the specified scenario
var url = gGetScenarioCardUrl.replace( "ID", scenarioId ) ;
if ( briefMode )
url = addUrlParam( url, "brief", 1 ) ;
$.get( url, function( resp ) {
// check if this response is for the most recently requested scenario card
if ( scenarioId != gActiveScenaridCardRequest )
return ;
gActiveScenaridCardRequest = null ;
clearTimeout( gScenarioCardRequestTimerId ) ;
gScenarioCardRequestTimerId = null ;
// NOTE: We used to load the received HTML into the UI here, then get the scenario details,
// but updating the UI with the details when they arrive can cause the layout to change.
// Instead, we hold everything and only update the UI when it's all ready.
var $card = $( resp ) ;
fixup_external_links( $card ) ;
$card.find( ".overview .more" ).on( "click", function() {
$(this).hide() ;
$(this).siblings( ".brief" ).hide() ;
$(this).siblings( ".full" ).fadeIn( 250 ) ;
$gSearchQueryInputBox.focus() ;
} ) ;
function showBoardPreviews( $elem, scenario ) {
// initialize the image gallery
var data = [] ;
scenario.map_images.forEach( function( mapImage ) {
var url = mapImage.screenshot ;
var buf = [] ;
if ( mapImage.user ) {
buf.push( "Contributed by <em>", mapImage.user, "</em>" ) ;
if ( mapImage.timestamp ) {
var tstamp = new Date( mapImage.timestamp ) ;
buf.push( "<div style='font-size:80%;font-style:italic;color:#ccc;'>",
tstamp.toLocaleDateString( undefined, { day: "numeric", month: "long", year: "numeric" } ),
"</div>"
) ;
}
}
data.push( {
src: url, thumb: url,
subHtml: buf.length > 0 ? buf.join("") : url.split("/").pop(),
} ) ;
} ) ;
$elem.lightGallery( { dynamic: true, dynamicEl: data, speed: 250 } ) ;
}
// get the scenario details
getScenarioData( scenarioId, roarOverride, function( scenario ) {
// add the details to the scenario card
insertPlayerFlags( $card, scenario ) ;
makeBalanceGraphs( $card, scenario, showRoarButtons ) ;
loadObaInfo( $card, scenario, scenarioDateOverride ) ;
// initialize the map previews
var $btn = $card.find( ".boards .map-previews" ) ;
if ( ! scenario.map_images || scenario.map_images.length === 0 )
$btn.hide() ;
else {
if ( scenario.map_images.length > 1 )
$btn.after( " <span class='map-preview-count'>(" + scenario.map_images.length + ")</span>" ) ;
$btn.on( "click", function() {
showBoardPreviews( $(this), scenario ) ;
} ) ;
}
// all done - load the card into the UI and notify the caller
$target.data( "scenario", scenario ) ;
$target.html( $card ).fadeIn( 100 ) ;
onDone( scenario ) ;
$target.attr( "data-seqNo", parseInt(seqNo)+1 ) ;
} ) ;
} ).fail( onError ) ;
}
function insertPlayerFlags( $target, scenario )
{
// insert flags for each player
[ "defender", "attacker" ].forEach( function( playerType ) {
var effectiveNat = getEffectivePlayerNat( scenario[ playerType+"_name" ] ) ;
if ( ! effectiveNat )
return ;
var url = make_player_flag_url( effectiveNat[0], false ) ;
$target.find( ".player-info ." + playerType + " .flag" ).html(
$( "<img src='" + url + "'>" )
) ;
} ) ;
}
function loadObaInfo( $target, scenario, scenarioDateOverride )
{
// initialize
var theater = getEffectiveTheater( scenario.theater, scenario ) ;
var scenarioDate = scenario.scenario_date_iso ;
if ( ! theater || ( !scenarioDate && !scenarioDateOverride ) )
return ;
if ( scenarioDateOverride ) {
if ( scenarioDateOverride[3].substring(0,7) == scenarioDate.substring(0,7) )
scenarioDateOverride = null ;
else
scenarioDate = scenarioDateOverride[3] ;
}
var params = {
SCENARIO_THEATER: theater,
SCENARIO_YEAR: scenarioDate.substring( 0, 4 ),
SCENARIO_MONTH: scenarioDate.substring( 5, 7 ),
} ;
// show the OBA info for the defender/attacker
function showInfo( playerType ) {
// get the OBA info
var effectiveNat = getEffectivePlayerNat( scenario[ playerType+"_name" ] ) ;
if ( ! effectiveNat )
return ;
delete params.NAT_CAPS ;
set_nat_caps_params( effectiveNat[0], params ) ;
if ( params.NAT_CAPS === undefined )
params.NAT_CAPS = {} ;
// load the OBA into the scenario card
$target.find( ".oba ." + playerType + " .black" ).text( params.NAT_CAPS.OBA_BLACK || "-" ) ;
$target.find( ".oba ." + playerType + " .red" ).text( params.NAT_CAPS.OBA_RED || "-" ) ;
// show any OBA comments
var $comments = $target.find( ".oba ." + playerType + " .comments" ) ;
if ( params.NAT_CAPS.OBA_COMMENTS ) {
var buf = [] ;
params.NAT_CAPS.OBA_COMMENTS.forEach( function( cmt ) {
buf.push( "<div>", cmt, "</div>" ) ;
} ) ;
$comments.html( buf.join("") ).show() ;
} else {
$comments.hide() ;
}
// update the date warning
if ( scenarioDateOverride ) {
$target.find( ".date-warning .val" ).text(
scenarioDateOverride[1] + "/" + scenarioDateOverride[2].toString().substring(2,4)
) ;
$target.find( ".date-warning" ).show() ;
}
}
showInfo( "defender" ) ;
showInfo( "attacker" ) ;
// NOTE: To stop the OBA panel from flickering on-screen, it is configured to be hidden
// in the template, and we show it here after it has been loaded.
$target.find( ".oba" ).show() ;
}
// --------------------------------------------------------------------
const IMPORT_FIELDS = [
{ key: "scenario_name", name: "scenario name", paramName: "SCENARIO_NAME", type: "text" },
{ key: "scenario_display_id", name: "scenario ID", paramName: "SCENARIO_ID", type: "text" },
{ key: "scenario_location", name: "location", paramName: "SCENARIO_LOCATION", type: "text" },
{ key: "scenario_date_iso", name: "scenario date", paramName: "SCENARIO_DATE", type: "date" },
{ key: "scenario_turns", name: "scenario_turns", paramName: "TURN_TRACK_NTURNS", type: "select2" },
{ key: "theater", name: "theater", paramName: "SCENARIO_THEATER", type: "select2" },
{ key: "defender_name", name: "defender", paramName: "PLAYER_1", type: "player" },
{ key: "defender_desc", name: "defender description", paramName: "PLAYER_1_DESCRIPTION", type: "text" },
{ key: "attacker_name", name: "attacker", paramName: "PLAYER_2", type: "player" },
{ key: "attacker_desc", name: "attacker description", paramName: "PLAYER_2_DESCRIPTION", type: "text" },
] ;
function onImportScenario()
{
var warnings=[], warnings2=[] ;
function getWarnings( scenario ) {
// check if it's OK to import each field
var buf ;
IMPORT_FIELDS.forEach( function( importField ) {
// check for warnings for the next field
var newVal = trimString( scenario[ importField.key ] ) ;
if ( newVal ) {
var msg = eval( "checkImportField_" + importField.type )( importField, newVal, scenario ) ; // jshint ignore: line
if ( msg ) {
if ( msg.substring(0,2) == "!=" )
newVal = msg.substring( 2 ) ;
else {
buf = [ "<div class='warning2'>",
"<img src='" + gImagesBaseUrl + "/warning.gif'>",
msg,
"</div>"
] ;
warnings2.push( $( buf.join("") ) ) ;
return ;
}
}
}
// get the next field's current value
var currVal = eval( "getImportFieldCurrVal_" + importField.type )( importField ) ; // jshint ignore: line
if ( ! currVal )
return ;
var displayCurrVal, extraMsg ;
if ( $.isArray( currVal ) ) {
displayCurrVal = currVal[1] ;
extraMsg = currVal[2] ;
currVal = currVal[0].trim() ;
} else {
currVal = currVal.trim() ;
displayCurrVal = currVal ;
}
// compare the field's current value with what it will be changed to
if ( currVal != newVal ) {
// add a warning that the current value will be changed
var checked = extraMsg ? "" : " checked" ;
buf = [ "<div style='display:flex;align-items:center;'>",
"<input type='checkbox' name='" + importField.key + "'" + checked + ">",
"<span>", "Update the " + importField.name, "</span>"
] ;
if ( displayCurrVal.length <= 20 ) {
buf.push( "&nbsp;", "<span class='hint'>", "(from \"" + displayCurrVal + "\")", "</span>" ) ;
buf.push( "</div>" ) ;
} else {
buf.push( "</div>" ) ;
buf.push( "<div class='hint'>", "Currently \"" + displayCurrVal + "\".", "</div>" ) ;
}
if ( extraMsg )
buf.push( "<div class='hint'>", "<img src='"+gImagesBaseUrl+"/warning.gif'>", extraMsg, "</div>" ) ;
warnings.push( $( buf.join("") ) ) ;
}
} ) ;
}
// check if it will be a clean import
var scenario = $gScenarioCard.data( "scenario" ) ;
getWarnings( scenario ) ;
if ( warnings.length === 0 && warnings2.length === 0 ) {
// yup - do the import
doImportScenario( scenario ) ;
} else {
// nope - show the warnings
if ( warnings.length > 0 ) {
var buf = [
"<div class='header'>",
"<img src='" + gImagesBaseUrl + "/warning.gif'>",
"<div class='caption'> Some values in your scenario will be changed: </div>",
"</div>"
] ;
warnings.unshift( $( buf.join("") ) ) ;
}
if ( warnings2.length > 0 ) {
if ( warnings.length > 0 )
warnings2[0].css( "margin-top", "0.5em" ) ;
$.merge( warnings, warnings2 ) ;
}
$gImportWarnings.empty().append( warnings ).slideToggle( 100 ) ;
$gConfirmImportButton.data( "scenario", scenario ).show() ;
$gCancelImportButton.show() ;
$gDownloadsButton.hide() ;
$gImportScenarioButton.hide() ;
}
}
function doImportScenario( scenario )
{
// reset the turn track
$( "#panel-scenario input" ).each( function() {
if ( $(this).attr( "name" ).substr( 0, 11 ) !== "TURN_TRACK_" )
return ;
var inputType = $(this).attr( "type" ) ;
if ( inputType === "text" )
$(this).val( "" ) ;
else if ( inputType === "checkbox" )
$(this).prop( "checked", false ) ;
} ) ;
// reset the compass
$( "#panel-scenario input[name='COMPASS']" ).val( "" ) ;
updateCompassImage() ;
// NOTE: We could reset the ELR/SAN here, but if the user is importing on top of an existing setup,
// the most likely reason is because they want to connect it to an ASA scenario, not because
// they want to import a whole set of new details, so clearing the ELR/SAN wouldn't make sense.
// Ditto for the scenario turn count, and the associated turn track settings.
// import each field
IMPORT_FIELDS.forEach( function( importField ) {
var $elem = $gDialog.find( ".import-control .warnings input[name='" + importField.key + "']" ) ;
if ( $elem.length > 0 && ! $elem.prop("checked") )
return ;
var newVal = scenario[ importField.key ] ;
eval( "doImportField_" + importField.type )( importField, newVal, scenario ) ; // jshint ignore: line
} ) ;
// update for the newly-connected scenario
updateForConnectedScenario(
scenario.scenario_id,
scenario.roar ? scenario.roar.scenario_id : null
) ;
// all done - we can now close the dialog
$gDialog.dialog( "close" ) ;
}
function onConfirmImportScenario()
{
// import the scenario
var scenario = $gConfirmImportButton.data( "scenario" ) ;
doImportScenario( scenario ) ;
}
function onCancelImportScenario()
{
// remove all the warnings and cancel/confirm buttons, and revert back to the "import" state
// NOTE: Because we "cancel" the import every time the scenario card is updated, we only
// call slideToggle() if we actually need to.
if ( $gImportWarnings.css( "display" ) != "none" ) {
$gImportWarnings.slideToggle( 100, function() {
// FUDGE! If the content box is short enough to not need a vertical scrollbar, but the warning box
// causes one to appear, it doesn't go away when the warning box disappears. Triggering resizes
// doesn't seem to help, so we force the content box to be taller, which accumulates, but stops
// once the bottom reaches the bottom of the flex box. Sigh...
var $content = $gScenarioCard.find( ".content" ) ;
$content.css( "height", $content.outerHeight() + 100 ) ;
} ) ;
}
$gConfirmImportButton.hide() ;
$gCancelImportButton.hide() ;
$gDownloadsButton.show() ;
$gImportScenarioButton.show() ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function checkImportField_text( importField, newVal, scenario ) {
return null ;
}
function getImportFieldCurrVal_text( importField ) {
// get the current field value
return $( "input[name='" + importField.paramName + "']" ).val().trim() ;
}
function doImportField_text( importField, newVal, scenario ) {
// update the field in the scenario
$( "input[name='" + importField.paramName + "']" ).val( newVal ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function checkImportField_select2( importField, newVal, scenario ) {
if ( importField.paramName == "SCENARIO_THEATER" ) {
// check if we will be able to import this theater
if ( newVal && ! getEffectiveTheater( newVal, scenario ) ) {
// nope - issue a warning
return "Unknown theater: " + newVal ;
}
}
return null ;
}
function getImportFieldCurrVal_select2( importField ) {
// get the current field value
if ( importField.paramName == "SCENARIO_THEATER" )
return null ; // nb: this will always be updated without warning
if ( importField.paramName == "TURN_TRACK_NTURNS" )
return null ; // nb: this will always be updated without warning
return $( "select[name='" + importField.paramName + "']" ).val().trim() ;
}
function doImportField_select2( importField, newVal, scenario ) {
// update the field in the scenario
if ( importField.paramName == "SCENARIO_THEATER" ) {
if ( newVal ) {
newVal = getEffectiveTheater( newVal, scenario ) ;
if ( ! newVal )
newVal = "other" ;
}
else
newVal = "ETO" ;
} else if ( importField.paramName === "TURN_TRACK_NTURNS" ) {
// ensure the turn count we are about to import is in the droplist
updateTurnTrackNTurns( newVal ) ;
}
var $elem = $( "select[name='" + importField.paramName + "']" ) ;
$elem.val( newVal ).trigger( "change" ) ;
}
function getEffectiveTheater( theater, scenario ) {
if ( gAppConfig.THEATERS.indexOf( theater ) !== -1 )
return theater ;
// NOTE: The ASL Scenario Archive has a bunch of different theaters, which we try to map to ours.
// The most common ones are:
// - WTO: places like France, Holland, Belgium, so I think "Western Theatre Of Operations"
// - MTO: places like Sicily, Italy, Greece, so I think "Mediterranean Theatre Of Operations"
if ( theater === "CBI" ) {
// I think this is "China, Burma, India", but this one is tricky since we can't just
// map it to PTO, since we have "Burma" as a separate theatre. We peek at the scenario
// location to figure out what to do.
var loc = scenario.scenario_location || "" ;
if ( loc.indexOf( "Burma" ) !== -1 )
return "Burma" ;
// NOTE: It's debatable whether scenarios in India should be considered PTO, but why not...
return "PTO" ;
}
theater = gAppConfig.SCENARIOS_CONFIG["theater-mappings"][ theater ] ;
if ( theater )
return theater ;
return null ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function checkImportField_date( importField, newVal, scenario ) {
return null ;
}
function getImportFieldCurrVal_date( importField ) {
// get the current field value
if ( importField.paramName != "SCENARIO_DATE" )
return null ; // nb: shouldn't get here!
var scenarioDate = get_scenario_date() ;
if ( ! scenarioDate )
return null ;
return [ scenarioDate[3], scenarioDate[4] ] ;
}
function doImportField_date( importField, newVal, scenario ) {
// update the field in the scenario
var $elem = $( "input[name='" + importField.paramName + "']" ) ;
$elem.datepicker( "setDate",
newVal ? $.datepicker.parseDate( "yy-mm-dd", newVal ) : null
) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function checkImportField_player( importField, newVal, scenario ) {
// check if we will be able to import this player
if ( newVal ) {
var effectiveNat = getEffectivePlayerNat( newVal ) ;
if ( ! effectiveNat ) {
// nope - issue a warning
return "Unknown player: " + newVal ;
}
return "!=" + effectiveNat[0] ; // nb: tell the caller to update its "newVal"
}
return null ;
}
function getImportFieldCurrVal_player( importField ) {
// get the current field value
// NOTE: Player nationalities will be changed without warning, *if* they have no OB.
// If they have OB, no warning will be issued if the nationality is the same.
if ( importField.paramName == "PLAYER_1" && is_player_ob_empty( 1 ) )
return null ;
if ( importField.paramName == "PLAYER_2" && is_player_ob_empty( 2 ) )
return null ;
var currVal = $( "select[name='" + importField.paramName + "']" ).val() ;
// NOTE: The extra warning will only show if the new player is different from the current player.
return [ currVal, get_nationality_display_name(currVal), "This player's OB will be removed." ] ;
}
function doImportField_player( importField, newVal, scenario ) {
// update the player's nationality in the scenario
var effectiveNat = getEffectivePlayerNat( newVal ) ;
if ( ! effectiveNat )
return ; // nb: unknown nationality - ignore it
newVal = effectiveNat[0] ;
var $elem = $( "select[name='" + importField.paramName + "']" ) ;
if ( $elem.val() != newVal ) {
// NOTE: We manually call on_player_change() to reset the player's OB, so that the user
// doesn't get a warning about losing OB settings when the player droplist is changed.
var playerNo = importField.paramName.substring( importField.paramName.length-1 ) ;
on_player_change( playerNo ) ;
$elem.val( newVal ).trigger( "change" ) ;
}
}
window.getEffectivePlayerNat = function( playerName ) {
if ( ! playerName )
return null ;
// try to find an exact match with one of our nationalities
var playerNameLC = playerName.toLowerCase() ;
for ( var nat in gTemplatePack.nationalities ) {
if ( gTemplatePack.nationalities[nat].display_name.toLowerCase() == playerNameLC )
return [ nat, "exactMatch", playerName ] ;
}
// try to find a mapping (exact match)
var nat2 = gAppConfig.SCENARIOS_CONFIG["nat-mappings"][ playerNameLC ] ;
if ( nat2 )
return [ nat2, "exactMapping", playerName ] ;
// try to find a partial match with one of our nationalities
for ( nat in gTemplatePack.nationalities ) {
var natDisplayName = gTemplatePack.nationalities[ nat ].display_name ;
if ( playerName.match( new RegExp( "\\b"+natDisplayName+"\\b" ), "i" ) )
return [ nat, "partialMatch", natDisplayName ] ;
}
// try to find a mapping (partial match)
var mappings = gAppConfig.SCENARIOS_CONFIG[ "nat-mappings" ] ;
for ( var key in mappings ) {
if ( playerName.match( new RegExp( "\\b"+key+"\\b", "i" ) ) )
return [ mappings[key], "partialMapping", key ] ;
}
return null ;
} ;
// --------------------------------------------------------------------
function onDownloads() {
// initialize
var scenario = $gScenarioCard.data( "scenario" ) ;
var eventHandlers = new jQueryHandlers() ;
var $dlg ;
function loadFileGroups( $fgroups ) {
var $items = [] ;
scenario.downloads.forEach( function( fgroup ) {
var buf = [] ;
var url = fgroup.screenshot || gImagesBaseUrl+"/missing-image.png" ;
buf.push( "<div class='screenshot'>", "<img src='"+url+"'>", "</div>" ) ;
buf.push( "<div>" ) ;
if ( fgroup.user )
buf.push( "<div class='contrib'>", "Contributed by <span class='user'>"+fgroup.user+"</span>", "</div>" ) ;
if ( fgroup.timestamp ) {
var tstamp = new Date( fgroup.timestamp ) ;
tstamp = tstamp.toLocaleDateString( undefined, { day: "numeric", month: "long", year: "numeric" } ) ;
buf.push( "<div class='timestamp'>", tstamp, "</div>" ) ;
}
if ( fgroup.vt_setup ) {
buf.push( "<button class='vt_setup' data-url='" + fgroup.vt_setup + "' title='Import the vasl-templates setup'>",
"<img src='"+gImagesBaseUrl+"/sortable-add.png'>", "Import",
"</button>"
) ;
}
if ( fgroup.vasl_setup ) {
buf.push( "<button class='vasl_setup' data-url='" + fgroup.vasl_setup + "' title='Download the VASL setup'>",
"<img src='"+gImagesBaseUrl+"/download.png'>", "Download",
"</button>"
) ;
}
buf.push( "</div>" ) ;
var $item = $( "<li class='fgroup'>" + buf.join("") + "</li>" ) ;
$item.find( "button.vt_setup" ).on( "click", function() {
if ( ! is_scenario_dirty() )
onDownloadVtSetup( fgroup.vt_setup ) ;
else {
ask( "Import scenario",
"<p> Your scenario has been changed. <p> Do you want to import a new scenario, and lose your changes?", {
width: 470,
ok: function() { onDownloadVtSetup( fgroup.vt_setup ) ; },
} ) ;
}
} ) ;
$item.find( "button.vasl_setup" ).on( "click", function() {
onDownloadVaslSetup( fgroup.vasl_setup ) ;
} ) ;
fixup_external_links( $item ) ;
$items.push( $item ) ;
} ) ;
$fgroups.html( $items ) ;
}
function onDownloadVtSetup( url ) {
// download the vasl-templates setup
var $pleaseWait = showPleaseWaitDialog( "Downloading the scenario..." ) ;
$.ajax( {
url: url, type: "GET",
} ).done( function( resp ) {
$pleaseWait.dialog( "close" ) ;
// the file was downloaded OK - load it into the UI
var fname = url.split( "/" ).pop() ;
if ( ! do_load_scenario( JSON.stringify(resp), fname ) )
return ;
// all done - we can now close the downloads popup *and* the parent search dialog
$dlg.dialog( "close" ) ;
$gDialog.dialog( "close" ) ;
} ).fail( function( xhr, status, errorMsg ) {
$pleaseWait.dialog( "close" ) ;
showErrorMsg( "Can't download the <em>vasl-templates</em> setup:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
function onDownloadVaslSetup( url ) {
// download the VASL setup
// FUDGE! Triggering a download (that works in both a browser window and the desktop app)
// is depressingly tricky :-( We don't want to mess with window.location, since the download
// could end up replacing the webapp in the browser :-/ Wrapping the button with an <a> tag
// sorta works, but it can cause an external browser window to open, and remain open :-/
// We download the file ourself and then ask the user to save it.
var $pleaseWait = showPleaseWaitDialog( "Downloading the VASL scenario...", { width: 320 } ) ;
$.ajax( {
url: url, type: "GET",
xhrFields: { responseType: "arraybuffer" }
} ).done( function( resp ) {
$pleaseWait.dialog( "close" ) ;
// the file was downloaded OK - give it to the user to save
var fname = url.split( "/" ).pop().split( "|" ).pop() ;
if ( gWebChannelHandler ) {
var vsavData = new Uint8Array( resp ) ;
gWebChannelHandler.save_downloaded_vsav( fname,
btoa( String.fromCharCode.apply( null, vsavData ) )
) ;
} else {
download( resp, fname, "application/octet-stream" ) ;
}
// all done - we can now close the downloads popup
$dlg.dialog( "close" ) ;
} ).fail( function( xhr, status, errorMsg ) {
$pleaseWait.dialog( "close" ) ;
showErrorMsg( "Can't download the VASL scenario:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
} ) ;
}
// show the dialog
$( "#scenario-downloads-dialog" ).dialog( {
dialogClass: "scenario-downloads",
title: "Downloads for this scenario:",
modal: true,
width: 450, minWidth: 300,
height: 200, minHeight: 132,
draggable: false,
closeOnEscape: false, // nb: handled in handle_escape()
open: function() {
$dlg = $(this) ;
$dlg.parent().draggable() ;
eventHandlers.addHandler( $(".ui-widget-overlay"), "click", function( evt ) {
$dlg.dialog( "close" ) ; // nb: clicking outside the popup closes it
} ) ;
// load the available downloads
var $fgroups = $dlg.find( ".fgroups" ) ;
loadFileGroups( $fgroups ) ;
$(this).css( "height", Math.min( $fgroups.outerHeight(), 400 ) ) ;
var $parentDlg = $( ".ui-dialog.scenario-search" ) ;
$( ".ui-dialog.scenario-downloads" ).position( {
my: "right bottom", at: "right top-2", of: $parentDlg.find( "button.downloads" )
} ) ;
},
close: function() {
// clean up
eventHandlers.cleanUp() ;
},
} ) ;
}
// --------------------------------------------------------------------
function updateLayout()
{
// resize and position the search select2
var $dlg = $( "#scenario-search" ) ;
var $sel = $dlg.find( ".select2-container" ) ;
$dlg.find( ".select2-dropdown" ).css( "width",
$dlg.find( ".scenarios" ).width()
) ;
var newHeight = $dlg.find( ".scenarios" ).height() - $dlg.find( ".select2-search" ).height() - 15 ;
$sel.find( ".select2-results__options" ).css( {
height: newHeight,
"max-height": newHeight,
} ) ;
// resize and position the info box
updateInfoBox( $dlg ) ;
}
function updateInfoBox( $parent )
{
// resize and position the info box
var $header = $parent.find( ".scenario-card .header" ) ;
var $info = $parent.find( ".scenario-card .info" ) ;
if ( $header.length > 0 && $info.length > 0 ) {
$header.css( { "padding-right": $info.width() + 30 } ) ;
var newTop = $header.outerHeight() + 5 - $info.outerHeight() ;
$info.css( { top: Math.max( newTop, 10 ), right: 10 } ) ;
// NOTE: To stop the info box from jumping around visibly, it is configured to be hidden
// in the template, and we show it here after it has been moved into position.
$info.show() ;
}
}
// --------------------------------------------------------------------
function makeBalanceGraphs( $target, scenario, showRoarButtons )
{
// NOTE: If we have balance graphs for both the ASL Scenario Archive and ROAR, we try to show
// the players in the same order for both. Since the player nationalities may be unknown to us,
// we just do a simple text comparison on the display names.
// NOTE: If we only have the ROAR balance, it would be nice to order the players so that they match
// the ASL Scenario Archive's attacker/defender order, but would be more trouble than it's worth :-/
var asaBalance = scenario.balance ;
var roarBalance = scenario.roar ? scenario.roar.balance : null ;
var roar_url = scenario.roar ? scenario.roar.url : null ;
if ( asaBalance && roarBalance ) {
if ( ( asaBalance[0].name.toLowerCase() == roarBalance[1].name.toLowerCase() ) ||
( asaBalance[1].name.toLowerCase() == roarBalance[0].name.toLowerCase() ) ) {
var tmp = roarBalance[0] ;
roarBalance[0] = roarBalance[1] ;
roarBalance[1] = tmp ;
}
}
// make the balance graphs
var rc1 = doMakeBalanceGraph( $target.find( ".balance-graph.asa" ),
asaBalance, scenario.scenario_url, false,
"Balance at the ASL Scenario Archive"
) ;
var rc2 = doMakeBalanceGraph( $target.find( ".balance-graph.roar" ),
roarBalance, roar_url, true,
"Balance at ROAR"
) ;
// update the UI
if ( showRoarButtons && ! rc2 )
$target.find( ".connect-roar" ).show() ;
else
$target.find( ".connect-roar" ).hide() ;
if ( rc1 || rc2 )
return true ;
else {
if ( ! showRoarButtons )
$target.find( ".balance-graphs" ).hide() ;
return false ;
}
function doMakeBalanceGraph( $bgraph, balance, url, isRoar, tooltip )
{
// initialize
if ( ! balance ) {
$bgraph.hide() ;
return false ;
}
var buf = [] ;
var link1 = url ? "<a href='" + url + "'>" : "" ;
var link2 = url ? "</a>" : "" ;
// add the the 1st player's details
buf.push( "<div class='player player1'>", balance[0].name, "</div>" ) ;
buf.push( "&nbsp;" ) ;
buf.push( "<div class='wins player1'>", "("+balance[0].wins+")", "</div>" ) ;
buf.push( "&nbsp;", "&nbsp;" ) ;
// NOTE: The wrapper div contains both progress bars (to work-around a Chromium rendering problem :-/).
buf.push( "<div class='wrapper'>", link1, "<nobr>",
"<div class='progressbar player1'>", "<div class='score'></div>", "</div>"
) ;
// add the the 2nd player's details
buf.push(
"<div class='progressbar player2'>", "<div class='score'></div>", "</div>",
"</nobr>", link2, "</div>"
) ;
buf.push( "&nbsp;", "&nbsp;" ) ;
buf.push( "<div class='wins player2'>", "("+balance[1].wins+")", "</div>" ) ;
buf.push( "&nbsp;" ) ;
buf.push( "<div class='player player2'>", balance[1].name, "</div>" ) ;
// show the "disconnect from ROAR" button
if ( showRoarButtons && isRoar ) {
buf.push( "<div class='disconnect-roar'>",
"<img src='" + gImagesBaseUrl+"/cross.png" + "' title='Disconnect from ROAR'>",
"</div>"
) ;
}
// load the balance graph
$bgraph.empty().append( buf.join("") ).show() ;
fixup_external_links( $bgraph ) ;
// configure the progressbar's
$bgraph.find( ".progressbar" ).each( function() {
var isPlayer1 = $(this).hasClass( "player1" ) ;
var score = balance[ isPlayer1 ? 0 : 1 ].percentage ;
if ( score === undefined )
score = 0 ;
else
score = parseInt( score ) ;
$(this).progressbar( {
value: isPlayer1 ? 100-score : score
} ) ;
$(this).children( ".score" ).text( score+"%" ) ;
$(this).attr( "title", tooltip ) ;
} ) ;
// show the progressbar's in grey if there are not many playings
var threshold = gAppConfig.BALANCE_GRAPH_THRESHOLD || 20 ;
var totalGames = parseInt(balance[0].wins) + parseInt(balance[1].wins) ;
if ( totalGames < threshold ) {
var alpha = Math.max( totalGames / threshold, 0.5 ) ;
var color = "rgba( 224, 224, 224, " + fpFmt(alpha,1) + ")" ;
var borderColor = "#d0d0d0" ;
$bgraph.find( ".progressbar.player1" ).css( "background", color ) ;
$bgraph.find( ".progressbar.player2 .ui-progressbar-value" ).css( "background", color ) ;
$bgraph.find( ".progressbar.player1" ).css( {
border: "1px solid "+borderColor,
"border-right": 0
} ) ;
$bgraph.find( ".progressbar.player2" ).css( "border", "1px solid "+borderColor ) ;
}
// add a click handler for "disconnect from ROAR"
$bgraph.find( ".disconnect-roar" ).on( "click", function() {
updateForConnectedScenario( scenario.scenario_id, null ) ;
getScenarioData( scenario.scenario_id, null, function( newScenario ) {
// NOTE: We enable "showRoarButtons" so that the "connect to ROAR" button appears.
makeBalanceGraphs( $target, newScenario, true ) ;
$target.find( ".connect-roar" ).show() ;
} ) ;
} ) ;
return true ;
}
}
// --------------------------------------------------------------------
window.showScenarioInfo = function()
{
// initialize
var $dlg ;
var eventHandlers = new jQueryHandlers() ;
var scenarioId = $( "input[name='ASA_ID']" ).val() ;
var scenarioDate = get_scenario_date() ;
function onResize() { updateInfoBox( $dlg ) ; }
// request the scenario card
var roarOverride = $( "input[name='ROAR_ID']" ).val() ;
loadScenarioCard( $("#scenario-info-dialog .scenario-card"), scenarioId, false, scenarioDate, roarOverride, true,
function( scenario ) {
// show the dialog
$( "#scenario-info-dialog" ).dialog( {
dialogClass: "scenario-info",
modal: true,
closeOnEscape: false, // nb: handled in handle_escape()
width: $(window).width() * 0.8,
minWidth: 550,
height: $(window).height() * 0.8,
minHeight: 300,
create: function() {
addAsaCreditPanel( $(".ui-dialog.scenario-info"), null ) ;
},
open: function() {
// initialize
$dlg = $(this) ;
var $draggable = $dlg.parent().draggable() ;
// add a click handler for "connect to ROAR"
$(this).find( ".connect-roar" ).on( "click", function() {
connectToRoar( $dlg.find(".scenario-card"), scenario ) ;
} ) ;
// change the credit link
var url = gAppConfig.ASA_SCENARIO_URL.replace( "{ID}", scenarioId ) ;
var $btnPane = $( ".ui-dialog.scenario-info .ui-dialog-buttonpane" ) ;
$btnPane.find( ".credit a" ).attr( "href", url ) ;
// configure the "upload scenario" button
var $btn = $btnPane.find( "button.upload" ) ;
$btn.prepend(
$( "<img src='" + gImagesBaseUrl+"/upload.png" + "' style='height:0.9em;margin:0 0.35em -1px 0;'>" )
) ;
var creditWidth = $btnPane.find( ".credit" ).outerWidth() ;
$btn.css( { position: "absolute", left: creditWidth+20, padding: "2px 5px" } ) ;
$btn.attr( "title", "Upload your setup to the ASL Scenario Archive" ) ;
// configure the "unlink scenario" button
var $btn2 = $btnPane.find( "button.unlink" ) ;
$btn2.prepend(
$( "<img src='" + gImagesBaseUrl+"/cross.png" + "' style='height:0.6em;margin-right:0.35em;padding-bottom:1px;'>" )
) ;
$btn2.css( { position: "absolute", left: creditWidth+40+$btn.outerWidth(), padding: "2px 5px" } ) ;
$btn2.attr( "title", "Unlink your scenario from the ASL Scenario Archive" ) ;
// update the layout
onResize() ;
eventHandlers.addHandler( $(document), "keydown", function( evt ) {
if ( evt.keyCode == 16 ) { // nb: checking evt.shiftKey is unreliable
window.getSelection().empty() ;
$draggable.draggable( "disable" ) ;
}
} ) ;
eventHandlers.addHandler( $(document), "keyup", function( evt ) {
if ( evt.keyCode == 16 )
$draggable.draggable( "enable" ) ;
} ) ;
// set initial focus
$btnPane.find( "button.ok" ).focus() ;
},
close: function() {
// clean up
eventHandlers.cleanUp() ;
},
resize: onResize,
draggable: false,
buttons: {
OK: { text: "OK", class: "ok", click: function() {
$dlg.dialog( "close" ) ;
} },
Unlink: { text: "Unlink", class: "unlink", click: function() {
updateForConnectedScenario( null, null ) ;
$dlg.dialog( "close" ) ;
} },
Upload: { text: "Upload", class: "upload", click: function() {
$dlg.dialog( "close" ) ;
uploadScenario() ;
} },
},
} ) ;
},
function( xhr, status, errorMsg ) {
showErrorMsg( "Can't get the scenario card:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
}
) ;
} ;
function connectToRoar( $target, scenario )
{
// ask the user to select a ROAR scenario
selectRoarScenario( function( roarId ) {
// save the selected ROAR scenario and update the UI
updateForConnectedScenario( scenario.scenario_id, roarId ) ;
getScenarioData( scenario.scenario_id, roarId, function( newScenario ) {
if ( ! newScenario.roar || ! newScenario.roar.balance ) {
showWarningMsg( "There are no playing results for this ROAR scenario." ) ;
return ;
}
makeBalanceGraphs( $target, newScenario, true ) ;
} ) ;
} ) ;
}
// --------------------------------------------------------------------
window.updateForConnectedScenario = function( scenarioId, roarId )
{
// save the scenario ID's
$( "input[name='ASA_ID']" ).val( scenarioId ) ;
$( "input[name='ROAR_ID']" ).val( roarId ) ;
// update the UI
var $btn = $( "button.scenario-search" ) ;
if ( scenarioId ) {
$btn.find( "img" ).attr( "src", gImagesBaseUrl+"/info.gif" ) ;
$btn.attr( "title", "Scenario details" ) ;
} else {
$btn.find( "img" ).attr( "src", gImagesBaseUrl+"/search.png" ) ;
$btn.attr( "title", "Search for scenarios" ) ;
}
} ;
// --------------------------------------------------------------------
var _scenarioIndex ; // nb: don't access this directly, use getScenarioIndex()
var _scenarioIndexETag ;
function getScenarioIndex( onReady )
{
$.ajax( {
url: gGetScenarioIndexUrl,
type: "GET",
datatype: "json",
beforeSend: function( xhr ) {
if ( _scenarioIndexETag )
xhr.setRequestHeader( "If-None-Match", _scenarioIndexETag ) ;
},
success: function( resp, status, xhr ) {
if ( xhr.status == 304 ) {
// our cached copy is still valid
onReady( _scenarioIndex, false ) ;
return ;
}
// check if a warning was issued
if ( resp.warning ) {
var msg = resp.warning ;
if ( resp.message )
msg += "<div class='pre'>" + escapeHTML(resp.message) + "</div>" ;
showWarningMsg( msg ) ;
return ;
}
// save a copy of the data, then notify the caller
_scenarioIndex = resp ;
_scenarioIndexETag = xhr.getResponseHeader( "ETag" ) ;
onReady( resp, true ) ;
},
error: function( xhr, status, errorMsg ) {
showErrorMsg( "Can't get the scenario index:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
return ;
},
} ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function getScenarioData( scenarioId, roarOverride, onReady )
{
// get the scenario data
var url = gGetScenarioUrl.replace( "ID", scenarioId ) ;
if ( roarOverride )
url = addUrlParam( url, "roar", roarOverride) ;
$.getJSON( url, function( resp ) {
onReady( resp ) ;
} ).fail( function( xhr, status, errorMsg ) {
showErrorMsg( "Can't get the scenario details:<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ;
return ;
} ) ;
}
// --------------------------------------------------------------------
window.addAsaCreditPanel = function( $dlg, scenarioId )
{
// create the credit panel
var url = scenarioId ? gAppConfig.ASA_SCENARIO_URL.replace( "{ID}", scenarioId ) : "https://aslscenarioarchive.com" ;
var buf = [ "<div class='credit'>",
"<a href='"+url+"'>", "<img src='" + gImagesBaseUrl+"/asl-scenario-archive.png" + "'>", "</a>",
"<div class='caption'>",
"<a href='"+url+"'>", "Information provided by", "<br>", "the ASL Scenario Archive.", "</a>",
"</div>",
"</div>"
] ;
var $credit = $( buf.join("") ) ;
$credit.css( { position: "absolute", left: "1em", bottom: "13px", display: "flex", "align-items": "center" } ) ;
$credit.find( "img" ).css( { height: "1.4em", "margin-right": "0.5em", opacity: 0.7 } ) ;
$credit.find( ".caption" ).css( { "font-size": "70%", "line-height": "1em", "margin-top": "-4px" } ) ;
$credit.find( "a" ).css( { "text-decoration": "none", "font-style": "italic", color: "#666" } ) ;
$credit.find( "a" ).on( "click", function() { $(this).blur() ; } ) ;
// add the credit panel to the dialog's button pane
fixup_external_links( $credit ) ;
var $btnPane = $dlg.find( ".ui-dialog-buttonpane" ) ;
$btnPane.find( ".credit" ).remove() ;
$btnPane.prepend( $credit ) ;
} ;
// --------------------------------------------------------------------
} )() ; // end local namespace