fix: buttons and actions no longer getting stuck

fix: context menu resizes if accordion is opened
feat: added expanding descriptions for context actions
This commit is contained in:
Davide Passoni 2024-12-11 18:06:59 +01:00
parent e9896963ca
commit 7fe9b0d19f
16 changed files with 145 additions and 64 deletions

View File

@ -2267,7 +2267,7 @@
"coalition": "red",
"era": "Mid Cold War",
"category": "groundunit",
"label": "AK-74",
"label": "AK-74 (Type 1)",
"shortLabel": "AK-74",
"filename": "",
"type": "Infantry",
@ -5558,7 +5558,7 @@
"coalition": "red",
"era": "Early Cold War",
"category": "groundunit",
"label": "AK-74",
"label": "AK-74 (Type 4)",
"shortLabel": "AK-74",
"filename": "",
"type": "Infantry",
@ -7722,7 +7722,7 @@
"coalition": "red",
"era": "Early Cold War",
"category": "groundunit",
"label": "Insurgent AK-74",
"label": "AK-74 (Insurgent)",
"shortLabel": "AK-74 (Ins)",
"type": "Infantry",
"enabled": true,
@ -7880,7 +7880,7 @@
"coalition": "red",
"era": "Late Cold War",
"category": "groundunit",
"label": "AK-74",
"label": "AK-74 (Type 2)",
"shortLabel": "AK-74",
"type": "Infantry",
"enabled": true,
@ -7974,7 +7974,7 @@
"coalition": "red",
"era": "Late Cold War",
"category": "groundunit",
"label": "AK-74",
"label": "AK-74 (Type 3)",
"shortLabel": "AK-74",
"type": "Infantry",
"enabled": true,

View File

