src/jquery.fancytree.persist.js
/*!
* jquery.fancytree.persist.js
*
* Persist tree status in cookiesRemove or highlight tree nodes, based on a filter.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* @depends: js-cookie or jquery-cookie
*
* Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
(function (factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery", "./jquery.fancytree"], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
require("./jquery.fancytree");
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function ($) {
"use strict";
/* global Cookies:false */
/*******************************************************************************
* Private functions and variables
*/
var cookieStore = null,
localStorageStore = null,
sessionStorageStore = null,
_assert = $.ui.fancytree.assert,
ACTIVE = "active",
EXPANDED = "expanded",
FOCUS = "focus",
SELECTED = "selected";
// Accessing window.xxxStorage may raise security exceptions (see #1022)
try {
_assert(window.localStorage && window.localStorage.getItem);
localStorageStore = {
get: function (key) {
return window.localStorage.getItem(key);
},
set: function (key, value) {
window.localStorage.setItem(key, value);
},
remove: function (key) {
window.localStorage.removeItem(key);
},
};
} catch (e) {
$.ui.fancytree.warn("Could not access window.localStorage", e);
}
try {
_assert(window.sessionStorage && window.sessionStorage.getItem);
sessionStorageStore = {
get: function (key) {
return window.sessionStorage.getItem(key);
},
set: function (key, value) {
window.sessionStorage.setItem(key, value);
},
remove: function (key) {
window.sessionStorage.removeItem(key);
},
};
} catch (e) {
$.ui.fancytree.warn("Could not access window.sessionStorage", e);
}
if (typeof Cookies === "function") {
// Assume https://github.com/js-cookie/js-cookie
cookieStore = {
get: Cookies.get,
set: function (key, value) {
Cookies.set(key, value, this.options.persist.cookie);
},
remove: Cookies.remove,
};
} else if ($ && typeof $.cookie === "function") {
// Fall back to https://github.com/carhartl/jquery-cookie
cookieStore = {
get: $.cookie,
set: function (key, value) {
$.cookie(key, value, this.options.persist.cookie);
},
remove: $.removeCookie,
};
}
/* Recursively load lazy nodes
* @param {string} mode 'load', 'expand', false
*/
function _loadLazyNodes(tree, local, keyList, mode, dfd) {
var i,
key,
l,
node,
foundOne = false,
expandOpts = tree.options.persist.expandOpts,
deferredList = [],
missingKeyList = [];
keyList = keyList || [];
dfd = dfd || $.Deferred();
for (i = 0, l = keyList.length; i < l; i++) {
key = keyList[i];
node = tree.getNodeByKey(key);
if (node) {
if (mode && node.isUndefined()) {
foundOne = true;
tree.debug(
"_loadLazyNodes: " + node + " is lazy: loading..."
);
if (mode === "expand") {
deferredList.push(node.setExpanded(true, expandOpts));
} else {
deferredList.push(node.load());
}
} else {
tree.debug("_loadLazyNodes: " + node + " already loaded.");
try {
node.setExpanded(true, expandOpts);
} catch (e) {
// #1157
tree.warn(
"ext-persist: setExpanded failed for " + node,
e
);
}
}
} else {
missingKeyList.push(key);
tree.debug("_loadLazyNodes: " + node + " was not yet found.");
}
}
$.when.apply($, deferredList).always(function () {
// All lazy-expands have finished
if (foundOne && missingKeyList.length > 0) {
// If we read new nodes from server, try to resolve yet-missing keys
_loadLazyNodes(tree, local, missingKeyList, mode, dfd);
} else {
if (missingKeyList.length) {
tree.warn(
"_loadLazyNodes: could not load those keys: ",
missingKeyList
);
for (i = 0, l = missingKeyList.length; i < l; i++) {
key = keyList[i];
local._appendKey(EXPANDED, keyList[i], false);
}
}
dfd.resolve();
}
});
return dfd;
}
/**
* [ext-persist] Remove persistence data of the given type(s).
* Called like
* $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected");
*
* @alias Fancytree#clearPersistData
* @requires jquery.fancytree.persist.js
*/
$.ui.fancytree._FancytreeClass.prototype.clearPersistData = function (
types
) {
var local = this.ext.persist,
prefix = local.cookiePrefix;
types = types || "active expanded focus selected";
if (types.indexOf(ACTIVE) >= 0) {
local._data(prefix + ACTIVE, null);
}
if (types.indexOf(EXPANDED) >= 0) {
local._data(prefix + EXPANDED, null);
}
if (types.indexOf(FOCUS) >= 0) {
local._data(prefix + FOCUS, null);
}
if (types.indexOf(SELECTED) >= 0) {
local._data(prefix + SELECTED, null);
}
};
$.ui.fancytree._FancytreeClass.prototype.clearCookies = function (types) {
this.warn(
"'tree.clearCookies()' is deprecated since v2.27.0: use 'clearPersistData()' instead."
);
return this.clearPersistData(types);
};
/**
* [ext-persist] Return persistence information from cookies
*
* Called like
* $.ui.fancytree.getTree("#tree").getPersistData();
*
* @alias Fancytree#getPersistData
* @requires jquery.fancytree.persist.js
*/
$.ui.fancytree._FancytreeClass.prototype.getPersistData = function () {
var local = this.ext.persist,
prefix = local.cookiePrefix,
delim = local.cookieDelimiter,
res = {};
res[ACTIVE] = local._data(prefix + ACTIVE);
res[EXPANDED] = (local._data(prefix + EXPANDED) || "").split(delim);
res[SELECTED] = (local._data(prefix + SELECTED) || "").split(delim);
res[FOCUS] = local._data(prefix + FOCUS);
return res;
};
/******************************************************************************
* Extension code
*/
$.ui.fancytree.registerExtension({
name: "persist",
version: "@VERSION",
// Default options for this extension.
options: {
cookieDelimiter: "~",
cookiePrefix: undefined, // 'fancytree-<treeId>-' by default
cookie: {
raw: false,
expires: "",
path: "",
domain: "",
secure: false,
},
expandLazy: false, // true: recursively expand and load lazy nodes
expandOpts: undefined, // optional `opts` argument passed to setExpanded()
fireActivate: true, // false: suppress `activate` event after active node was restored
overrideSource: true, // true: cookie takes precedence over `source` data attributes.
store: "auto", // 'cookie': force cookie, 'local': force localStore, 'session': force sessionStore
types: "active expanded focus selected",
},
/* Generic read/write string data to cookie, sessionStorage or localStorage. */
_data: function (key, value) {
var store = this._local.store;
if (value === undefined) {
return store.get.call(this, key);
} else if (value === null) {
store.remove.call(this, key);
} else {
store.set.call(this, key, value);
}
},
/* Append `key` to a cookie. */
_appendKey: function (type, key, flag) {
key = "" + key; // #90
var local = this._local,
instOpts = this.options.persist,
delim = instOpts.cookieDelimiter,
cookieName = local.cookiePrefix + type,
data = local._data(cookieName),
keyList = data ? data.split(delim) : [],
idx = $.inArray(key, keyList);
// Remove, even if we add a key, so the key is always the last entry
if (idx >= 0) {
keyList.splice(idx, 1);
}
// Append key to cookie
if (flag) {
keyList.push(key);
}
local._data(cookieName, keyList.join(delim));
},
treeInit: function (ctx) {
var tree = ctx.tree,
opts = ctx.options,
local = this._local,
instOpts = this.options.persist;
// // For 'auto' or 'cookie' mode, the cookie plugin must be available
// _assert((instOpts.store !== "auto" && instOpts.store !== "cookie") || cookieStore,
// "Missing required plugin for 'persist' extension: js.cookie.js or jquery.cookie.js");
local.cookiePrefix =
instOpts.cookiePrefix || "fancytree-" + tree._id + "-";
local.storeActive = instOpts.types.indexOf(ACTIVE) >= 0;
local.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0;
local.storeSelected = instOpts.types.indexOf(SELECTED) >= 0;
local.storeFocus = instOpts.types.indexOf(FOCUS) >= 0;
local.store = null;
if (instOpts.store === "auto") {
instOpts.store = localStorageStore ? "local" : "cookie";
}
if ($.isPlainObject(instOpts.store)) {
local.store = instOpts.store;
} else if (instOpts.store === "cookie") {
local.store = cookieStore;
} else if (instOpts.store === "local") {
local.store =
instOpts.store === "local"
? localStorageStore
: sessionStorageStore;
} else if (instOpts.store === "session") {
local.store =
instOpts.store === "local"
? localStorageStore
: sessionStorageStore;
}
_assert(local.store, "Need a valid store.");
// Bind init-handler to apply cookie state
tree.$div.on("fancytreeinit", function (event) {
if (
tree._triggerTreeEvent("beforeRestore", null, {}) === false
) {
return;
}
var cookie,
dfd,
i,
keyList,
node,
prevFocus = local._data(local.cookiePrefix + FOCUS), // record this before node.setActive() overrides it;
noEvents = instOpts.fireActivate === false;
// tree.debug("document.cookie:", document.cookie);
cookie = local._data(local.cookiePrefix + EXPANDED);
keyList = cookie && cookie.split(instOpts.cookieDelimiter);
if (local.storeExpanded) {
// Recursively load nested lazy nodes if expandLazy is 'expand' or 'load'
// Also remove expand-cookies for unmatched nodes
dfd = _loadLazyNodes(
tree,
local,
keyList,
instOpts.expandLazy ? "expand" : false,
null
);
} else {
// nothing to do
dfd = new $.Deferred().resolve();
}
dfd.done(function () {
if (local.storeSelected) {
cookie = local._data(local.cookiePrefix + SELECTED);
if (cookie) {
keyList = cookie.split(instOpts.cookieDelimiter);
for (i = 0; i < keyList.length; i++) {
node = tree.getNodeByKey(keyList[i]);
if (node) {
if (
node.selected === undefined ||
(instOpts.overrideSource &&
node.selected === false)
) {
// node.setSelected();
node.selected = true;
node.renderStatus();
}
} else {
// node is no longer member of the tree: remove from cookie also
local._appendKey(
SELECTED,
keyList[i],
false
);
}
}
}
// In selectMode 3 we have to fix the child nodes, since we
// only stored the selected *top* nodes
if (tree.options.selectMode === 3) {
tree.visit(function (n) {
if (n.selected) {
n.fixSelection3AfterClick();
return "skip";
}
});
}
}
if (local.storeActive) {
cookie = local._data(local.cookiePrefix + ACTIVE);
if (
cookie &&
(opts.persist.overrideSource || !tree.activeNode)
) {
node = tree.getNodeByKey(cookie);
if (node) {
node.debug("persist: set active", cookie);
// We only want to set the focus if the container
// had the keyboard focus before
node.setActive(true, {
noFocus: true,
noEvents: noEvents,
});
}
}
}
if (local.storeFocus && prevFocus) {
node = tree.getNodeByKey(prevFocus);
if (node) {
// node.debug("persist: set focus", cookie);
if (tree.options.titlesTabbable) {
$(node.span)
.find(".fancytree-title")
.trigger("focus");
} else {
$(tree.$container).trigger("focus");
}
// node.setFocus();
}
}
tree._triggerTreeEvent("restore", null, {});
});
});
// Init the tree
return this._superApply(arguments);
},
nodeSetActive: function (ctx, flag, callOpts) {
var res,
local = this._local;
flag = flag !== false;
res = this._superApply(arguments);
if (local.storeActive) {
local._data(
local.cookiePrefix + ACTIVE,
this.activeNode ? this.activeNode.key : null
);
}
return res;
},
nodeSetExpanded: function (ctx, flag, callOpts) {
var res,
node = ctx.node,
local = this._local;
flag = flag !== false;
res = this._superApply(arguments);
if (local.storeExpanded) {
local._appendKey(EXPANDED, node.key, flag);
}
return res;
},
nodeSetFocus: function (ctx, flag) {
var res,
local = this._local;
flag = flag !== false;
res = this._superApply(arguments);
if (local.storeFocus) {
local._data(
local.cookiePrefix + FOCUS,
this.focusNode ? this.focusNode.key : null
);
}
return res;
},
nodeSetSelected: function (ctx, flag, callOpts) {
var res,
selNodes,
tree = ctx.tree,
node = ctx.node,
local = this._local;
flag = flag !== false;
res = this._superApply(arguments);
if (local.storeSelected) {
if (tree.options.selectMode === 3) {
// In selectMode 3 we only store the the selected *top* nodes.
// De-selecting a node may also de-select some parents, so we
// calculate the current status again
selNodes = $.map(tree.getSelectedNodes(true), function (n) {
return n.key;
});
selNodes = selNodes.join(
ctx.options.persist.cookieDelimiter
);
local._data(local.cookiePrefix + SELECTED, selNodes);
} else {
// beforeSelect can prevent the change - flag doesn't reflect the node.selected state
local._appendKey(SELECTED, node.key, node.selected);
}
}
return res;
},
});
// Value returned by `require('jquery.fancytree..')`
return $.ui.fancytree;
}); // End of closure