2022-06-29 10:19:20 +00:00
|
|
|
import {safeJsonParse} from 'app/common/gutil';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {Observable} from 'grainjs';
|
|
|
|
|
2021-01-21 19:12:24 +00:00
|
|
|
/**
|
|
|
|
* Returns true if storage is functional. In some cass (e.g. when embedded), localStorage may
|
|
|
|
* throw errors. If so, we return false. This implementation is the approach taken by store.js.
|
|
|
|
*/
|
|
|
|
function testStorage(storage: Storage) {
|
|
|
|
try {
|
|
|
|
const testStr = '__localStorage_test';
|
|
|
|
storage.setItem(testStr, testStr);
|
|
|
|
const ok = (storage.getItem(testStr) === testStr);
|
|
|
|
storage.removeItem(testStr);
|
|
|
|
return ok;
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns localStorage if functional, or sessionStorage, or an in-memory storage. The fallbacks
|
|
|
|
* help with tests, and may help when Grist is embedded.
|
|
|
|
*/
|
|
|
|
export function getStorage(): Storage {
|
|
|
|
return _storage || (_storage = createStorage());
|
|
|
|
}
|
|
|
|
|
2022-04-21 17:57:33 +00:00
|
|
|
/**
|
|
|
|
* Similar to `getStorage`, but always returns sessionStorage (or an in-memory equivalent).
|
|
|
|
*/
|
|
|
|
export function getSessionStorage(): Storage {
|
|
|
|
return _sessionStorage || (_sessionStorage = createSessionStorage());
|
|
|
|
}
|
|
|
|
|
2021-01-21 19:12:24 +00:00
|
|
|
let _storage: Storage|undefined;
|
2022-04-21 17:57:33 +00:00
|
|
|
let _sessionStorage: Storage|undefined;
|
2021-01-21 19:12:24 +00:00
|
|
|
|
|
|
|
function createStorage(): Storage {
|
|
|
|
if (typeof localStorage !== 'undefined' && testStorage(localStorage)) {
|
|
|
|
return localStorage;
|
2022-04-21 17:57:33 +00:00
|
|
|
} else {
|
|
|
|
return createSessionStorage();
|
2021-01-21 19:12:24 +00:00
|
|
|
}
|
2022-04-21 17:57:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function createSessionStorage(): Storage {
|
2021-01-21 19:12:24 +00:00
|
|
|
if (typeof sessionStorage !== 'undefined' && testStorage(sessionStorage)) {
|
|
|
|
return sessionStorage;
|
2022-04-21 17:57:33 +00:00
|
|
|
} else {
|
|
|
|
// Fall back to a Map-based implementation of (non-persistent) sessionStorage.
|
|
|
|
return createInMemoryStorage();
|
2021-01-21 19:12:24 +00:00
|
|
|
}
|
2022-04-21 17:57:33 +00:00
|
|
|
}
|
2021-01-21 19:12:24 +00:00
|
|
|
|
2022-04-21 17:57:33 +00:00
|
|
|
function createInMemoryStorage(): Storage {
|
2021-01-21 19:12:24 +00:00
|
|
|
const values = new Map<string, string>();
|
|
|
|
return {
|
|
|
|
setItem(key: string, val: string) { values.set(key, val); },
|
|
|
|
getItem(key: string) { return values.get(key) ?? null; },
|
|
|
|
removeItem(key: string) { values.delete(key); },
|
|
|
|
clear() { values.clear(); },
|
|
|
|
get length() { return values.size; },
|
|
|
|
key(index: number): string|null { throw new Error('Not implemented'); },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-21 17:57:33 +00:00
|
|
|
function getStorageBoolObs(store: Storage, key: string, defValue: boolean) {
|
|
|
|
const storedNegation = defValue ? 'false' : 'true';
|
|
|
|
const obs = Observable.create(null, store.getItem(key) === storedNegation ? !defValue : defValue);
|
|
|
|
obs.addListener((val) => val === defValue ? store.removeItem(key) : store.setItem(key, storedNegation));
|
|
|
|
return obs;
|
|
|
|
}
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* Helper to create a boolean observable whose state is stored in localStorage.
|
2021-02-25 16:11:59 +00:00
|
|
|
*
|
|
|
|
* Optionally, a default value of true will make the observable start off as true. Note that the
|
|
|
|
* same default value should be used for an observable every time it's created.
|
2020-10-02 15:10:00 +00:00
|
|
|
*/
|
2021-02-25 16:11:59 +00:00
|
|
|
export function localStorageBoolObs(key: string, defValue = false): Observable<boolean> {
|
2022-04-21 17:57:33 +00:00
|
|
|
return getStorageBoolObs(getStorage(), key, defValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Similar to `localStorageBoolObs`, but always uses sessionStorage (or an in-memory equivalent).
|
|
|
|
*/
|
|
|
|
export function sessionStorageBoolObs(key: string, defValue = false): Observable<boolean> {
|
|
|
|
return getStorageBoolObs(getSessionStorage(), key, defValue);
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper to create a string observable whose state is stored in localStorage.
|
|
|
|
*/
|
2022-03-11 10:48:36 +00:00
|
|
|
export function localStorageObs(key: string, defaultValue?: string): Observable<string|null> {
|
2021-01-21 19:12:24 +00:00
|
|
|
const store = getStorage();
|
2022-03-11 10:48:36 +00:00
|
|
|
const obs = Observable.create<string|null>(null, store.getItem(key) ?? defaultValue ?? null);
|
2021-01-21 19:12:24 +00:00
|
|
|
obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val));
|
2020-10-02 15:10:00 +00:00
|
|
|
return obs;
|
|
|
|
}
|
2022-06-29 10:19:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper to create a JSON observable whose state is stored in localStorage.
|
|
|
|
*/
|
|
|
|
export function localStorageJsonObs<T>(key: string, defaultValue: T): Observable<T> {
|
|
|
|
const store = getStorage();
|
|
|
|
const currentValue = safeJsonParse(store.getItem(key) || '', defaultValue ?? null);
|
|
|
|
const obs = Observable.create<T>(null, currentValue);
|
|
|
|
obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, JSON.stringify(val ?? null)));
|
|
|
|
return obs;
|
|
|
|
}
|