forked from MapComplete/MapComplete
170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
import Hash from "./Hash"
|
|
import { MenuState } from "../../Models/MenuState"
|
|
import { AndroidPolyfill } from "./AndroidPolyfill"
|
|
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
|
import { Feature } from "geojson"
|
|
import { Store, UIEventSource } from "../UIEventSource"
|
|
|
|
export default class ThemeViewStateHashActor {
|
|
private readonly _state: {
|
|
indexedFeatures: IndexedFeatureSource
|
|
selectedElement: UIEventSource<Feature>
|
|
guistate: MenuState
|
|
}
|
|
private isUpdatingHash = false
|
|
|
|
public static readonly documentation = [
|
|
"The URL-hash can contain multiple values:",
|
|
"",
|
|
"- The id of the currently selected object, e.g. `node/1234`",
|
|
"- The currently opened menu view",
|
|
"",
|
|
"### Possible hashes to open a menu",
|
|
"",
|
|
"The possible hashes are:",
|
|
"",
|
|
MenuState.pageNames.map((tab) => "`" + tab + "`").join(","),
|
|
]
|
|
|
|
/**
|
|
* Converts the hash to the appropriate theme-view state and, vice versa, sets the hash.
|
|
*
|
|
* As the navigator-back-button changes the hash first, this class thus also handles the (browser) 'back'-button events.
|
|
*
|
|
* Note that there is no "real" way to intercept the back button, we can only detect the removal of the hash.
|
|
* As such, we use a change in the hash to close the appropriate windows
|
|
*
|
|
*/
|
|
constructor(state: {
|
|
featureSwitches: { featureSwitchBackToThemeOverview: Store<boolean> }
|
|
indexedFeatures: IndexedFeatureSource
|
|
selectedElement: UIEventSource<Feature>
|
|
guistate: MenuState
|
|
}) {
|
|
this._state = state
|
|
AndroidPolyfill.onBackButton(() => this.back(), {
|
|
returnToIndex: state.featureSwitches.featureSwitchBackToThemeOverview,
|
|
})
|
|
|
|
const hashOnLoad = Hash.hash.data
|
|
const containsMenu = this.loadStateFromHash(hashOnLoad)
|
|
// First of all, try to recover the selected element
|
|
if (!containsMenu && hashOnLoad?.length > 0) {
|
|
state.indexedFeatures.featuresById.addCallbackAndRunD(() => {
|
|
// once that we have found a matching element, we can be sure the indexedFeaturesource was popuplated and that the job is done
|
|
return this.loadSelectedElementFromHash(hashOnLoad)
|
|
})
|
|
}
|
|
|
|
// At last, register callbacks on the state to update the hash when they change.
|
|
// Note: these should use 'addCallback', not 'addCallbackAndRun'
|
|
state.selectedElement.addCallback(() => this.setHash())
|
|
|
|
// Register a hash change listener to correctly handle the back button
|
|
Hash.hash.addCallback((hash) => {
|
|
if (this.isUpdatingHash) {
|
|
return
|
|
}
|
|
if (!hash) {
|
|
this.back()
|
|
} else {
|
|
if (!this.loadStateFromHash(hash)) {
|
|
this.loadSelectedElementFromHash(hash)
|
|
}
|
|
}
|
|
})
|
|
|
|
for (const key in state.guistate.pageStates) {
|
|
const toggle = state.guistate.pageStates[key]
|
|
toggle.addCallback(() => this.setHash())
|
|
}
|
|
|
|
// When all is done, set the hash. This must happen last to give the code above correct info
|
|
this.setHash()
|
|
}
|
|
|
|
/**
|
|
* Selects the appropriate element
|
|
* Returns true if this method can be unregistered for the first run
|
|
*/
|
|
private loadSelectedElementFromHash(hash: string): boolean {
|
|
const state = this._state
|
|
const selectedElement = state.selectedElement
|
|
|
|
// Set the hash based on the selected element...
|
|
// ... search and select an element based on the hash
|
|
if (selectedElement.data?.properties?.id === hash) {
|
|
// We already have the correct hash
|
|
return true
|
|
}
|
|
|
|
const found = state.indexedFeatures.featuresById.data?.get(hash)
|
|
if (!found) {
|
|
return false
|
|
}
|
|
if (found.properties.id.startsWith("last_click")) {
|
|
return true
|
|
}
|
|
console.log(
|
|
"Setting selected element based on hash",
|
|
hash,
|
|
"; found",
|
|
found,
|
|
"current:",
|
|
selectedElement.data?.properties?.id
|
|
)
|
|
selectedElement.setData(found)
|
|
return true
|
|
}
|
|
|
|
private loadStateFromHash(hash: string): boolean {
|
|
for (const page in this._state.guistate.pageStates) {
|
|
if (page === hash) {
|
|
const toggle = this._state.guistate.pageStates[page]
|
|
toggle.set(true)
|
|
console.log("Loading menu view from hash:", page)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Sets the hash based on:
|
|
*
|
|
* 1. Selected element ID
|
|
* 2. A selected 'page' from the menu
|
|
*
|
|
*/
|
|
private setHash(): void {
|
|
// Important ! This function is called by 'addCallback' and might thus never return 'true' or it will be unregistered
|
|
this.isUpdatingHash = true
|
|
try {
|
|
const selectedElement = this._state.selectedElement.data
|
|
if (selectedElement) {
|
|
Hash.hash.set(selectedElement.properties.id)
|
|
return
|
|
}
|
|
for (const page in this._state.guistate.pageStates) {
|
|
const toggle = this._state.guistate.pageStates[page]
|
|
if (toggle.data) {
|
|
Hash.hash.set(page)
|
|
return
|
|
}
|
|
}
|
|
Hash.hash.set(undefined)
|
|
return
|
|
} finally {
|
|
this.isUpdatingHash = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns 'true' if an action was taken
|
|
* @private
|
|
*/
|
|
private back(): boolean {
|
|
const state = this._state
|
|
return state.guistate.closeAll()
|
|
}
|
|
}
|