/* 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: 750,
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",
"
Your scenario has been changed.
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 = $( "" ) ;
$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( "" ) ;
if ( prefix )
buf.push( prefix ) ;
buf.push( val ) ;
if ( postfix )
buf.push( postfix ) ;
buf.push( "" ) ;
}
}
// generate the search result
const nowrap = "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" ;
var buf = [ "
" ) ;
// 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:
" + escapeHTML(errorMsg) + "
" ) ;
}
) ;
}
// --------------------------------------------------------------------
// 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( ""
) ;
}, 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 ", mapImage.user, "" ) ;
if ( mapImage.timestamp ) {
var tstamp = new Date( mapImage.timestamp ) ;
buf.push( "
"
) ;
}
}
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( " (" + scenario.map_images.length + ")" ) ;
$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(
$( "" )
) ;
} ) ;
}
function loadObaInfo( $target, scenario, scenarioDateOverride )
{
// initialize
var theater = getEffectiveTheater( scenario.theater ) ;
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( "
", cmt, "
" ) ;
} ) ;
$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: "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 ) ; // jshint ignore: line
if ( msg ) {
if ( msg.substring(0,2) == "!=" )
newVal = msg.substring( 2 ) ;
else {
buf = [ "
",
"",
msg,
"
"
] ;
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 = [ "
" ) ;
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 = [
"
",
"",
"
Some values in your scenario will be changed:
",
"
"
] ;
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 )
{
// 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 ) ; // jshint ignore: line
} ) ;
// update for the newly-connected scenario
// 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.
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, warnings2 ) {
return null ;
}
function getImportFieldCurrVal_text( importField ) {
// get the current field value
return $( "input[name='" + importField.paramName + "']" ).val().trim() ;
}
function doImportField_text( importField, newVal ) {
// update the field in the scenario
$( "input[name='" + importField.paramName + "']" ).val( newVal ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function checkImportField_select2( importField, newVal, warnings2 ) {
if ( importField.paramName == "SCENARIO_THEATER" ) {
// check if we will be able to import this theater
if ( newVal && ! getEffectiveTheater( newVal ) ) {
// 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
return $( "select[name='" + importField.paramName + "']" ).val().trim() ;
}
function doImportField_select2( importField, newVal ) {
// update the field in the scenario
if ( importField.paramName == "SCENARIO_THEATER" ) {
if ( newVal ) {
newVal = getEffectiveTheater( newVal ) ;
if ( ! newVal )
newVal = "other" ;
}
}
var $elem = $( "select[name='" + importField.paramName + "']" ) ;
$elem.val( newVal || "ETO" ).trigger( "change" ) ;
}
function getEffectiveTheater( theater ) {
if ( gAppConfig.THEATERS.indexOf( theater ) !== -1 )
return theater ;
theater = gAppConfig.SCENARIOS_CONFIG["theater-mappings"][ theater ] ;
if ( theater )
return theater ;
return null ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function checkImportField_date( importField, newVal, warnings2 ) {
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 ) {
// 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, warnings2 ) {
// 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 ) {
// 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( "