All files / src/web url-state.js

100% Statements 28/28
100% Branches 13/13
100% Functions 6/6
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126        37x     37x                   37x     37x                                                                                                                         238x 238x 238x   238x 238x 238x         96x         14x                 110x 110x 142x 37x 37x   105x 105x     110x 107x 89x       95x      
import { buildUrlFromParams, ParamTypes, parseQueryString } from "../url-params.js";
import { debounce } from "../debounce.js";
 
// A slider fires for every pixel of a drag, and each URL update is a history entry.
const UrlSettleMs = 300;
 
/** The parameters that have the page do something once it is up, rather than describe it. */
const StartupActionTypes = {
    autoboot: ParamTypes.BOOL,
    autochain: ParamTypes.BOOL,
    autorun: ParamTypes.BOOL,
    autotype: ParamTypes.STRING,
    loadBasic: ParamTypes.STRING,
    embedBasic: ParamTypes.STRING,
    patch: ParamTypes.STRING,
};
 
export const StartupActionParams = Object.keys(StartupActionTypes);
 
/** How each parameter the page understands is parsed and written back. */
export const UrlParamTypes = {
    ...StartupActionTypes,
 
    // Array parameters
    rom: ParamTypes.ARRAY,
 
    // Boolean parameters
    embed: ParamTypes.BOOL,
    fasttape: ParamTypes.BOOL,
    noseek: ParamTypes.BOOL,
    hasMusic5000: ParamTypes.BOOL,
    hasTeletextAdaptor: ParamTypes.BOOL,
    hasEconet: ParamTypes.BOOL,
    glEnabled: ParamTypes.BOOL,
    lowLatency: ParamTypes.BOOL,
    fakeVideo: ParamTypes.BOOL,
    logFdcCommands: ParamTypes.BOOL,
    logFdcStateChanges: ParamTypes.BOOL,
    coProcessor: ParamTypes.BOOL,
    mouseJoystickEnabled: ParamTypes.BOOL,
    speechOutput: ParamTypes.BOOL,
    audioDebug: ParamTypes.BOOL,
 
    // Numeric parameters
    stationId: ParamTypes.INT,
    frameSkip: ParamTypes.INT,
    videoCyclesBatch: ParamTypes.INT,
    audiofilterfreq: ParamTypes.FLOAT,
    audiofilterq: ParamTypes.FLOAT,
    speakerAmount: ParamTypes.FLOAT,
    palPersistenceMs: ParamTypes.FLOAT,
    rgbPersistenceMs: ParamTypes.FLOAT,
    audioLatencyMs: ParamTypes.FLOAT,
    cpuMultiplier: ParamTypes.FLOAT,
    tubeCpuMultiplier: ParamTypes.FLOAT,
    microphoneChannel: ParamTypes.INT,
 
    // String parameters (these are the default but listed for clarity)
    model: ParamTypes.STRING,
    disc: ParamTypes.STRING,
    disc1: ParamTypes.STRING,
    disc2: ParamTypes.STRING,
    tape: ParamTypes.STRING,
    mmc: ParamTypes.STRING,
    keyLayout: ParamTypes.STRING,
    displayMode: ParamTypes.STRING,
    audioOutput: ParamTypes.STRING,
    drive0Tracks: ParamTypes.STRING,
    drive1Tracks: ParamTypes.STRING,
    sbLeft: ParamTypes.STRING,
    sbRight: ParamTypes.STRING,
    sbBottom: ParamTypes.STRING,
};
 
/**
 * The page's settings as carried in its URL. `params` is the one parsed
 * object: whoever changes a setting hands the change to set(), which edits
 * it in place and writes the URL in one step.
 */
export class UrlState {
    constructor(location, history, paramTypes = UrlParamTypes) {
        this.history = history;
        this.paramTypes = paramTypes;
        this.baseUrl = location.origin + location.pathname;
        // Parameters may be given after the hash as well as in the query.
        const queryString = location.search.substring(1) + "&" + location.hash.substring(1);
        this.params = parseQueryString(queryString, paramTypes);
        this.updateUrlOnceSettled = debounce(() => this.updateUrl(), UrlSettleMs);
    }
 
    /** The page's URL with the parameters as they are now. */
    url() {
        return buildUrlFromParams(this.baseUrl, this.params, this.paramTypes);
    }
 
    /** The page's URL with some parameters changed, leaving `params` as it is. */
    urlWith(overrides) {
        return buildUrlFromParams(this.baseUrl, { ...this.params, ...overrides }, this.paramTypes);
    }
 
    /**
     * Apply `changes` to the parameters (undefined deletes a key) and push the
     * new URL; `settle: true` lets a burst of changes finish before pushing one
     * history entry for the lot.
     */
    set(changes, { settle = false } = {}) {
        let changed = false;
        for (const [key, value] of Object.entries(changes)) {
            if (value === undefined) {
                if (key in this.params) changed = true;
                delete this.params[key];
            } else {
                if (this.params[key] !== value) changed = true;
                this.params[key] = value;
            }
        }
        if (!changed) return;
        if (settle) this.updateUrlOnceSettled();
        else this.updateUrl();
    }
 
    updateUrl() {
        this.history.pushState(null, null, this.url());
    }
}