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.
329 lines
14 KiB
329 lines
14 KiB
import { getJSON, showErrorMsg, showNotificationMsg, hideFootnotes } from "./utils.js" ;
|
|
|
|
// parse any URL parameters
|
|
export let gUrlParams = new URLSearchParams( window.location.search.substring(1) ) ;
|
|
|
|
// create the main application
|
|
export const gMainApp = Vue.createApp( { //eslint-disable-line no-undef
|
|
template: "<main-app />",
|
|
} ) ;
|
|
export const gEventBus = new TinyEmitter() ; //eslint-disable-line no-undef
|
|
$(document).ready( () => {
|
|
gMainApp.mount( "#main-app" ) ;
|
|
} ) ;
|
|
|
|
// FUDGE! Can't seem to get access to gMainApp member variables, so we make them available
|
|
// to the rest of the program via global variables :-/
|
|
export let gAppConfig = null ;
|
|
export let gContentDocs = null ;
|
|
export let gTargetIndex = null ;
|
|
export let gFootnoteIndex = null ;
|
|
export let gChapterResources = null ;
|
|
export let gASOPChapterIndex = null ;
|
|
export let gASOPSectionIndex = null ;
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
gMainApp.component( "main-app", {
|
|
|
|
data() { return {
|
|
contentDocs: [],
|
|
asop: {},
|
|
isLoaded: false,
|
|
} ; },
|
|
|
|
template: `
|
|
<nav-pane id="nav" :asop=asop ref="navPane" />
|
|
<content-pane id="content" :contentDocs=contentDocs />
|
|
<div v-if=isLoaded id="_mainapp-loaded_" />
|
|
`,
|
|
|
|
created() {
|
|
gEventBus.on( "show-target", () => {
|
|
hideFootnotes() ;
|
|
} ) ;
|
|
},
|
|
|
|
mounted() {
|
|
|
|
// initialize the splitter
|
|
Split( [ "#nav", "#content" ], { //eslint-disable-line no-undef
|
|
direction: "horizontal",
|
|
sizes: [ 25, 75 ],
|
|
gutterSize: 2,
|
|
} ) ;
|
|
|
|
// initialize hotkeys
|
|
jQuery.hotkeys.options.filterInputAcceptingElements = false ;
|
|
jQuery.hotkeys.options.filterContentEditable = false ;
|
|
jQuery.hotkeys.options.filterTextInputs = false ;
|
|
function selectNav( tabId ) {
|
|
$( "#nav .close-rule-info" ).click() ;
|
|
$( "#nav .tab-strip .tab[data-tabid='" + tabId + "']" ).click() ;
|
|
}
|
|
$( "body" ).bind( "keydown", "alt+r", function( evt ) {
|
|
selectNav( "search" ) ;
|
|
$( "#query-string" ).select() ;
|
|
$( "#query-string" ).focus() ;
|
|
evt.preventDefault() ;
|
|
} ) ;
|
|
$( "body" ).bind( "keydown", "alt+c", function( evt ) { selectNav( "chapters" ) ; evt.preventDefault() ; } ) ;
|
|
$( "body" ).bind( "keydown", "alt+a", function( evt ) { selectNav( "asop" ) ; evt.preventDefault() ; } ) ;
|
|
|
|
// monitor the backend startup process
|
|
this.startupTimerId = setInterval( this.checkStartupStatus, 1*1000 ) ;
|
|
|
|
// initialze the webapp
|
|
Promise.all( [
|
|
this.getAppConfig(),
|
|
this.getContentDocs( this ),
|
|
this.getFootnoteIndex(),
|
|
this.getASOP(),
|
|
] ).then( () => {
|
|
this.onStartupDone() ;
|
|
} ).catch( () => {
|
|
// NOTE: Each individual Promise should report their own errors i.e. what could we do here,
|
|
// other than show a generic "startup failed" error?
|
|
} ) ;
|
|
|
|
// add a global keypress handler
|
|
$(document).on( "keyup", (evt) => {
|
|
if ( evt.keyCode == 27 )
|
|
this.onEscapePressed() ;
|
|
} ) ;
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
getAppConfig() {
|
|
// get the app config
|
|
return getJSON( gGetAppConfigUrl ).then( (resp) => { //eslint-disable-line no-undef
|
|
gAppConfig = resp ;
|
|
gEventBus.emit( "app-config-loaded" ) ;
|
|
} ).catch( (errorMsg) => {
|
|
showErrorMsg( "Couldn't get the app config.", errorMsg ) ;
|
|
} ) ;
|
|
},
|
|
|
|
getContentDocs( self ) {
|
|
// get the content docs
|
|
return getJSON( gGetContentDocsUrl ).then( (resp) => { //eslint-disable-line no-undef
|
|
// install the content docs
|
|
if ( gUrlParams.get( "add-empty-doc" ) )
|
|
resp["empty"] = { "cdoc_id": "empty", "title": "Empty document" } ; // nb: for testing porpoises
|
|
self.contentDocs = resp ;
|
|
self.installContentDocs( resp ) ;
|
|
} ).catch( (errorMsg) => {
|
|
showErrorMsg( "Couldn't get the content docs.", errorMsg ) ;
|
|
} ) ;
|
|
},
|
|
|
|
getFootnoteIndex() {
|
|
// get the footnotes
|
|
return getJSON( gGetFootnotesUrl ).then( (resp) => { //eslint-disable-line no-undef
|
|
gFootnoteIndex = resp ;
|
|
} ).catch( (errorMsg) => {
|
|
showErrorMsg( "Couldn't get the footnote index.", errorMsg ) ;
|
|
} ) ;
|
|
},
|
|
|
|
getASOP() {
|
|
// get the ASOP
|
|
return getJSON( gGetASOPUrl ).then( (resp) => { //eslint-disable-line no-undef
|
|
this.asop = resp ;
|
|
// build an index of the ASOP chapters and sections
|
|
gASOPChapterIndex = {} ;
|
|
gASOPSectionIndex = {} ;
|
|
if ( resp.chapters ) {
|
|
resp.chapters.forEach( (chapter) => {
|
|
gASOPChapterIndex[ chapter.chapter_id ] = chapter ;
|
|
if ( chapter.sections ) {
|
|
chapter.sections.forEach( (section) => {
|
|
gASOPSectionIndex[ section.section_id ] = section ;
|
|
} ) ;
|
|
}
|
|
} ) ;
|
|
}
|
|
} ).catch( (errorMsg) => {
|
|
showErrorMsg( "Couldn't get the ASOP.", errorMsg ) ;
|
|
} ) ;
|
|
},
|
|
|
|
installContentDocs( contentDocs ) {
|
|
// install the content docs
|
|
gContentDocs = contentDocs ;
|
|
// build an index of all the targets
|
|
function updateTargetIndex( entries, key, cdoc ) {
|
|
for ( let vo_note_id in entries ) {
|
|
let dest = key + ":" + vo_note_id ;
|
|
if ( ! gTargetIndex[ dest ] )
|
|
gTargetIndex[ dest ] = [] ;
|
|
gTargetIndex[ dest ].push( {
|
|
cset_id: cdoc.parent_cset_id,
|
|
cdoc_id: cdoc.cdoc_id,
|
|
ruleid: dest,
|
|
} ) ;
|
|
}
|
|
}
|
|
gTargetIndex = {} ;
|
|
Object.values( contentDocs ).forEach( (cdoc) => {
|
|
if ( cdoc.targets ) {
|
|
// add entries for each of the normal rules
|
|
for ( let ruleid in cdoc.targets ) {
|
|
let ruleidLC = ruleid.toLowerCase() ;
|
|
if ( ! gTargetIndex[ ruleidLC ] )
|
|
gTargetIndex[ ruleidLC ] = [] ;
|
|
gTargetIndex[ ruleidLC ].push( {
|
|
cset_id: cdoc.parent_cset_id,
|
|
cdoc_id: cdoc.cdoc_id,
|
|
ruleid: ruleid
|
|
} ) ;
|
|
}
|
|
}
|
|
if ( cdoc["vo-notes"] ) {
|
|
// add entries for each of the vehicle/ordnance notes
|
|
for ( let nat in cdoc["vo-notes"] ) {
|
|
if ( nat == "landing-craft" ) {
|
|
updateTargetIndex( cdoc["vo-notes"][nat], nat, cdoc ) ;
|
|
continue ;
|
|
}
|
|
for ( let vo_type in cdoc["vo-notes"][nat] ) {
|
|
let key = nat + "_" + vo_type ;
|
|
updateTargetIndex( cdoc["vo-notes"][nat][vo_type], key, cdoc ) ;
|
|
}
|
|
}
|
|
}
|
|
} ) ;
|
|
// build an index of the available chapters resources
|
|
function preloadImage( url ) {
|
|
// FUDGE! If we try to preload the image immediately (even using a Promise), it causes
|
|
// a delay showing the search box background, so we give it a chance to show first.
|
|
setTimeout( () => {
|
|
// FUDGE! $.get() doesn't do the trick, we have to insert an image element into the DOM :-/
|
|
let $img = $( "<img src='" + url + "' style='display:none'></img>" ) ;
|
|
$("body").append( $img ) ;
|
|
}, 200 ) ;
|
|
}
|
|
gChapterResources = { background: {}, icon: {} } ;
|
|
Object.values( contentDocs ).forEach( (cdoc) => {
|
|
if ( ! cdoc.chapters )
|
|
return ;
|
|
cdoc.chapters.forEach( (chapter) => {
|
|
if ( "background" in chapter ) {
|
|
gChapterResources.background[ chapter.chapter_id ] = chapter.background ;
|
|
preloadImage( chapter.background ) ;
|
|
}
|
|
if ( "icon" in chapter ) {
|
|
gChapterResources.icon[ chapter.chapter_id ] = chapter.icon ;
|
|
preloadImage( chapter.icon ) ;
|
|
}
|
|
} ) ;
|
|
} ) ;
|
|
},
|
|
|
|
onStartupDone() {
|
|
// show any startup messages
|
|
getJSON( gGetStartupMsgsUrl ).then( (resp) => { //eslint-disable-line no-undef
|
|
[ "info", "warning", "error" ].forEach( (msgType) => {
|
|
if ( ! resp[msgType] )
|
|
return ;
|
|
resp[msgType].forEach( (msg) => {
|
|
if ( Array.isArray( msg ) )
|
|
msg = msg[0] + " <div class='pre'>" + msg[1] + "</div>" ;
|
|
showNotificationMsg( msgType, msg ) ;
|
|
} ) ;
|
|
} ) ;
|
|
} ).catch( (errorMsg) => {
|
|
showErrorMsg( "Couldn't get the startup messages.", errorMsg ) ;
|
|
} ) ;
|
|
// check if we should start with a query or target
|
|
let queryString = gUrlParams.get( "query" ) || gUrlParams.get( "q" ) ;
|
|
if ( window.location.hash != "" )
|
|
queryString = window.location.hash.substring( 1 ) ;
|
|
let target = gUrlParams.get( "target" ) || gUrlParams.get( "t" ) || "" ;
|
|
let targetSplitPos = target.indexOf( "/" ) ;
|
|
if ( queryString != null && queryString != undefined ) {
|
|
// yup - make it so
|
|
// NOTE: The content pane flickers as it shows the cover page, then jumps to search result.
|
|
// I tried opening the PDF at the target destination, but the same thing still happens :-(
|
|
gEventBus.emit( "search", queryString ) ;
|
|
} else if ( targetSplitPos > 0 ) {
|
|
// yup - make it so
|
|
gEventBus.emit( "show-target", target.substring(0,targetSplitPos), target.substring(targetSplitPos+1) ) ;
|
|
} else {
|
|
// start off showing the main ASL rulebook
|
|
// NOTE: To avoid forcing the user to configure which document this is,
|
|
// we assume that it's the one with the most targets.
|
|
let targetCdocId = null ;
|
|
for ( let cdocId in this.contentDocs ) {
|
|
if ( this.contentDocs[cdocId].targets == undefined )
|
|
continue
|
|
if ( targetCdocId == null || Object.keys(this.contentDocs[cdocId].targets).length > Object.keys(this.contentDocs[targetCdocId].targets).length )
|
|
targetCdocId = cdocId ;
|
|
}
|
|
if ( targetCdocId != null ) {
|
|
Vue.nextTick( () => {
|
|
gEventBus.emit( "show-page", targetCdocId, 1 ) ;
|
|
} ) ;
|
|
}
|
|
}
|
|
// update the UI
|
|
$( "#watermark" ).css( "opacity", 0.1 ) ;
|
|
this.$nextTick( () => {
|
|
$( "#query-string" ).focus() ; // nb: because autofocus on the <input> doesn't work :-/
|
|
} ) ;
|
|
// notify everyone that startup has completed
|
|
this.isLoaded = true ;
|
|
gEventBus.emit( "app-loaded" ) ;
|
|
},
|
|
|
|
checkStartupStatus() {
|
|
// check the backend startup status
|
|
getJSON( gGetStartupStatusUrl ).then( (resp) => { //eslint-disable-line no-undef
|
|
if ( resp.status < 0 ) {
|
|
// startup has finished
|
|
$( "#startup-tasks-loading" ).fadeOut() ;
|
|
clearInterval( this.startupTimerId ) ;
|
|
this.startupTimerId = -1 ;
|
|
} else if ( resp.status == 2 ) {
|
|
// startup is in progress
|
|
// NOTE: We don't show the loading spinner for STARTED, since for most users, the startup process
|
|
// will be fast, and we don't want to show the spinner at all.
|
|
$( "#startup-tasks-loading" ).show() ;
|
|
}
|
|
} ).catch( (errorMsg) => { //eslint-disable-line no-unused-vars
|
|
// NOTE: We occasionally get an error on the first call, but we can probably ignore errors here.
|
|
} ) ;
|
|
},
|
|
|
|
onEscapePressed() {
|
|
// check if there are any notification balloons open
|
|
if ( $( ".growl" ).length > 0 ) {
|
|
// yup - close them all
|
|
// FUDGE! We used to trigger a click on the close button, so that the balloons would fade out,
|
|
// but there seems to be a bug where they would just build up, hidden. Just removing them
|
|
// from the DOM is a bit abrupt visually, but it's arguable that it's more in line with what
|
|
// the user wants (i.e. get rid of them!)
|
|
$( ".growl" ).each( function() {
|
|
$(this).remove() ;
|
|
} ) ;
|
|
return ;
|
|
}
|
|
// check if an image is currently zoomed
|
|
if ( $(".jquery-image-zoom").length > 0 ) {
|
|
// yup - no need to do anything here, the image will un-zoom itself
|
|
return ;
|
|
}
|
|
// check if the rule info popup is open
|
|
if ( this.$refs.navPane.closeRuleInfo() ) {
|
|
// yup - let the Escape close it
|
|
return ;
|
|
}
|
|
// if one of the non-search nav panes are open, switch to the search pane
|
|
gEventBus.emit( "activate-tab", "nav", "search" ) ;
|
|
},
|
|
|
|
},
|
|
|
|
} ) ;
|
|
|