2022-04-21 18:06:11 +02:00
|
|
|
import { get } from "svelte/store"
|
2022-04-25 16:36:01 +02:00
|
|
|
import { isChangingPage } from "@roxi/routify"
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
export const syncURLToState = options => {
|
2022-04-28 13:05:34 +02:00
|
|
|
const {
|
|
|
|
urlParam,
|
|
|
|
stateKey,
|
|
|
|
validate,
|
2022-12-17 15:13:06 +01:00
|
|
|
update,
|
2022-04-28 13:05:34 +02:00
|
|
|
baseUrl = "..",
|
|
|
|
fallbackUrl,
|
|
|
|
store,
|
|
|
|
routify,
|
2022-10-17 10:23:52 +02:00
|
|
|
beforeNavigate,
|
2022-12-22 14:09:07 +01:00
|
|
|
decode,
|
2022-04-28 13:05:34 +02:00
|
|
|
} = options || {}
|
2022-04-21 18:06:11 +02:00
|
|
|
if (
|
2022-04-28 13:05:34 +02:00
|
|
|
!urlParam ||
|
|
|
|
!stateKey ||
|
|
|
|
!baseUrl ||
|
|
|
|
!urlParam ||
|
2022-04-21 18:06:11 +02:00
|
|
|
!store?.subscribe ||
|
2022-04-28 13:05:34 +02:00
|
|
|
!routify ||
|
|
|
|
!routify.params?.subscribe ||
|
|
|
|
!routify.goto?.subscribe ||
|
|
|
|
!routify.redirect?.subscribe ||
|
|
|
|
!routify.page?.subscribe
|
2022-04-21 18:06:11 +02:00
|
|
|
) {
|
2022-04-25 16:36:01 +02:00
|
|
|
console.warn("syncURLToState invoked with missing parameters")
|
2022-04-21 18:06:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-12-22 14:09:07 +01:00
|
|
|
// Decodes encoded URL params if required
|
|
|
|
const decodeParams = urlParams => {
|
|
|
|
if (!decode) {
|
|
|
|
return urlParams
|
|
|
|
}
|
|
|
|
let decoded = {}
|
|
|
|
Object.keys(urlParams || {}).forEach(key => {
|
|
|
|
decoded[key] = decode(urlParams[key])
|
|
|
|
})
|
|
|
|
return decoded
|
|
|
|
}
|
|
|
|
|
2022-04-25 16:36:01 +02:00
|
|
|
// We can't dynamically fetch the value of stateful routify stores so we need
|
|
|
|
// to just subscribe and cache the latest versions.
|
2022-04-21 18:06:11 +02:00
|
|
|
// We can grab their initial values as this is during component
|
|
|
|
// initialisation.
|
2022-12-22 14:09:07 +01:00
|
|
|
let cachedParams = decodeParams(get(routify.params))
|
2022-04-28 13:05:34 +02:00
|
|
|
let cachedGoto = get(routify.goto)
|
|
|
|
let cachedRedirect = get(routify.redirect)
|
|
|
|
let cachedPage = get(routify.page)
|
|
|
|
let previousParamsHash = null
|
2022-12-19 14:16:54 +01:00
|
|
|
let debug = false
|
2024-01-31 14:47:27 +01:00
|
|
|
const log = (...params) => debug && console.debug(`[${urlParam}]`, ...params)
|
2022-04-25 16:36:01 +02:00
|
|
|
|
|
|
|
// Navigate to a certain URL
|
2022-04-28 13:05:34 +02:00
|
|
|
const gotoUrl = (url, params) => {
|
2023-08-23 12:03:00 +02:00
|
|
|
// Clean URL
|
|
|
|
if (url?.endsWith("/index")) {
|
|
|
|
url = url.replace("/index", "")
|
|
|
|
}
|
|
|
|
// Allow custom URL handling
|
2022-10-17 10:23:52 +02:00
|
|
|
if (beforeNavigate) {
|
|
|
|
const res = beforeNavigate(url, params)
|
|
|
|
if (res?.url) {
|
|
|
|
url = res.url
|
|
|
|
}
|
|
|
|
if (res?.params) {
|
|
|
|
params = res.params
|
|
|
|
}
|
|
|
|
}
|
2022-04-28 13:05:34 +02:00
|
|
|
log("Navigating to", url, "with params", params)
|
|
|
|
cachedGoto(url, params)
|
2022-04-25 16:36:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Redirect to a certain URL
|
|
|
|
const redirectUrl = url => {
|
2022-04-25 20:33:43 +02:00
|
|
|
log("Redirecting to", url)
|
2022-04-25 16:36:01 +02:00
|
|
|
cachedRedirect(url)
|
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
// Updates state with new URL params
|
|
|
|
const mapUrlToState = params => {
|
2022-04-28 13:05:34 +02:00
|
|
|
// Check if we have new URL params
|
|
|
|
const paramsHash = JSON.stringify(params)
|
|
|
|
const newParams = paramsHash !== previousParamsHash
|
|
|
|
previousParamsHash = paramsHash
|
|
|
|
const urlValue = params?.[urlParam]
|
|
|
|
const stateValue = get(store)?.[stateKey]
|
|
|
|
if (!newParams || !urlValue) {
|
2022-04-21 18:06:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 13:05:34 +02:00
|
|
|
// Check if new value is valid
|
|
|
|
if (validate && fallbackUrl) {
|
|
|
|
if (!validate(urlValue)) {
|
2022-12-22 14:09:07 +01:00
|
|
|
log("Invalid URL param!", urlValue)
|
2022-04-28 13:05:34 +02:00
|
|
|
redirectUrl(fallbackUrl)
|
|
|
|
return
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
2022-04-28 13:05:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Only update state if we have a new value
|
|
|
|
if (urlValue !== stateValue) {
|
|
|
|
log(`state.${stateKey} (${stateValue}) <= url.${urlParam} (${urlValue})`)
|
2022-12-17 15:13:06 +01:00
|
|
|
if (update) {
|
|
|
|
// Use custom update function if provided
|
|
|
|
update(urlValue)
|
|
|
|
} else {
|
|
|
|
// Otherwise manually update the store
|
|
|
|
store.update(state => ({
|
|
|
|
...state,
|
|
|
|
[stateKey]: urlValue,
|
|
|
|
}))
|
|
|
|
}
|
2022-04-28 13:05:34 +02:00
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Updates the URL with new state values
|
|
|
|
const mapStateToUrl = state => {
|
2022-04-28 13:05:34 +02:00
|
|
|
const urlValue = cachedParams?.[urlParam]
|
|
|
|
const stateValue = state?.[stateKey]
|
2023-07-03 12:14:07 +02:00
|
|
|
|
|
|
|
// As the store updated, validate that the current state value is valid
|
|
|
|
if (validate && fallbackUrl) {
|
|
|
|
if (!validate(stateValue)) {
|
|
|
|
log("Invalid state param!", stateValue)
|
|
|
|
redirectUrl(fallbackUrl)
|
|
|
|
return
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Avoid updating the URL if not necessary to prevent a wasted render
|
|
|
|
// cycle
|
2023-07-03 12:14:07 +02:00
|
|
|
if (stateValue === urlValue) {
|
2022-04-21 18:06:11 +02:00
|
|
|
return
|
|
|
|
}
|
2023-07-03 12:14:07 +02:00
|
|
|
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
// Navigate to the new URL
|
2022-04-25 16:36:01 +02:00
|
|
|
if (!get(isChangingPage)) {
|
2022-04-28 13:05:34 +02:00
|
|
|
const newUrlParams = {
|
|
|
|
...cachedParams,
|
|
|
|
[urlParam]: stateValue,
|
|
|
|
}
|
|
|
|
gotoUrl(cachedPage.path, newUrlParams)
|
2022-04-25 16:36:01 +02:00
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Initially hydrate state from URL
|
|
|
|
mapUrlToState(cachedParams)
|
|
|
|
|
|
|
|
// Subscribe to URL changes and cache them
|
2022-04-28 13:05:34 +02:00
|
|
|
const unsubscribeParams = routify.params.subscribe($urlParams => {
|
2022-12-22 14:09:07 +01:00
|
|
|
$urlParams = decodeParams($urlParams)
|
2022-04-21 18:06:11 +02:00
|
|
|
cachedParams = $urlParams
|
|
|
|
mapUrlToState($urlParams)
|
|
|
|
})
|
|
|
|
|
2022-04-25 16:36:01 +02:00
|
|
|
// Subscribe to routify store changes and cache them
|
2022-04-28 13:05:34 +02:00
|
|
|
const unsubscribeGoto = routify.goto.subscribe($goto => {
|
2022-04-21 18:06:11 +02:00
|
|
|
cachedGoto = $goto
|
|
|
|
})
|
2022-04-28 13:05:34 +02:00
|
|
|
const unsubscribeRedirect = routify.redirect.subscribe($redirect => {
|
2022-04-25 16:36:01 +02:00
|
|
|
cachedRedirect = $redirect
|
|
|
|
})
|
2022-04-28 13:05:34 +02:00
|
|
|
const unsubscribePage = routify.page.subscribe($page => {
|
|
|
|
cachedPage = $page
|
|
|
|
})
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
// Subscribe to store changes and keep URL up to date
|
|
|
|
const unsubscribeStore = store.subscribe(mapStateToUrl)
|
|
|
|
|
|
|
|
// Return an unsync function to cancel subscriptions
|
|
|
|
return () => {
|
|
|
|
unsubscribeParams()
|
|
|
|
unsubscribeGoto()
|
2022-04-25 16:36:01 +02:00
|
|
|
unsubscribeRedirect()
|
2022-04-28 13:05:34 +02:00
|
|
|
unsubscribePage()
|
2022-04-21 18:06:11 +02:00
|
|
|
unsubscribeStore()
|
|
|
|
}
|
|
|
|
}
|