local Cryo = require(script.Parent.Parent.Cryo) --[[ StateUtils provides utilities to read and write standard route data. Routes have the following general structure: { index = , routes = [ { routeName = , key = , params = , action = , }, ... ] } This structure is independent of the notion of stack, tab, drawer, or any other kind of navigation. It simply represents a list of pages and their parameters. Different kinds of routers can treat the data in their own way. ]] local StateUtils = {} StateUtils.__index = StateUtils -- Get the route matching the given key. Returns nil if no match is found. function StateUtils.get(state, key) assert(type(state) == "table", "state must be a table") assert(type(key) == "string", "key must be a string") for _, route in ipairs(state.routes) do if route.key == key then return route end end return nil end -- Get the route at the given index. Returns nil if no match is found. function StateUtils.getAtIndex(state, index) assert(type(state) == "table", "state must be a table") assert(type(index) == "number", "index must be a number") assert(index >= 0, "index must be non-negative") return state.routes[index] end -- Get the active route from state. Returns nil if no routes. function StateUtils.getActiveRoute(state) assert(type(state) == "table", "state must be a table") local index = state.index if index <= 0 then return nil end return state.routes[index] end -- Get the index of the route matching the given key. Returns nil if no match is found. function StateUtils.indexOf(state, key) assert(type(state) == "table", "state must be a table") assert(type(key) == "string", "key must be a string") for index, route in ipairs(state.routes) do if route.key == key then return index end end return nil end -- Returns true if a route exists matching the given key, false otherwise. function StateUtils.has(state, key) assert(type(state) == "table", "state must be a table") assert(type(key) == "string", "key must be a string") for _, route in ipairs(state.routes) do if route.key == key then return true end end return false end -- Push a new route into the navigation state. Makes the pushed route active. function StateUtils.push(state, route) assert(type(state) == "table", "state must be a table") assert(type(route) == "table", "route must be a table") assert(StateUtils.indexOf(state, route.key) == nil, string.format("route with key '%s' already exists", route.key)) local routes = Cryo.List.join(state.routes, { route }) return Cryo.Dictionary.join(state, { index = #routes, routes = routes, }) end -- Pop the top-most route from the navigation state (NOT the active route). -- Makes the new top-most route active. function StateUtils.pop(state) assert(type(state) == "table", "state must be a table") if #state.routes == 0 then -- NOTE: Popping empty state is a no-op return state end local routes = Cryo.List.removeIndex(state.routes, #state.routes) return Cryo.Dictionary.join(state, { index = #routes, routes = routes, }) end -- Sets the active route to match the given index. function StateUtils.jumpToIndex(state, index) assert(type(state) == "table", "state must be a table") assert(type(index) == "number", "index must be a number") if index == state.index then return state end assert(state.routes[index] ~= nil, string.format("cannot jump to out-of-range index '%d'", index)) return Cryo.Dictionary.join(state, { index = index, }) end -- Sets the active route to match the given key. function StateUtils.jumpTo(state, key) assert(type(state) == "table", "state must be a table") assert(type(key) == "string", "key must be a string") local index = StateUtils.indexOf(state, key) return StateUtils.jumpToIndex(state, index) end -- Sets the active route to the previous route in the list. function StateUtils.back(state) assert(type(state) == "table", "state must be a table") local index = state.index - 1 if not state.routes[index] then return state end return StateUtils.jumpToIndex(state, index) end -- Sets the active route to the next route in the list. function StateUtils.forward(state) assert(type(state) == "table", "state must be a table") local index = state.index + 1 if not state.routes[index] then return state end return StateUtils.jumpToIndex(state, index) end -- Replace the route matching the given key. Sets the active route to the -- newly replaced entry. Prunes the old entries that follow the replaced one. function StateUtils.replaceAndPrune(state, key, route) assert(type(state) == "table", "state must be a table") assert(type(key) == "string", "key must be a string") assert(type(route) == "table", "route must be a table") local index = StateUtils.indexOf(state, key) local replaced = StateUtils.replaceAtIndex(state, index, route) return Cryo.Dictionary.join(replaced, { routes = { unpack(replaced.routes, 1, index) } }) end -- Replace the route matching the given key without pruning the following routes. -- The active route will be updated to match the newly replaced one unless -- preserveIndex is true. function StateUtils.replaceAt(state, key, route, preserveIndex) assert(type(state) == "table", "state must be a table") assert(type(key) == "string", "key must be a string") assert(type(route) == "table", "route must be a table") assert(preserveIndex == nil or type(preserveIndex) == "boolean", "preserveIndex must be nil or a boolean") local index = StateUtils.indexOf(state, key) local nextIndex = preserveIndex and state.index or index local nextState = StateUtils.replaceAtIndex(state, index, route) nextState.index = nextIndex return nextState end -- Replace the route at the given index. Updates the active route to point to -- the replaced entry. function StateUtils.replaceAtIndex(state, index, route) assert(type(state) == "table", "state must be a table") assert(type(index) == "number", "index must be a number") assert(type(route) == "table", "route must be a table") assert(state.routes[index] ~= nil, string.format("index '%d' does not exist in route '%s'", index, route.key)) if state.routes[index] == route and index == state.index then return state end local routes = Cryo.List.join(state.routes) routes[index] = route return Cryo.Dictionary.join(state, { index = index, routes = routes, }) end -- Wipe away the existing routes and replace them with new routes. -- Sets the active route to the provided index (if provided), otherwise -- sets the active route to the last one in the list. function StateUtils.reset(state, routes, index) assert(type(state) == "table", "state must be a table") assert(type(routes) == "table" and #routes > 0, "routes must be a list with at least one element") assert(index == nil or type(index) == "number", "index must be a number or nil") local nextIndex = not index and #routes or index -- Bail out without replacing IFF index and routes all match if #state.routes == #routes and state.index == nextIndex then local routesAreEqual = true for i = 1, #routes, 1 do if state.routes[i] ~= routes[i] then routesAreEqual = false break end end if routesAreEqual then return state end end assert(routes[nextIndex] ~= nil, string.format("cannot reset index '%d' that does not exist", nextIndex)) return Cryo.Dictionary.join(state, { index = nextIndex, routes = routes, }) end return StateUtils