/* eslint-disable no-use-before-define */
import {
reset,
setStateByKey,
getStateByKey,
getStorage,
} from "./storageManager";
import PersistQuotaExceededError from "./PersistQuotaExceededError";
import {isNeeded, getUrl, getStorageKey, getNavigationType, isQuotaExceededError} from "./utils";
import {console, window} from "./browser";
import {TYPE_BACK_FORWARD, TYPE_NAVIGATE, CONST_PERSIST_STATE, CONST_DEPTHS, CONST_LAST_URL} from "./consts";
let currentUrl = "";
function execRec(obj, path, func) {
let _obj = obj;
if (!_obj) {
_obj = isNaN(path[0]) ? {} : [];
}
const head = path.shift();
if (path.length === 0) {
if (_obj instanceof Array && isNaN(head)) {
console.warn("Don't use key string on array");
}
func(_obj, head);
return _obj;
}
_obj[head] = execRec(_obj[head], path, func);
return _obj;
}
function setPersistState(key, value) {
try {
setStateByKey(CONST_PERSIST_STATE, key, value);
} catch (e) {
if (catchQuotaExceededError(e, CONST_PERSIST_STATE, value)) {
if (key === CONST_LAST_URL) {
setPersistState(key, value);
} else if (key === CONST_DEPTHS) {
setPersistState(key, value && value.slice(1));
}
}
}
}
function getPersistState(key) {
return getStateByKey(CONST_PERSIST_STATE, key);
}
function replaceDepth() {
const url = getUrl();
if (currentUrl === url) {
return;
}
const prevUrl = currentUrl;
try {
currentUrl = url;
const depths = getPersistState(CONST_DEPTHS) || [];
// remove prev url
const prevIndex = depths.indexOf(prevUrl);
if (prevIndex >= 0) {
depths.splice(prevIndex, 1);
reset(getStorageKey(prevUrl));
}
// remove next url info
const currentIndex = depths.indexOf(url);
if (currentIndex >= 0) {
depths.splice(currentIndex, 1);
reset(getStorageKey(url));
}
depths.push(url);
setPersistState(CONST_DEPTHS, depths);
setPersistState(CONST_LAST_URL, url);
} catch (e) {
// revert currentUrl
currentUrl = prevUrl;
throw e;
}
}
function updateDepth(type = 0) {
const url = getUrl();
if (currentUrl === url) {
return;
}
// url is not the same for the first time, pushState, or replaceState.
const prevUrl = currentUrl;
try {
currentUrl = url;
const depths = getPersistState(CONST_DEPTHS) || [];
if (type === TYPE_BACK_FORWARD) {
// Change current url only
const currentIndex = depths.indexOf(url);
~currentIndex && setPersistState(CONST_LAST_URL, url);
} else {
const prevLastUrl = getPersistState(CONST_LAST_URL);
reset(getStorageKey(url));
if (type === TYPE_NAVIGATE && url !== prevLastUrl) {
// Remove all url lists with higher index than current index
const prevLastIndex = depths.indexOf(prevLastUrl);
const removedList = depths.splice(prevLastIndex + 1, depths.length);
removedList.forEach(removedUrl => {
reset(getStorageKey(removedUrl));
});
// If the type is NAVIGATE and there is information about current url, delete it.
const currentIndex = depths.indexOf(url);
~currentIndex && depths.splice(currentIndex, 1);
}
// Add depth for new address.
if (depths.indexOf(url) < 0) {
depths.push(url);
}
setPersistState(CONST_DEPTHS, depths);
setPersistState(CONST_LAST_URL, url);
}
} catch (e) {
// revert currentUrl
currentUrl = prevUrl;
throw e;
}
}
function catchQuotaExceededError(e, key, value) {
if (clearFirst()) {
return true;
} else if (isQuotaExceededError(e)) {
throw new PersistQuotaExceededError(key, value ? JSON.stringify(value) : "");
} else {
throw e;
}
}
function clearFirst() {
const depths = getPersistState(CONST_DEPTHS) || [];
const removed = depths.splice(0, 1);
if (!removed.length) {
// There is an error because there is no depth to add data.
return false;
}
const removedUrl = removed[0];
reset(getStorageKey(removedUrl));
if (currentUrl === removedUrl) {
currentUrl = "";
setPersistState(CONST_LAST_URL, "");
if (!depths.length) {
// I tried to add myself, but it didn't add up, so I got an error.
return false;
}
}
setPersistState(CONST_DEPTHS, depths);
// Clear the previous record and try to add data again.
return true;
}
function clear() {
const depths = getPersistState(CONST_DEPTHS) || [];
depths.forEach(url => {
reset(getStorageKey(url));
});
reset(CONST_PERSIST_STATE);
currentUrl = "";
}
/**
* Get or store the current state of the web page using JSON.
* @ko 웹 페이지의 현재 상태를 JSON 형식으로 저장하거나 읽는다.
* @alias eg.Persist
*
* @support {"ie": "9+", "ch" : "latest", "ff" : "latest", "sf" : "latest" , "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"}
*/
class Persist {
static VERSION = "#__VERSION__#";
static StorageManager = {
reset,
setStateByKey,
getStateByKey,
getStorage,
};
/**
* @static
* Clear all information in Persist
*/
static clear() {
clear();
}
/**
* @static
* Return whether you need "Persist" module by checking the bfCache support of the current browser
* @return {Boolean}
*/
static isNeeded() {
return isNeeded;
}
/**
* Constructor
* @param {String} key The key of the state information to be stored <ko>저장할 상태 정보의 키</ko>
**/
constructor(key) {
this.key = key || "";
}
/**
* Read value
* @param {String?} path target path
* @return {String|Number|Boolean|Object|Array}
*/
get(path) {
// update url for pushState, replaceState
updateDepth(TYPE_NAVIGATE);
// find path
const urlKey = getStorageKey(getUrl());
const globalState = getStateByKey(urlKey, this.key);
if (!path || path.length === 0) {
return globalState;
}
const pathToken = path.split(".");
let currentItem = globalState;
let isTargetExist = true;
for (let i = 0; i < pathToken.length; i++) {
if (!currentItem) {
isTargetExist = false;
break;
}
currentItem = currentItem[pathToken[i]];
}
if (!isTargetExist || currentItem == null) {
return null;
}
return currentItem;
}
/**
* Save value
* @param {String} path target path
* @param {String|Number|Boolean|Object|Array} value value to save
* @return {Persist}
*/
set(path, value) {
// update url for pushState, replaceState
updateDepth(TYPE_NAVIGATE);
// find path
const key = this.key;
const urlKey = getStorageKey(getUrl());
const globalState = getStateByKey(urlKey, key);
try {
if (path.length === 0) {
setStateByKey(urlKey, key, value);
} else {
const allValue = execRec(globalState, path.split("."), (obj, head) => {
obj[head] = value;
});
setStateByKey(
urlKey,
key,
allValue
);
}
} catch (e) {
if (catchQuotaExceededError(e, urlKey, value)) {
this.set(path, value);
}
}
return this;
}
/**
* Remove value
* @param {String} path target path
* @return {Persist}
*/
remove(path) {
// update url for pushState, replaceState
updateDepth(TYPE_NAVIGATE);
// find path
const key = this.key;
const urlKey = getStorageKey(getUrl());
const globalState = getStateByKey(urlKey, key);
try {
if (path.length === 0) {
setStateByKey(urlKey, key, null);
} else {
const value = execRec(globalState, path.split("."), (obj, head) => {
if (typeof obj === "object") {
delete obj[head];
}
});
setStateByKey(
urlKey,
key,
value
);
}
} catch (e) {
if (catchQuotaExceededError(e)) {
this.remove(path);
}
}
return this;
}
}
if ("onpopstate" in window) {
window.addEventListener("popstate", () => {
// popstate event occurs when backward or forward
try {
updateDepth(TYPE_BACK_FORWARD);
} catch (e) {
// Global function calls prevent errors.
if (!isQuotaExceededError(e)) {
throw e;
}
}
});
}
// If navigation's type is not TYPE_BACK_FORWARD, delete information about current url.
try {
updateDepth(getNavigationType());
} catch (e) {
// Global function calls prevent errors.
if (!isQuotaExceededError(e)) {
throw e;
}
}
export {
updateDepth,
replaceDepth,
};
export default Persist;