forked from MapComplete/MapComplete
Add hotkeys, document hotkeys, make 'DefaultGUI' runnable via NodeJS, generate hotkey documentation
This commit is contained in:
parent
e2419c19cd
commit
43613e4ece
16 changed files with 328 additions and 89 deletions
110
UI/Base/Hotkeys.ts
Normal file
110
UI/Base/Hotkeys.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import Combine from "./Combine"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Title from "./Title"
|
||||
import Table from "./Table"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
|
||||
export default class Hotkeys {
|
||||
private static readonly _docs: UIEventSource<
|
||||
{
|
||||
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
|
||||
documentation: string
|
||||
}[]
|
||||
> = new UIEventSource<
|
||||
{
|
||||
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
|
||||
documentation: string
|
||||
}[]
|
||||
>([])
|
||||
public static RegisterHotkey(
|
||||
key: (
|
||||
| {
|
||||
ctrl: string
|
||||
}
|
||||
| {
|
||||
shift: string
|
||||
}
|
||||
| {
|
||||
alt: string
|
||||
}
|
||||
| {
|
||||
nomod: string
|
||||
}
|
||||
) & {
|
||||
onUp?: boolean
|
||||
},
|
||||
documentation: string,
|
||||
action: () => void
|
||||
) {
|
||||
const type = key["onUp"] ? "keyup" : "keypress"
|
||||
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
|
||||
if (keycode.length == 1) {
|
||||
keycode = keycode.toLowerCase()
|
||||
if (key["shift"] !== undefined) {
|
||||
keycode = keycode.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
this._docs.data.push({ key, documentation })
|
||||
this._docs.ping()
|
||||
if (Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
if (key["ctrl"] !== undefined) {
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.ctrlKey && event.key === keycode) {
|
||||
action()
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
} else if (key["shift"] !== undefined) {
|
||||
document.addEventListener(type, function (event) {
|
||||
if (event.shiftKey && event.key === keycode) {
|
||||
action()
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
} else if (key["alt"] !== undefined) {
|
||||
document.addEventListener(type, function (event) {
|
||||
if (event.altKey && event.key === keycode) {
|
||||
action()
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
} else if (key["nomod"] !== undefined) {
|
||||
document.addEventListener(type, function (event) {
|
||||
if (event.key === keycode) {
|
||||
action()
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static generateDocumentation(): BaseUIElement {
|
||||
return new Combine([
|
||||
new Title("Hotkeys", 1),
|
||||
"MapComplete supports the following keys:",
|
||||
new Table(
|
||||
["Key combination", "Action"],
|
||||
Hotkeys._docs.data
|
||||
.map(({ key, documentation }) => {
|
||||
const modifiers = Object.keys(key).filter(
|
||||
(k) => k !== "nomod" && k !== "onUp"
|
||||
)
|
||||
const keycode: string =
|
||||
key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
|
||||
modifiers.push(keycode)
|
||||
return [modifiers.join("+"), documentation]
|
||||
})
|
||||
.sort()
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
static generateDocumentationDynamic(): BaseUIElement {
|
||||
return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation()))
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,83 @@ import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
|||
import AvailableBaseLayersImplementation from "../../Logic/Actors/AvailableBaseLayersImplementation"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import ScrollableFullScreen from "./ScrollableFullScreen"
|
||||
import Constants from "../../Models/Constants"
|
||||
import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
|
||||
|
||||
/**
|
||||
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
|
||||
* Shows the given uiToShow-element in the messagebox
|
||||
*/
|
||||
export class StrayClickHandlerImplementation {
|
||||
private _lastMarker
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
|
||||
selectedElement: UIEventSource<string>
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
leafletMap: UIEventSource<L.Map>
|
||||
},
|
||||
uiToShow: ScrollableFullScreen,
|
||||
iconToShow: BaseUIElement
|
||||
) {
|
||||
const self = this
|
||||
const leafletMap = state.leafletMap
|
||||
state.filteredLayers.data.forEach((filteredLayer) => {
|
||||
filteredLayer.isDisplayed.addCallback((isEnabled) => {
|
||||
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
|
||||
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
|
||||
// This reclick might be at a location where a feature now appeared...
|
||||
state.leafletMap.data.removeLayer(self._lastMarker)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
state.LastClickLocation.addCallback(function (lastClick) {
|
||||
if (self._lastMarker !== undefined) {
|
||||
state.leafletMap.data?.removeLayer(self._lastMarker)
|
||||
}
|
||||
|
||||
if (lastClick === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
state.selectedElement.setData(undefined)
|
||||
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
|
||||
self._lastMarker = L.marker(clickCoor, {
|
||||
icon: L.divIcon({
|
||||
html: iconToShow.ConstructElement(),
|
||||
iconSize: [50, 50],
|
||||
iconAnchor: [25, 50],
|
||||
popupAnchor: [0, -45],
|
||||
}),
|
||||
})
|
||||
|
||||
self._lastMarker.addTo(leafletMap.data)
|
||||
|
||||
self._lastMarker.on("click", () => {
|
||||
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
leafletMap.data.flyTo(
|
||||
clickCoor,
|
||||
Constants.userJourney.minZoomLevelToAddNewPoints
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
uiToShow.Activate()
|
||||
})
|
||||
})
|
||||
|
||||
state.selectedElement.addCallback(() => {
|
||||
if (self._lastMarker !== undefined) {
|
||||
leafletMap.data.removeLayer(self._lastMarker)
|
||||
this._lastMarker = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
|
||||
private static _nextId = 0
|
||||
public readonly leafletMap: UIEventSource<Map>
|
||||
|
|
@ -53,6 +129,18 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
AvailableBaseLayers.implement(new AvailableBaseLayersImplementation())
|
||||
Minimap.createMiniMap = (options) => new MinimapImplementation(options)
|
||||
ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options)
|
||||
StrayClickHandler.construct = (
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
|
||||
selectedElement: UIEventSource<string>
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
leafletMap: UIEventSource<L.Map>
|
||||
},
|
||||
uiToShow: ScrollableFullScreen,
|
||||
iconToShow: BaseUIElement
|
||||
) => {
|
||||
return new StrayClickHandlerImplementation(state, uiToShow, iconToShow)
|
||||
}
|
||||
}
|
||||
|
||||
public installBounds(factor: number | BBox, showRange?: boolean) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { UIEventSource } from "../../Logic/UIEventSource"
|
|||
import Hash from "../../Logic/Web/Hash"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Title from "./Title"
|
||||
import Hotkeys from "./Hotkeys"
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -82,12 +83,11 @@ export default class ScrollableFullScreen {
|
|||
}
|
||||
|
||||
private static initEmpty(): FixedUiElement {
|
||||
document.addEventListener("keyup", function (event) {
|
||||
if (event.code === "Escape") {
|
||||
ScrollableFullScreen.collapse()
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "Escape", onUp: true },
|
||||
"Close the sidebar",
|
||||
ScrollableFullScreen.collapse
|
||||
)
|
||||
|
||||
return new FixedUiElement("")
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ export default class ScrollableFullScreen {
|
|||
this._fullscreencomponent.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen")
|
||||
ScrollableFullScreen._currentlyOpen = this
|
||||
fs.classList.remove("hidden")
|
||||
fs?.classList?.remove("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue