2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* createSessionObs() creates an observable tied to window.sessionStorage, i.e. preserved for the
|
|
|
|
* lifetime of a browser tab for the current origin.
|
|
|
|
*/
|
|
|
|
import {safeJsonParse} from 'app/common/gutil';
|
|
|
|
import {IDisposableOwner, Observable} from 'grainjs';
|
2022-11-30 15:55:47 +00:00
|
|
|
import {getSessionStorage} from 'app/client/lib/storage';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-07-27 19:02:28 +00:00
|
|
|
export interface SessionObs<T> extends Observable<T> {
|
|
|
|
pauseSaving(yesNo: boolean): void;
|
|
|
|
}
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* Creates and returns an Observable tied to sessionStorage, to make its value stick across
|
|
|
|
* reloads and navigation, but differ across browser tabs. E.g. whether a side pane is open.
|
|
|
|
*
|
|
|
|
* The `key` isn't visible to the user, so pick any unique string name. You may include the
|
|
|
|
* docId into the key, to remember a separate value for each doc.
|
|
|
|
*
|
|
|
|
* To use it, you must specify a default, and a validation function: this module exposes a few
|
|
|
|
* helpful ones. Some examples:
|
|
|
|
*
|
|
|
|
* panelWidth = createSessionObs(owner, "panelWidth", 240, isNumber); // Has type Observable<number>
|
|
|
|
*
|
|
|
|
* import {StringUnion} from 'app/common/StringUnion';
|
|
|
|
* const SomeTab = StringUnion("foo", "bar", "baz");
|
|
|
|
* tab = createSessionObs(owner, "tab", "baz", SomeTab.guard); // Type Observable<"foo"|"bar"|"baz">
|
2022-07-27 19:02:28 +00:00
|
|
|
*
|
|
|
|
* You can disable saving to sessionStorage:
|
|
|
|
* panelWidth.pauseSaving(true);
|
|
|
|
* doStuff();
|
|
|
|
* panelWidth.pauseSaving(false);
|
|
|
|
*
|
2020-10-02 15:10:00 +00:00
|
|
|
*/
|
|
|
|
export function createSessionObs<T>(
|
|
|
|
owner: IDisposableOwner|null,
|
|
|
|
key: string,
|
|
|
|
_default: T,
|
|
|
|
isValid: (val: any) => val is T,
|
2022-07-27 19:02:28 +00:00
|
|
|
): SessionObs<T> {
|
2020-10-02 15:10:00 +00:00
|
|
|
function fromString(value: string|null): T {
|
|
|
|
const parsed = value == null ? null : safeJsonParse(value, null);
|
|
|
|
return isValid(parsed) ? parsed : _default;
|
|
|
|
}
|
|
|
|
function toString(value: T): string|null {
|
|
|
|
return value === _default || !isValid(value) ? null : JSON.stringify(value);
|
|
|
|
}
|
2022-07-27 19:02:28 +00:00
|
|
|
let _pauseSaving = false;
|
2022-11-30 15:55:47 +00:00
|
|
|
const storage = getSessionStorage();
|
|
|
|
const obs = Observable.create<T>(owner, fromString(storage.getItem(key)));
|
2020-10-02 15:10:00 +00:00
|
|
|
obs.addListener((value: T) => {
|
2022-07-27 19:02:28 +00:00
|
|
|
if (_pauseSaving) { return; }
|
2020-10-02 15:10:00 +00:00
|
|
|
const stored = toString(value);
|
|
|
|
if (stored == null) {
|
2022-11-30 15:55:47 +00:00
|
|
|
storage.removeItem(key);
|
2020-10-02 15:10:00 +00:00
|
|
|
} else {
|
2022-11-30 15:55:47 +00:00
|
|
|
storage.setItem(key, stored);
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
});
|
2022-07-27 19:02:28 +00:00
|
|
|
return Object.assign(obs, {pauseSaving(yesNo: boolean) { _pauseSaving = yesNo; }});
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Helper functions to check simple types, useful for the `isValid` argument to createSessionObs. */
|
|
|
|
export function isNumber(t: any): t is number { return typeof t === 'number'; }
|
|
|
|
export function isBoolean(t: any): t is boolean { return typeof t === 'boolean'; }
|
|
|
|
export function isString(t: any): t is string { return typeof t === 'string'; }
|