Added box selection for mobiles and ability to drag markers, units can be moved only via context action

This commit is contained in:
Davide Passoni
2024-07-05 17:26:53 +02:00
parent 61e52efc07
commit fbc22676c5
21 changed files with 303 additions and 205 deletions

View File

@@ -34,14 +34,14 @@ export function OlCoalitionToggle(props: {
></div>
<span
className={`
ms-3 text-gray-900
ms-3 overflow-hidden text-ellipsis text-nowrap text-gray-900
dark:text-white
data-[flash='true']:after:animate-pulse
`}
>
{props.coalition
? `Coalition (${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)})`
: "Different values"}
? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}`
: "Diff. values"}
</span>
</div>
);

View File

@@ -19,7 +19,21 @@ export function OlDropdown(props: {
content.style.top = "0px";
content.style.height = "";
/* Get the position and size of the button and the content elements */
/* Get the position and size of the button */
var [bxl, byt, bxr, byb, bw, bh] = [
button.getBoundingClientRect().x,
button.getBoundingClientRect().y,
button.getBoundingClientRect().x + button.clientWidth,
button.getBoundingClientRect().y + button.clientHeight,
button.clientWidth,
button.clientHeight,
];
/* Set the minimum and maximum width to be equal to the button width */
content.style.minWidth = `${bw}px`;
content.style.maxWidth = `${bw}px`;
/* Get the position and size of the content element */
var [cxl, cyt, cxr, cyb, cw, ch] = [
content.getBoundingClientRect().x,
content.getBoundingClientRect().y,
@@ -28,14 +42,6 @@ export function OlDropdown(props: {
content.clientWidth,
content.clientHeight,
];
var [bxl, byt, bxr, byb, bbw, bh] = [
button.getBoundingClientRect().x,
button.getBoundingClientRect().y,
button.getBoundingClientRect().x + button.clientWidth,
button.getBoundingClientRect().y + button.clientHeight,
button.clientWidth,
button.clientHeight,
];
/* Limit the maximum height */
if (ch > 400) {
@@ -134,8 +140,8 @@ export function OlDropdown(props: {
ref={contentRef}
data-open={open}
className={`
absolute z-ui-4 divide-y divide-gray-100 overflow-y-scroll rounded-lg
bg-white p-2 shadow
absolute z-ui-4 divide-y divide-gray-100 overflow-y-scroll
no-scrollbar rounded-lg bg-white p-2 shadow
dark:bg-gray-700
data-[open='false']:hidden
`}

View File

@@ -31,7 +31,7 @@ export function OlLabelToggle(props: {
<span
data-active={!(props.toggled ?? false)}
className={`
my-auto pl-3 font-normal transition-colors z-ui-2
my-auto pl-3 font-normal transition-colors z-ui-1
dark:data-[active='false']:text-gray-400
dark:data-[active='true']:text-white
`}
@@ -41,7 +41,7 @@ export function OlLabelToggle(props: {
<span
data-active={props.toggled ?? false}
className={`
my-auto pr-3.5 font-normal transition-colors z-ui-2
my-auto pr-3.5 font-normal transition-colors z-ui-1
dark:data-[active='false']:text-gray-400
dark:data-[active='true']:text-white
`}

View File

@@ -9,7 +9,7 @@ export function OlNumberInput(props: {
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<div className="w-fit">
<div className="min-w-32">
<div className="relative flex max-w-[8rem] items-center">
<button
type="button"

View File

@@ -19,20 +19,24 @@ export function Menu(props: {
return (
<div
data-open={props.open}
data-hide={hide}
className={`
absolute left-16 right-0 top-[58px] bg-gray-200 backdrop-grayscale
z-ui-3 h-[calc(100vh-58px)] backdrop-blur-lg transition-transform
dark:bg-olympus-800/90
data-[hide='true']:-translate-y-[calc(100vh-1.5rem-58px)]
absolute left-16 right-0 top-[58px] bg-transparent z-ui-3
pointer-events-none h-[calc(100vh-58px)] transition-transform
data-[open='false']:-translate-x-full
sm:w-[400px]
`}
tabIndex={-1}
>
<div className={`
h-[calc(100vh-58px-1.5rem)] overflow-y-auto overflow-x-hidden
`}>
<div
data-hide={hide}
className={`
pointer-events-auto h-[calc(100vh-58px-2rem)] overflow-y-auto
overflow-x-hidden no-scrollbar backdrop-blur-lg backdrop-grayscale
transition-transform
dark:bg-olympus-800/90
data-[hide='true']:translate-y-[calc(100vh-58px)]
`}
>
<h5
className={`
inline-flex w-full items-center px-5 py-3 pb-2 font-semibold
@@ -65,11 +69,15 @@ export function Menu(props: {
{props.children}
</div>
{props.canBeHidden == true && (
<div className="flex h-6 justify-center" onClick={() => setHide(!hide)}>
<div className={`
flex h-8 justify-center z-ui-6 pointer-events-auto backdrop-blur-lg
backdrop-grayscale
dark:bg-olympus-800/90
`} onClick={() => setHide(!hide)}>
{hide ? (
<FaChevronDown className="mx-auto my-auto text-gray-400" />
) : (
<FaChevronUp className="mx-auto my-auto text-gray-400" />
) : (
<FaChevronDown className="mx-auto my-auto text-gray-400" />
)}
</div>
)}

View File

@@ -29,8 +29,24 @@ import {
olButtonsVisibilityNavyunit,
olButtonsVisibilityOlympus,
} from "../components/olicons";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
export function Header() {
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
function onScroll(ev) {
const sl = ev.target.scrollLeft;
const sr =
ev.target.scrollWidth - ev.target.scrollLeft - ev.target.clientWidth;
sl < 1 && !scrolledLeft && setScrolledLeft(true);
sl > 1 && scrolledLeft && setScrolledLeft(false);
sr < 1 && !scrolledRight && setScrolledRight(true);
sr > 1 && scrolledRight && setScrolledRight(false);
}
return (
<StateConsumer>
{(appState) => (
@@ -47,11 +63,21 @@ export function Header() {
src="images/icon.png"
className="my-auto h-10 w-10 rounded-md p-0"
></img>
{!scrolledLeft && (
<FaChevronLeft
className={`
absolute left-14 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200 z-ui-1
dark:bg-olympus-900
`}
/>
)}
<div
className={`
my-2 flex w-full items-center gap-3 overflow-x-scroll
no-scrollbar
`}
onScroll={(ev) => onScroll(ev)}
>
<div
className={`
@@ -228,6 +254,15 @@ export function Header() {
})}
</OlDropdown>
</div>
{!scrolledRight && (
<FaChevronRight
className={`
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200 z-ui-1
dark:bg-olympus-900
`}
/>
)}
</nav>
)}
</EventsConsumer>

View File

@@ -72,7 +72,7 @@ export function MiniMapPanel(props: {}) {
onClick={() => setShowMissionTime(!showMissionTime)}
className={`
absolute right-[10px]
${showMinimap ? `top-[232px]` : `top-[90px]`}
${showMinimap ? `top-[232px]` : `top-[70px]`}
w-[288px] z-ui-0 flex items-center justify-between
${showMinimap ? `rounded-b-lg` : `rounded-lg`}
bg-gray-200 p-3 text-sm backdrop-blur-lg backdrop-grayscale
@@ -114,13 +114,13 @@ export function MiniMapPanel(props: {}) {
onClick={() => {
getApp().getMap().setOption("showMinimap", false);
}}
/>
></FaChevronUp>
) : (
<FaChevronDown
onClick={() => {
getApp().getMap().setOption("showMinimap", true);
}}
/>
></FaChevronDown>
)}
</div>
);

View File

@@ -8,6 +8,8 @@ import {
faCog,
faQuestionCircle,
faPlusSquare,
faBox,
faObjectGroup,
} from "@fortawesome/free-solid-svg-icons";
import { EventsConsumer } from "../../eventscontext";
import { StateConsumer } from "../../statecontext";
@@ -17,7 +19,7 @@ export function SideBar() {
const [mapState, setMapState] = useState(IDLE);
document.addEventListener("mapStateChanged", (ev) => {
setMapState((ev as CustomEvent).detail)
setMapState((ev as CustomEvent).detail);
});
return (
@@ -71,6 +73,16 @@ export function SideBar() {
icon={faPencil}
tooltip="Hide/show drawing menu"
></OlStateButton>
<OlStateButton
onClick={() => {
document.dispatchEvent(
new CustomEvent("mapForceBoxSelect")
);
}}
checked={appState.mapBoxSelection}
icon={faObjectGroup}
tooltip="Enable box selection on the map"
></OlStateButton>
</div>
</div>
<div className="flex w-16 flex-wrap content-end justify-center p-4">

View File

@@ -98,7 +98,9 @@ export function SpawnMenu(props: {
<div className="p-5">
<OlSearchBar onChange={(ev) => setFilterString(ev.target.value)} />
<OlAccordion title={`Aircraft`}>
<div className="flex max-h-80 flex-col gap-1 overflow-y-scroll">
<div className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}>
{Object.keys(filteredAircraft).map((key) => {
const blueprint =
getApp().getAircraftDatabase().blueprints[key];
@@ -114,7 +116,9 @@ export function SpawnMenu(props: {
</div>
</OlAccordion>
<OlAccordion title={`Helicopters`}>
<div className="flex max-h-80 flex-col gap-1 overflow-y-scroll">
<div className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}>
{Object.keys(filteredHelicopters).map((key) => {
const blueprint =
getApp().getHelicopterDatabase().blueprints[key];
@@ -130,7 +134,9 @@ export function SpawnMenu(props: {
</div>
</OlAccordion>
<OlAccordion title={`SAM & AAA`}>
<div className="flex max-h-80 flex-col gap-1 overflow-y-scroll">
<div className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}>
{Object.keys(filteredAirDefense).map((key) => {
const blueprint =
getApp().getGroundUnitDatabase().blueprints[key];
@@ -146,7 +152,9 @@ export function SpawnMenu(props: {
</div>
</OlAccordion>
<OlAccordion title={`Ground Units`}>
<div className="flex max-h-80 flex-col gap-1 overflow-y-scroll">
<div className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}>
{Object.keys(filteredGroundUnits).map((key) => {
const blueprint =
getApp().getGroundUnitDatabase().blueprints[key];
@@ -162,7 +170,9 @@ export function SpawnMenu(props: {
</div>
</OlAccordion>
<OlAccordion title={`Ships and submarines`}>
<div className="flex max-h-80 flex-col gap-1 overflow-y-scroll">
<div className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}>
{Object.keys(filteredNavyUnits).map((key) => {
const blueprint =
getApp().getNavyUnitDatabase().blueprints[key];

View File

@@ -4,8 +4,9 @@ import { ContextActionSet } from "../../unit/contextactionset";
import { OlStateButton } from "../components/olstatebutton";
import { getApp } from "../../olympusapp";
import { ContextAction } from "../../unit/contextaction";
import { CONTEXT_ACTION, MOVE_UNIT } from "../../constants/constants";
import { CONTEXT_ACTION } from "../../constants/constants";
import { FaInfoCircle } from "react-icons/fa";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
export function UnitMouseControlBar(props: {}) {
var [open, setOpen] = useState(false);
@@ -16,6 +17,8 @@ export function UnitMouseControlBar(props: {}) {
var [activeContextAction, setActiveContextAction] = useState(
null as null | ContextAction
);
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
/* When a unit is selected, open the menu */
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
@@ -40,7 +43,7 @@ export function UnitMouseControlBar(props: {}) {
/* Deselect the context action when exiting state */
document.addEventListener("mapStateChanged", (ev) => {
setOpen((ev as CustomEvent).detail === CONTEXT_ACTION || (ev as CustomEvent).detail === MOVE_UNIT);
setOpen((ev as CustomEvent).detail === CONTEXT_ACTION);
});
/* Update the current values of the shown data */
@@ -58,6 +61,18 @@ export function UnitMouseControlBar(props: {}) {
setActiveContextAction(null);
}
function onScroll(ev) {
const sl = ev.target.scrollLeft;
const sr =
ev.target.scrollWidth - ev.target.scrollLeft - ev.target.clientWidth;
sl < 1 && !scrolledLeft && setScrolledLeft(true);
sl > 1 && scrolledLeft && setScrolledLeft(false);
sr < 1 && !scrolledRight && setScrolledRight(true);
sr > 1 && scrolledRight && setScrolledRight(false);
}
return (
<>
{" "}
@@ -65,39 +80,59 @@ export function UnitMouseControlBar(props: {}) {
<>
<div
className={`
absolute left-[50%] top-16 flex translate-x-[calc(-50%+2rem)]
gap-2 rounded-md bg-gray-200 p-2 z-ui-2
absolute left-[50%] top-16 flex max-w-[80%]
translate-x-[calc(-50%+2rem)] gap-2 rounded-md bg-gray-200 z-ui-2
dark:bg-olympus-900
`}
>
{Object.values(contextActionsSet.getContextActions()).map(
(contextAction) => {
return (
<OlStateButton
checked={contextAction === activeContextAction}
icon={contextAction.getIcon()}
tooltip={contextAction.getLabel()}
onClick={() => {
if (contextAction.getOptions().executeImmediately) {
setActiveContextAction(null);
contextAction.executeCallback(null, null);
} else {
if (activeContextAction != contextAction) {
setActiveContextAction(contextAction);
getApp().getMap().setState(CONTEXT_ACTION, {
contextAction: contextAction,
});
} else {
{!scrolledLeft && (
<FaChevronLeft
className={`
absolute left-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
<div
className="flex gap-2 overflow-x-auto no-scrollbar p-2"
onScroll={(ev) => onScroll(ev)}
>
{Object.values(contextActionsSet.getContextActions()).map(
(contextAction) => {
return (
<OlStateButton
checked={contextAction === activeContextAction}
icon={contextAction.getIcon()}
tooltip={contextAction.getLabel()}
onClick={() => {
if (contextAction.getOptions().executeImmediately) {
setActiveContextAction(null);
getApp()
.getMap()
.setState(MOVE_UNIT);
contextAction.executeCallback(null, null);
} else {
if (activeContextAction != contextAction) {
setActiveContextAction(contextAction);
getApp().getMap().setState(CONTEXT_ACTION, {
contextAction: contextAction,
});
} else {
setActiveContextAction(null);
}
}
}
}}
/>
);
}
}}
/>
);
}
)}
</div>
{!scrolledRight && (
<FaChevronRight
className={`
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
</div>
{activeContextAction && (
@@ -109,10 +144,12 @@ export function UnitMouseControlBar(props: {}) {
dark:bg-olympus-800
`}
>
<FaInfoCircle className={`
mr-2 hidden min-w-8 text-sm text-blue-500
md:block
`} />
<FaInfoCircle
className={`
mr-2 hidden min-w-8 text-sm text-blue-500
md:block
`}
/>
<div
className={`
px-2

View File

@@ -87,7 +87,11 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
<div className="flex flex-col">
<OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />
<div className="flex h-fit flex-col gap-5 px-5 pb-8 pt-6">
<div className="flex w-full flex-row content-center justify-between">
<div
className={`
inline-flex w-full flex-row content-center justify-between
`}
>
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
@@ -192,10 +196,7 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
Weapons
</span>
</div>
<OlDropdown
label={spawnLoadoutName}
className={`w-full w-max-full`}
>
<OlDropdown label={spawnLoadoutName} className={`w-full w-max-full`}>
{loadouts.map((loadout) => {
return (
<OlDropdownItem
@@ -230,7 +231,7 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
<div className="flex content-center gap-2">
<div
className={`
my-auto w-6 rounded-full py-0.5 text-center text-sm
my-auto w-6 min-w-6 rounded-full py-0.5 text-center text-sm
font-bold text-gray-500
dark:bg-[#17212D]
`}
@@ -239,7 +240,7 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint }) {
</div>
<div
className={`
my-auto text-sm
my-auto overflow-hidden text-ellipsis text-nowrap text-sm
dark:text-gray-300
`}
>

View File

@@ -51,6 +51,7 @@ export function UI() {
var [commandMode, setCommandMode] = useState(null as null | string);
var [mapSources, setMapSources] = useState([] as string[]);
var [activeMapSource, setActiveMapSource] = useState("");
var [mapBoxSelection, setMapBoxSelection] = useState(false);
document.addEventListener("hiddenTypesChanged", (ev) => {
setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() });
@@ -60,11 +61,11 @@ export function UI() {
setMapOptions({ ...getApp().getMap().getOptions() });
});
//document.addEventListener("mapStateChanged", (ev) => {
// if ((ev as CustomEvent).detail == IDLE) {
// hideAllMenus();
// }
//});
document.addEventListener("mapStateChanged", (ev) => {
if ((ev as CustomEvent).detail == IDLE) {
hideAllMenus();
}
});
document.addEventListener("mapSourceChanged", (ev) => {
var source = (ev as CustomEvent).detail;
@@ -80,6 +81,14 @@ export function UI() {
setActiveMapSource(sources[0]);
});
document.addEventListener("mapForceBoxSelect", (ev) => {
setMapBoxSelection(true);
});
document.addEventListener("mapSelectionEnd", (ev) => {
setMapBoxSelection(false);
});
function hideAllMenus() {
setMainMenuVisible(false);
setSpawnMenuVisible(false);
@@ -147,6 +156,7 @@ export function UI() {
mapHiddenTypes: mapHiddenTypes,
mapSources: mapSources,
activeMapSource: activeMapSource,
mapBoxSelection: mapBoxSelection,
}}
>
<EventsProvider