@ -513,7 +513,7 @@ export namespace ContextActions {
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.MOVE, code: "ControlLeft", shiftKey: false }
{ type: ContextActionType.MOVE, code: null}
);
export const DELETE = new ContextAction(

View File

@ -12,7 +12,6 @@ export var BoxSelect = Handler.extend({
this._map = map;
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
this._resetStateTimeout = 0;
map.on("unload", this._destroy, this);
},
@ -34,23 +33,12 @@ export var BoxSelect = Handler.extend({
},
_resetState: function () {
this._resetStateTimeout = 0;
this._moved = false;
},
_clearDeferredResetState: function () {
if (this._resetStateTimeout !== 0) {
clearTimeout(this._resetStateTimeout);
this._resetStateTimeout = 0;
}
},
_onMouseDown: function (e: any) {
if (this._map.getSelectionEnabled() && e.button == 0) {
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
if (this._moved) this._finish();
DomUtil.disableImageDrag();
this._map.dragging.disable();
@ -66,7 +54,7 @@ export var BoxSelect = Handler.extend({
touchmove: this._onMouseMove,
touchend: this._onMouseUp,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp
mouseup: this._onMouseUp,
},
this
);
@ -76,20 +64,10 @@ export var BoxSelect = Handler.extend({
},
_onMouseUp: function (e: any) {
if (e.button !== 0) {
return;
}
this._finish();
if (!this._moved) {
return;
}
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
if (e.button !== 0) return;
window.setTimeout(Util.bind(this._finish, this), 0);
if (!this._moved) return;
var bounds = new LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point));
this._map.fire("selectionend", { selectionBounds: bounds });
},
@ -141,5 +119,7 @@ export var BoxSelect = Handler.extend({
},
this
);
this._resetState();
},
});

View File

@ -297,6 +297,11 @@ export class Map extends L.Map {
ContextActionChangedEvent.on((contextAction) => this.#updateDestinationPreviewMarkers());
MapOptionsChangedEvent.on((mapOptions) => this.#moveDestinationPreviewMarkers());
window.addEventListener("blur", () => {
this.setSelectionEnabled(false);
this.setPasteEnabled(false);
})
/* Pan interval */
this.#panInterval = window.setInterval(() => {
if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft)
@ -806,6 +811,10 @@ export class Map extends L.Map {
return this.#contextAction;
}
getDefaultContextAction() {
return this.#contextActionSet?.getDefaultContextAction();
}
executeDefaultContextAction(targetUnit: Unit | null, targetPosition: L.LatLng | null, originalEvent?: MouseEvent) {
this.#contextActionSet?.getDefaultContextAction()?.executeCallback(targetUnit, targetPosition, originalEvent);
}

View File

@ -1,8 +1,16 @@
import { DivIcon, Map, Marker, MarkerOptions, LatLngExpression } from "leaflet";
import { SelectionEnabledChangedEvent } from "../../events";
export class CustomMarker extends Marker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
SelectionEnabledChangedEvent.on((enabled) => {
const el = this.getElement();
if (el === undefined) return;
if (enabled) el.classList.add("disable-pointer-events");
else el.classList.remove("disable-pointer-events");
});
}
onAdd(map: Map): this {

View File

@ -32,3 +32,7 @@
[data-awacs-mode] .airbase-icon svg * {
fill: transparent !important;
}
#map-container .leaflet-airbase-marker.airbase-disable-pointer-events {
pointer-events: none;
}

View File

@ -234,3 +234,7 @@ path.leaflet-interactive:focus {
.smoke-orange-cursor {
cursor: url("/images/cursors/smoke-orange.svg"), auto !important;
}
#map-container .disable-pointer-events {
pointer-events: none;
}

View File

@ -28,12 +28,14 @@ export class Airbase extends CustomMarker {
this.#img = document.createElement("img");
AppStateChangedEvent.on((state, subState) => {
const element = this.getElement();
if (element) element.style.pointerEvents = state === OlympusState.IDLE || state === OlympusState.AIRBASE ? "all" : "none";
const el = this.getElement();
if (el === undefined) return;
if (state === OlympusState.IDLE || state === OlympusState.AIRBASE) el.classList.remove("airbase-disable-pointer-events");
else el.classList.add("airbase-disable-pointer-events");
});
AirbaseSelectedEvent.on((airbase) => {
this.#selected = (airbase === this);
this.#selected = airbase === this;
if (this.getElement()?.querySelector(".airbase-icon"))
(this.getElement()?.querySelector(".airbase-icon") as HTMLElement).dataset.selected = `${this.#selected}`;
});

View File

@ -20,7 +20,17 @@ export class Shortcut {
document.addEventListener("keyup", (ev: any) => {
if (this.#modal) return;
if (this.#keydown && this.getOptions().code === ev.code) {
console.log(`Keydown up for shortcut ${this.#id}`)
console.log(`Keyup for shortcut ${this.#id}`)
ev.preventDefault();
this.#keydown = false;
this.getOptions().keyUpCallback(ev);
}
});
/* Forced keyup, in case the window loses focus */
document.addEventListener("blur", (ev: any) => {
if (this.#keydown) {
console.log(`Keyup (forced by blur) for shortcut ${this.#id}`)
ev.preventDefault();
this.#keydown = false;
this.getOptions().keyUpCallback(ev);

View File

@ -9,7 +9,7 @@ export function OlStateButton(props: {
buttonColor?: string | null;
checked: boolean;
icon?: IconProp;
tooltip?: string | JSX.Element | JSX.Element[];
tooltip?: string | (() => JSX.Element | JSX.Element[]);
tooltipPosition?: string;
onClick: () => void;
onMouseUp?: () => void;
@ -64,7 +64,7 @@ export function OlStateButton(props: {
{props.children}
</div>
</button>
{hover && props.tooltip && <OlTooltip buttonRef={buttonRef} content={props.tooltip} position={props.tooltipPosition}/>}
{hover && props.tooltip && <OlTooltip buttonRef={buttonRef} content={typeof(props.tooltip) === "string" ? props.tooltip: props.tooltip()} position={props.tooltipPosition}/>}
</>
);
}

View File

@ -72,6 +72,12 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
const button = props.buttonRef.current as HTMLButtonElement;
setPosition(content, button);
const resizeObserver = new ResizeObserver(() => {
setPosition(content, button);
});
resizeObserver.observe(content);
return () => resizeObserver.disconnect(); // clean up
}
});

View File

@ -26,6 +26,7 @@ export function MapContextMenu(props: {}) {
const [latLng, setLatLng] = useState(null as null | LatLng);
const [unit, setUnit] = useState(null as null | Unit);
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [height, setHeight] = useState(0);
var contentRef = useRef(null);
@ -68,6 +69,14 @@ export function MapContextMenu(props: {}) {
content.style.left = `${newXPosition}px`;
content.style.top = `${newYposition}px`;
setHeight(content.clientHeight);
const resizeObserver = new ResizeObserver(() => {
setHeight(content.clientHeight);
});
resizeObserver.observe(content);
return () => resizeObserver.disconnect(); // clean up
}
});

View File

@ -62,6 +62,7 @@ export function SpawnContextMenu(props: {}) {
const [showCost, setShowCost] = useState(false);
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
const [showMore, setShowMore] = useState(false);
const [height, setHeight] = useState(0);
useEffect(() => {
if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole));
@ -152,17 +153,24 @@ export function SpawnContextMenu(props: {}) {
content.style.left = `${newXPosition}px`;
content.style.top = `${newYposition}px`;
const resizeObserver = new ResizeObserver(() => {
setHeight(content.clientHeight);
});
resizeObserver.observe(content);
return () => resizeObserver.disconnect(); // clean up
}
});
// TODO fix button being moved if overflowing
return (
<>
<div
ref={contentRef}
data-hidden={appState !== OlympusState.SPAWN_CONTEXT}
className={`
absolute flex w-[395px] data- flex-wrap gap-2 rounded-md
bg-olympus-800
absolute flex w-[395px] data- max-h-[800px] flex-wrap gap-2
overflow-auto rounded-md bg-olympus-800
data-[hidden=true]:hidden
`}
>

View File

@ -152,26 +152,28 @@ export function MapToolBar(props: {}) {
key={"select"}
checked={selectionEnabled}
icon={faObjectGroup}
tooltip={
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["toggleSelectionEnabled"]?.getOptions())}
<div className="my-auto">Box selection</div>
tooltip={() => (
<div
className="overflow-hidden"
style={{ animationName: "fadeIn", animationDuration: "1s", animationFillMode: "forwards", height: "25px" }}
>
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["toggleSelectionEnabled"]?.getOptions())}
<div className="my-auto">Box selection</div>
</div>
</div>
}
)}
tooltipPosition="side"
onClick={() => {
getApp().getMap().setSelectionEnabled(!selectionEnabled);
if (!selectionEnabled) {
getApp()
.getMap()
.getContainer()
.addEventListener(
"mouseup",
() => {
getApp().getMap().setSelectionEnabled(false);
},
{ once: true, signal: controller.signal }
);
window.addEventListener(
"mouseup",
() => {
getApp().getMap().setSelectionEnabled(false);
},
{ once: true, signal: controller.signal }
);
} else {
controller.abort();
}
@ -184,12 +186,12 @@ export function MapToolBar(props: {}) {
key={"copy"}
checked={false}
icon={faCopy}
tooltip={
tooltip={() => (
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["copyUnits"]?.getOptions())}
<div className="my-auto">Copy selected units</div>
</div>
}
)}
tooltipPosition="side"
onClick={() => {
getApp().getUnitsManager().copy(selectedUnits);
@ -199,14 +201,21 @@ export function MapToolBar(props: {}) {
)}
{copiedUnitsData.length > 0 && (
<div className="flex flex-col gap-1">
<OlStateButton key={"paste"} checked={pasteEnabled} icon={faPaste} tooltip={
<OlStateButton
key={"paste"}
checked={pasteEnabled}
icon={faPaste}
tooltip={() => (
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["pasteUnits"]?.getOptions())}
<div className="my-auto">Paste copied units</div>
</div>
} tooltipPosition="side" onClick={() => {
getApp().getMap().setPasteEnabled(!pasteEnabled)
}} />
)}
tooltipPosition="side"
onClick={() => {
getApp().getMap().setPasteEnabled(!pasteEnabled);
}}
/>
</div>
)}
</>
@ -218,12 +227,18 @@ export function MapToolBar(props: {}) {
key={contextActionIt.getId()}
checked={contextActionIt === contextAction}
icon={contextActionIt.getIcon()}
tooltip={
tooltip={() => (
<div
className="overflow-hidden"
style={{ animationName: "tooltipFadeInHeight", animationDuration: "1s", animationFillMode: "forwards", height: "25px" }}
>
<div className="flex content-center gap-2">
{shortcutCombination(contextActionIt.getOptions())}
<div className="my-auto">{contextActionIt.getLabel()}</div>
</div>
}
<div className={"max-w-[200px] text-wrap"} style={{ animationName: "tooltipFadeInWidth", animationDuration: "1s", animationFillMode: "forwards", width: "1px" }}>{contextActionIt.getDescription()}</div>
</div>
)}
tooltipPosition="side"
buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]}
onClick={() => {

View File

@ -103,4 +103,22 @@ input[type="range"]:focus::-moz-range-thumb {
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@keyframes tooltipFadeInHeight {
99% {
height: 25px;
}
100% {
height: fit-content;
}
}
@keyframes tooltipFadeInWidth {
99% {
width: 0px;
}
100% {
width: 200px;
}
}

View File

@ -1372,6 +1372,14 @@ export abstract class Unit extends CustomMarker {
#onRightShortClick(e: any) {
console.log(`Right short click on ${this.getUnitName()}`);
window.clearTimeout(this.#rightMouseDownTimeout);
if (
getApp().getState() === OlympusState.UNIT_CONTROL &&
getApp().getMap().getDefaultContextAction() &&
getApp().getMap().getDefaultContextAction()?.getTarget() === ContextActionTarget.POINT
)
getApp().getMap().executeDefaultContextAction(null, this.getPosition(), e.originalEvent);
}
#onRightLongClick(e: any) {