diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css index 2dadac0d..8dc6e68d 100644 --- a/client/public/stylesheets/layout/layout.css +++ b/client/public/stylesheets/layout/layout.css @@ -140,6 +140,16 @@ align-items: center; } +#slow-delete-popup { + align-self: center; + display:flex; + justify-self: center; + position: absolute; + width: fit-content; + height: fit-content; + z-index: 9999999999; +} + #log-panel { position: absolute; right: 0px; diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index 368571d0..37609fbd 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -699,7 +699,7 @@ nav.ol-panel> :last-child { overflow: hidden; width: 70%; max-width: 1200px; - z-index: 99999; + z-index: 999999; } @media (min-width: 1700px) { @@ -796,7 +796,7 @@ nav.ol-panel> :last-child { position: fixed; top: 0px; width: 100%; - z-index: 9999; + z-index: 99999; } #authentication-form { @@ -1148,7 +1148,7 @@ dl.ol-data-grid dd { background-color: var(--background-slate-blue); color: white; position: absolute; - z-index: 9999; + z-index: 999999; } .ol-panel.ol-dialog { @@ -1178,8 +1178,10 @@ dl.ol-data-grid dd { } .ol-dialog-footer { + align-content: center; border-top: 1px solid var(--background-grey); display: flex; + justify-content: center; padding-top: 15px; row-gap: 10px; } diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 0e01b227..3c633a25 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -209,4 +209,7 @@ export const MGRS_PRECISION_10KM = 2; export const MGRS_PRECISION_1KM = 3; export const MGRS_PRECISION_100M = 4; export const MGRS_PRECISION_10M = 5; -export const MGRS_PRECISION_1M = 6; \ No newline at end of file +export const MGRS_PRECISION_1M = 6; + +export const DELETE_CYCLE_TIME = 0.1; +export const DELETE_SLOW_THRESHOLD = 50; \ No newline at end of file diff --git a/client/src/dialog/dialog.ts b/client/src/dialog/dialog.ts new file mode 100644 index 00000000..78b4cf94 --- /dev/null +++ b/client/src/dialog/dialog.ts @@ -0,0 +1,19 @@ +import { Panel } from "../panels/panel"; + +export class Dialog extends Panel { + + constructor( element:string ) { + super( element ); + } + + hide() { + super.hide(); + document.getElementById( "gray-out" )?.classList.toggle("hide", true); + } + + show() { + super.show(); + document.getElementById( "gray-out" )?.classList.toggle("hide", false); + } + +} \ No newline at end of file diff --git a/client/src/olympusapp.ts b/client/src/olympusapp.ts index a8d9483d..551b6363 100644 --- a/client/src/olympusapp.ts +++ b/client/src/olympusapp.ts @@ -34,21 +34,26 @@ export class OlympusApp { #map: Map | null = null; /* Managers */ + #dialogManager!: Manager; + #missionManager: MissionManager | null = null; + #panelsManager: Manager | null = null; + #pluginsManager: PluginsManager | null = null; + #popupsManager: Manager | null = null; #serverManager: ServerManager | null = null; + #shortcutManager: ShortcutManager | null = null; + #toolbarsManager: Manager | null = null; #unitsManager: UnitsManager | null = null; #weaponsManager: WeaponsManager | null = null; - #missionManager: MissionManager | null = null; - #pluginsManager: PluginsManager | null = null; - #panelsManager: Manager | null = null; - #popupsManager: Manager | null = null; - #toolbarsManager: Manager | null = null; - #shortcutManager: ShortcutManager | null = null; constructor() { } // TODO add checks on null + getDialogManager() { + return this.#dialogManager as Manager; + } + getMap() { return this.#map as Map; } @@ -163,16 +168,14 @@ export class OlympusApp { /* Initialize base functionalitites */ this.#map = new Map('map-container'); - this.#serverManager = new ServerManager(); - this.#unitsManager = new UnitsManager(); - this.#weaponsManager = new WeaponsManager(); this.#missionManager = new MissionManager(); - - this.#shortcutManager = new ShortcutManager(); - this.#panelsManager = new Manager(); this.#popupsManager = new Manager(); + this.#serverManager = new ServerManager(); + this.#shortcutManager = new ShortcutManager(); this.#toolbarsManager = new Manager(); + this.#unitsManager = new UnitsManager(); + this.#weaponsManager = new WeaponsManager(); // Panels this.getPanelsManager() @@ -186,7 +189,8 @@ export class OlympusApp { .add("unitList", new UnitListPanel("unit-list-panel", "unit-list-panel-content")) // Popups - this.getPopupsManager().add("infoPopup", new Popup("info-popup")); + this.getPopupsManager() + .add("infoPopup", new Popup("info-popup")); // Toolbars this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar")) diff --git a/client/src/server/servermanager.ts b/client/src/server/servermanager.ts index cb6286f6..c5321799 100644 --- a/client/src/server/servermanager.ts +++ b/client/src/server/servermanager.ts @@ -541,14 +541,15 @@ export class ServerManager { } setConnected(newConnected: boolean) { - if (this.#connected != newConnected) + if (this.#connected != newConnected) { newConnected ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Connected to DCS Olympus server") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Disconnected from DCS Olympus server"); - this.#connected = newConnected; - - if (this.#connected) { - document.querySelector("#splash-screen")?.classList.add("hide"); - document.querySelector("#gray-out")?.classList.add("hide"); + if (newConnected) { + document.getElementById("splash-screen")?.classList.add("hide"); + document.getElementById("gray-out")?.classList.add("hide"); + } } + + this.#connected = newConnected; } getConnected() { diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index 398d074a..7e6fa97d 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -4,7 +4,7 @@ import { Unit } from "./unit"; import { bearingAndDistanceToLatLng, deg2rad, getGroundElevation, getUnitDatabaseByCategory, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils"; import { CoalitionArea } from "../map/coalitionarea/coalitionarea"; import { groundUnitDatabase } from "./databases/groundunitdatabase"; -import { DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants"; +import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { citiesDatabase } from "./citiesDatabase"; import { aircraftDatabase } from "./databases/aircraftdatabase"; @@ -14,35 +14,40 @@ import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker"; import { Popup } from "../popups/popup"; import { HotgroupPanel } from "../panels/hotgrouppanel"; import { Contact, UnitData, UnitSpawnTable } from "../interfaces"; +import { Dialog } from "../dialog/dialog"; /** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only * result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows * to avoid client/server and client/client inconsistencies. */ export class UnitsManager { - #units: { [ID: number]: Unit }; #copiedUnits: UnitData[]; - #selectionEventDisabled: boolean = false; #deselectionEventDisabled: boolean = false; #requestDetectionUpdate: boolean = false; + #selectionEventDisabled: boolean = false; + #slowDeleteDialog!:Dialog; + #units: { [ID: number]: Unit }; + constructor() { - this.#units = {}; this.#copiedUnits = []; + this.#units = {}; + document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) }); + document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true }); document.addEventListener('copy', () => this.selectedUnitsCopy()); - document.addEventListener('paste', () => this.pasteUnits()); - document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); - document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true)); - document.addEventListener('keyup', (event) => this.#onKeyUp(event)); document.addEventListener('exportToFile', () => this.exportToFile()); document.addEventListener('importFromFile', () => this.importFromFile()); - document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true }); - document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) }); - document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) }); + document.addEventListener('keyup', (event) => this.#onKeyUp(event)); + document.addEventListener('paste', () => this.pasteUnits()); document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.selectedUnitsChangeAltitude(e.detail.type) }); + document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) }); + document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); + document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); + + this.#slowDeleteDialog = new Dialog( "slow-delete-dialog" ); } /** @@ -638,7 +643,7 @@ export class UnitsManager { try { groundElevation = parseFloat(response); } catch { - console.log("Simulate fire fight: could not retrieve ground elevation") + console.warn("Simulate fire fight: could not retrieve ground elevation") } for (let idx in selectedUnits) { selectedUnits[idx].simulateFireFight(latlng, groundElevation); @@ -754,14 +759,24 @@ export class UnitsManager { return; } - var immediate = false; - if (selectedUnits.length > 20) - immediate = confirm(`You are trying to delete ${selectedUnits.length} units, do you want to delete them immediately? This may cause lag for players.`) - - for (let idx in selectedUnits) { - selectedUnits[idx].delete(explosion, immediate); + const doDelete = (explosion = false, immediate = false) => { + const selectedUnits = this.getSelectedUnits(); + for (let idx in selectedUnits) { + selectedUnits[idx].delete(explosion, immediate); + } + this.#showActionMessage(selectedUnits, `deleted`); } - this.#showActionMessage(selectedUnits, `deleted`); + + if (selectedUnits.length >= DELETE_SLOW_THRESHOLD) + this.#showSlowDeleteDialog(selectedUnits).then((action:any) => { + if (action === "delete-slow") + doDelete(explosion, false); + else if (action === "delete-immediate") + doDelete(explosion, true); + }) + else + doDelete(explosion); + } /** Compute the destinations of every unit in the selected units. This function preserves the relative positions of the units, and rotates the whole formation by rotation. @@ -1083,4 +1098,32 @@ export class UnitsManager { else if (units.length > 1) (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`); } + + #showSlowDeleteDialog(selectedUnits:Unit[]) { + let button:HTMLButtonElement | null = null; + const deletionTime = Math.round( selectedUnits.length * DELETE_CYCLE_TIME ).toString(); + const dialog = this.#slowDeleteDialog; + const element = dialog.getElement(); + const listener = (ev:MouseEvent) => { + if (ev.target instanceof HTMLButtonElement && ev.target.matches("[data-action]")) + button = ev.target; + } + + element.querySelectorAll(".deletion-count").forEach( el => el.innerHTML = selectedUnits.length.toString() ); + element.querySelectorAll(".deletion-time").forEach( el => el.innerHTML = deletionTime ); + dialog.show(); + + return new Promise((resolve) => { + element.addEventListener("click", listener); + + const interval = setInterval(() => { + if (button instanceof HTMLButtonElement ) { + clearInterval(interval); + dialog.hide(); + element.removeEventListener("click", listener); + resolve( button.getAttribute("data-action") ); + } + }, 250); + }); + } } \ No newline at end of file diff --git a/client/views/other/dialogs.ejs b/client/views/other/dialogs.ejs index 1d56160c..55cee3f6 100644 --- a/client/views/other/dialogs.ejs +++ b/client/views/other/dialogs.ejs @@ -304,4 +304,27 @@ - \ No newline at end of file + + + +