router.js


import { app, isObject, isString, trace } from "./app"
import { $on, $ready } from "./dom"
import { emit, on } from "./events"
import { render } from "./render"

/**
  * Parses component path and returns an object with at least **{ name, params }** ready for rendering. External urls are ignored.
  *
  * Passing an object will retun a shallow copy of it with name and params properties possibly set if not provided.
  *
  * The path can be:
  * - component name
  * - relative path: name/param1/param2/param3/....
  * - absolute path: /app/name/param1/param2/...
  * - URL: https://host/app/name/param1/...
  *
  *  All parts from the path and query parameters will be placed in the **params** object.
  *
  * The **.html** extention will be stripped to support extrernal loading but other exts will be kept as is.
  *
  * @param {string|object} path
  * @returns {Object} in format { name, params }
  */
export function parsePath(path)
{
    var rc = { name: "", params: {} }, query, loc = window.location;

    if (isObject(path)) return Object.assign(rc, path);
    if (!isString(path)) return rc;

    // Custom parser b/c this is not always url
    var base = app.base;
    if (path.startsWith(loc.origin)) path = path.substr(loc.origin.length);
    if (path.includes("://")) path = path.replace(/^(.*:\/\/[^/]*)/, "");
    if (path.startsWith(base)) path = path.substr(base.length);
    if (path.startsWith("/")) path = path.substr(1);
    if (path == base.slice(1, -1)) path = "";

    const q = path.indexOf("?");
    if (q > 0) {
        query = path.substr(q + 1, 1024);
        rc.name = path = path.substr(0, q);
    }
    if (path.endsWith(".html")) {
        path = path.slice(0, -5);
    }
    if (path.includes("/")) {
        path = path.split("/").slice(0, 7);
        rc.name = path.shift();
        for (let i = 0; i < path.length; i++) {
            if (!path[i]) continue;
            rc.params[`param${i + 1}`] = path[i];
        }
    } else {
        rc.name = path || "";
    }
    if (query) {
        for (const [key, value] of new URLSearchParams(query).entries()) {
            rc.params[key] = value;
        }
    }
    return rc;
}

/**
 * Saves the given component in the history as ** /name/param1/param2/param3/.. **
 *
 * It is called on every **component:create** event for main components as a microtask,
 * meaning immediate callbacks have a chance to modify the behaviour.
 * @param {Object} options
 *
 */
export function savePath(options)
{
    if (isString(options)) options = { name: options };
    if (!options?.name) return;
    var path = [options.name];
    if (options?.params) {
        for (let i = 1; i < 7; i++) path.push(options.params[`param${i}`] || "");
    }
    while (!path.at(-1)) path.length--;
    path = path.join("/");
    trace("savePath:", path, options);
    if (!path) return;
    emit("path:push", window.location.origin + app.base + path);
    window.history.pushState(null, "", window.location.origin + app.base + path);
}

/**
 * Show a component by path, it is called on **path:restore** event by default from {@link start} and is used
 * to show first component on initial page load. If the path is not recognized or no component is
 * found then the default {@link index} component is shown.
 * @param {string} path
 */
export function restorePath(path)
{
    trace("restorePath:", path, app.index);
    render(path, app.index);
}

/**
 * Setup default handlers:
 * - on **path:restore** event call {@link restorePath} to render a component from the history
 * - on **path:save** call {@link savePath} to save the current component in the history
 * - on page ready call {@link restorePath} to render the initial page
 *
 * **If not called then no browser history will not be handled, up to the app to do it some other way.**. One good
 * reason is to create your own handlers to build different path and then save/restore.
 */
export function start()
{
    on("path:save", savePath);
    on("path:restore", restorePath);
    $ready(restorePath.bind(app, window.location.href));
}

$on(window, "popstate", () => emit("path:restore", window.location.href));