/* 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", "

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 = [ "

" ] ; buf.push( "
" ) ; addVal( scenario.scenario_name ) ; addVal( scenario.scenario_display_id, "scenario-id", " (", ")" ) ; buf.push( "
" ) ; if ( scenario.scenario_location ) { buf.push( "
" ) ; addVal( scenario.scenario_location, "scenario-location" ) ; addVal( scenario.scenario_date, "scenario-date", " (", ")" ) ; buf.push( "
" ) ; } if ( scenario.publication_name ) { buf.push( "
" ) ; 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( "
" ) ; } buf.push( "
" ) ; // 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( "
", tstamp.toLocaleDateString( undefined, { day: "numeric", month: "long", year: "numeric" } ), "
" ) ; } } 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, 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( "
", 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: "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 = [ "
", "", 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 = [ "
", "", "", "Update the " + importField.name, "" ] ; if ( displayCurrVal.length <= 20 ) { buf.push( " ", "", "(from \"" + displayCurrVal + "\")", "" ) ; buf.push( "
" ) ; } else { buf.push( "" ) ; buf.push( "
", "Currently \"" + displayCurrVal + "\".", "
" ) ; } if ( extraMsg ) buf.push( "
", "", extraMsg, "
" ) ; 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 ) { // 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( "
", "", "
" ) ; buf.push( "
" ) ; if ( fgroup.user ) buf.push( "
", "Contributed by "+fgroup.user+"", "
" ) ; if ( fgroup.timestamp ) { var tstamp = new Date( fgroup.timestamp ) ; tstamp = tstamp.toLocaleDateString( undefined, { day: "numeric", month: "long", year: "numeric" } ) ; buf.push( "
", tstamp, "
" ) ; } if ( fgroup.vt_setup ) { buf.push( "" ) ; } if ( fgroup.vasl_setup ) { buf.push( "" ) ; } buf.push( "
" ) ; var $item = $( "
  • " + buf.join("") + "
  • " ) ; $item.find( "button.vt_setup" ).on( "click", function() { if ( ! is_scenario_dirty() ) onDownloadVtSetup( fgroup.vt_setup ) ; else { ask( "Import scenario", "

    Your scenario has been changed.

    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 vasl-templates setup:

    " + escapeHTML(errorMsg) + "
    " ) ; } ) ; } 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 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:
    " + escapeHTML(errorMsg) + "
    " ) ; } ) ; } // 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 ? "
    " : "" ; var link2 = url ? "" : "" ; // add the the 1st player's details buf.push( "
    ", balance[0].name, "
    " ) ; buf.push( " " ) ; buf.push( "
    ", "("+balance[0].wins+")", "
    " ) ; buf.push( " ", " " ) ; // NOTE: The wrapper div contains both progress bars (to work-around a Chromium rendering problem :-/). buf.push( "
    ", link1, "", "
    ", "
    ", "
    " ) ; // add the the 2nd player's details buf.push( "
    ", "
    ", "
    ", "
    ", link2, "
    " ) ; buf.push( " ", " " ) ; buf.push( "
    ", "("+balance[1].wins+")", "
    " ) ; buf.push( " " ) ; buf.push( "
    ", balance[1].name, "
    " ) ; // show the "disconnect from ROAR" button if ( showRoarButtons && isRoar ) { buf.push( "
    ", "", "
    " ) ; } // 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( $( "" ) ) ; 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( $( "" ) ) ; $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:
    " + escapeHTML(errorMsg) + "
    " ) ; } ) ; } ; 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 += "
    " + escapeHTML(resp.message) + "
    " ; 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:
    " + escapeHTML(errorMsg) + "
    " ) ; 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:
    " + escapeHTML(errorMsg) + "
    " ) ; 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 = [ "
    ", "", "", "", "
    ", "", "Information provided by", "
    ", "the ASL Scenario Archive.", "
    ", "
    ", "
    " ] ; 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