Source: 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.");
					node.setExpanded(true, expandOpts);
				}
			} 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").focus();
							} else {
								$(tree.$container).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

Fork me on GitHub