1/*!
2 * jquery.fancytree.persist.js
3 *
4 * Persist tree status in cookiesRemove or highlight tree nodes, based on a filter.
5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
6 *
7 * @depends: js-cookie or jquery-cookie
8 *
9 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
10 *
11 * Released under the MIT license
12 * https://github.com/mar10/fancytree/wiki/LicenseInfo
13 *
14 * @version 2.38.3
15 * @date 2023-02-01T20:52:50Z
16 */
17
18(function (factory) {
19	if (typeof define === "function" && define.amd) {
20		// AMD. Register as an anonymous module.
21		define(["jquery", "./jquery.fancytree"], factory);
22	} else if (typeof module === "object" && module.exports) {
23		// Node/CommonJS
24		require("./jquery.fancytree");
25		module.exports = factory(require("jquery"));
26	} else {
27		// Browser globals
28		factory(jQuery);
29	}
30})(function ($) {
31	"use strict";
32	/* global Cookies:false */
33
34	/*******************************************************************************
35	 * Private functions and variables
36	 */
37	var cookieStore = null,
38		localStorageStore = null,
39		sessionStorageStore = null,
40		_assert = $.ui.fancytree.assert,
41		ACTIVE = "active",
42		EXPANDED = "expanded",
43		FOCUS = "focus",
44		SELECTED = "selected";
45
46	// Accessing window.xxxStorage may raise security exceptions (see #1022)
47	try {
48		_assert(window.localStorage && window.localStorage.getItem);
49		localStorageStore = {
50			get: function (key) {
51				return window.localStorage.getItem(key);
52			},
53			set: function (key, value) {
54				window.localStorage.setItem(key, value);
55			},
56			remove: function (key) {
57				window.localStorage.removeItem(key);
58			},
59		};
60	} catch (e) {
61		$.ui.fancytree.warn("Could not access window.localStorage", e);
62	}
63
64	try {
65		_assert(window.sessionStorage && window.sessionStorage.getItem);
66		sessionStorageStore = {
67			get: function (key) {
68				return window.sessionStorage.getItem(key);
69			},
70			set: function (key, value) {
71				window.sessionStorage.setItem(key, value);
72			},
73			remove: function (key) {
74				window.sessionStorage.removeItem(key);
75			},
76		};
77	} catch (e) {
78		$.ui.fancytree.warn("Could not access window.sessionStorage", e);
79	}
80
81	if (typeof Cookies === "function") {
82		// Assume https://github.com/js-cookie/js-cookie
83		cookieStore = {
84			get: Cookies.get,
85			set: function (key, value) {
86				Cookies.set(key, value, this.options.persist.cookie);
87			},
88			remove: Cookies.remove,
89		};
90	} else if ($ && typeof $.cookie === "function") {
91		// Fall back to https://github.com/carhartl/jquery-cookie
92		cookieStore = {
93			get: $.cookie,
94			set: function (key, value) {
95				$.cookie(key, value, this.options.persist.cookie);
96			},
97			remove: $.removeCookie,
98		};
99	}
100
101	/* Recursively load lazy nodes
102	 * @param {string} mode 'load', 'expand', false
103	 */
104	function _loadLazyNodes(tree, local, keyList, mode, dfd) {
105		var i,
106			key,
107			l,
108			node,
109			foundOne = false,
110			expandOpts = tree.options.persist.expandOpts,
111			deferredList = [],
112			missingKeyList = [];
113
114		keyList = keyList || [];
115		dfd = dfd || $.Deferred();
116
117		for (i = 0, l = keyList.length; i < l; i++) {
118			key = keyList[i];
119			node = tree.getNodeByKey(key);
120			if (node) {
121				if (mode && node.isUndefined()) {
122					foundOne = true;
123					tree.debug(
124						"_loadLazyNodes: " + node + " is lazy: loading..."
125					);
126					if (mode === "expand") {
127						deferredList.push(node.setExpanded(true, expandOpts));
128					} else {
129						deferredList.push(node.load());
130					}
131				} else {
132					tree.debug("_loadLazyNodes: " + node + " already loaded.");
133					node.setExpanded(true, expandOpts);
134				}
135			} else {
136				missingKeyList.push(key);
137				tree.debug("_loadLazyNodes: " + node + " was not yet found.");
138			}
139		}
140
141		$.when.apply($, deferredList).always(function () {
142			// All lazy-expands have finished
143			if (foundOne && missingKeyList.length > 0) {
144				// If we read new nodes from server, try to resolve yet-missing keys
145				_loadLazyNodes(tree, local, missingKeyList, mode, dfd);
146			} else {
147				if (missingKeyList.length) {
148					tree.warn(
149						"_loadLazyNodes: could not load those keys: ",
150						missingKeyList
151					);
152					for (i = 0, l = missingKeyList.length; i < l; i++) {
153						key = keyList[i];
154						local._appendKey(EXPANDED, keyList[i], false);
155					}
156				}
157				dfd.resolve();
158			}
159		});
160		return dfd;
161	}
162
163	/**
164	 * [ext-persist] Remove persistence data of the given type(s).
165	 * Called like
166	 *     $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected");
167	 *
168	 * @alias Fancytree#clearPersistData
169	 * @requires jquery.fancytree.persist.js
170	 */
171	$.ui.fancytree._FancytreeClass.prototype.clearPersistData = function (
172		types
173	) {
174		var local = this.ext.persist,
175			prefix = local.cookiePrefix;
176
177		types = types || "active expanded focus selected";
178		if (types.indexOf(ACTIVE) >= 0) {
179			local._data(prefix + ACTIVE, null);
180		}
181		if (types.indexOf(EXPANDED) >= 0) {
182			local._data(prefix + EXPANDED, null);
183		}
184		if (types.indexOf(FOCUS) >= 0) {
185			local._data(prefix + FOCUS, null);
186		}
187		if (types.indexOf(SELECTED) >= 0) {
188			local._data(prefix + SELECTED, null);
189		}
190	};
191
192	$.ui.fancytree._FancytreeClass.prototype.clearCookies = function (types) {
193		this.warn(
194			"'tree.clearCookies()' is deprecated since v2.27.0: use 'clearPersistData()' instead."
195		);
196		return this.clearPersistData(types);
197	};
198
199	/**
200	 * [ext-persist] Return persistence information from cookies
201	 *
202	 * Called like
203	 *     $.ui.fancytree.getTree("#tree").getPersistData();
204	 *
205	 * @alias Fancytree#getPersistData
206	 * @requires jquery.fancytree.persist.js
207	 */
208	$.ui.fancytree._FancytreeClass.prototype.getPersistData = function () {
209		var local = this.ext.persist,
210			prefix = local.cookiePrefix,
211			delim = local.cookieDelimiter,
212			res = {};
213
214		res[ACTIVE] = local._data(prefix + ACTIVE);
215		res[EXPANDED] = (local._data(prefix + EXPANDED) || "").split(delim);
216		res[SELECTED] = (local._data(prefix + SELECTED) || "").split(delim);
217		res[FOCUS] = local._data(prefix + FOCUS);
218		return res;
219	};
220
221	/******************************************************************************
222	 * Extension code
223	 */
224	$.ui.fancytree.registerExtension({
225		name: "persist",
226		version: "2.38.3",
227		// Default options for this extension.
228		options: {
229			cookieDelimiter: "~",
230			cookiePrefix: undefined, // 'fancytree-<treeId>-' by default
231			cookie: {
232				raw: false,
233				expires: "",
234				path: "",
235				domain: "",
236				secure: false,
237			},
238			expandLazy: false, // true: recursively expand and load lazy nodes
239			expandOpts: undefined, // optional `opts` argument passed to setExpanded()
240			fireActivate: true, // false: suppress `activate` event after active node was restored
241			overrideSource: true, // true: cookie takes precedence over `source` data attributes.
242			store: "auto", // 'cookie': force cookie, 'local': force localStore, 'session': force sessionStore
243			types: "active expanded focus selected",
244		},
245
246		/* Generic read/write string data to cookie, sessionStorage or localStorage. */
247		_data: function (key, value) {
248			var store = this._local.store;
249
250			if (value === undefined) {
251				return store.get.call(this, key);
252			} else if (value === null) {
253				store.remove.call(this, key);
254			} else {
255				store.set.call(this, key, value);
256			}
257		},
258
259		/* Append `key` to a cookie. */
260		_appendKey: function (type, key, flag) {
261			key = "" + key; // #90
262			var local = this._local,
263				instOpts = this.options.persist,
264				delim = instOpts.cookieDelimiter,
265				cookieName = local.cookiePrefix + type,
266				data = local._data(cookieName),
267				keyList = data ? data.split(delim) : [],
268				idx = $.inArray(key, keyList);
269			// Remove, even if we add a key,  so the key is always the last entry
270			if (idx >= 0) {
271				keyList.splice(idx, 1);
272			}
273			// Append key to cookie
274			if (flag) {
275				keyList.push(key);
276			}
277			local._data(cookieName, keyList.join(delim));
278		},
279
280		treeInit: function (ctx) {
281			var tree = ctx.tree,
282				opts = ctx.options,
283				local = this._local,
284				instOpts = this.options.persist;
285
286			// // For 'auto' or 'cookie' mode, the cookie plugin must be available
287			// _assert((instOpts.store !== "auto" && instOpts.store !== "cookie") || cookieStore,
288			// 	"Missing required plugin for 'persist' extension: js.cookie.js or jquery.cookie.js");
289
290			local.cookiePrefix =
291				instOpts.cookiePrefix || "fancytree-" + tree._id + "-";
292			local.storeActive = instOpts.types.indexOf(ACTIVE) >= 0;
293			local.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0;
294			local.storeSelected = instOpts.types.indexOf(SELECTED) >= 0;
295			local.storeFocus = instOpts.types.indexOf(FOCUS) >= 0;
296			local.store = null;
297
298			if (instOpts.store === "auto") {
299				instOpts.store = localStorageStore ? "local" : "cookie";
300			}
301			if ($.isPlainObject(instOpts.store)) {
302				local.store = instOpts.store;
303			} else if (instOpts.store === "cookie") {
304				local.store = cookieStore;
305			} else if (instOpts.store === "local") {
306				local.store =
307					instOpts.store === "local"
308						? localStorageStore
309						: sessionStorageStore;
310			} else if (instOpts.store === "session") {
311				local.store =
312					instOpts.store === "local"
313						? localStorageStore
314						: sessionStorageStore;
315			}
316			_assert(local.store, "Need a valid store.");
317
318			// Bind init-handler to apply cookie state
319			tree.$div.on("fancytreeinit", function (event) {
320				if (
321					tree._triggerTreeEvent("beforeRestore", null, {}) === false
322				) {
323					return;
324				}
325
326				var cookie,
327					dfd,
328					i,
329					keyList,
330					node,
331					prevFocus = local._data(local.cookiePrefix + FOCUS), // record this before node.setActive() overrides it;
332					noEvents = instOpts.fireActivate === false;
333
334				// tree.debug("document.cookie:", document.cookie);
335
336				cookie = local._data(local.cookiePrefix + EXPANDED);
337				keyList = cookie && cookie.split(instOpts.cookieDelimiter);
338
339				if (local.storeExpanded) {
340					// Recursively load nested lazy nodes if expandLazy is 'expand' or 'load'
341					// Also remove expand-cookies for unmatched nodes
342					dfd = _loadLazyNodes(
343						tree,
344						local,
345						keyList,
346						instOpts.expandLazy ? "expand" : false,
347						null
348					);
349				} else {
350					// nothing to do
351					dfd = new $.Deferred().resolve();
352				}
353
354				dfd.done(function () {
355					if (local.storeSelected) {
356						cookie = local._data(local.cookiePrefix + SELECTED);
357						if (cookie) {
358							keyList = cookie.split(instOpts.cookieDelimiter);
359							for (i = 0; i < keyList.length; i++) {
360								node = tree.getNodeByKey(keyList[i]);
361								if (node) {
362									if (
363										node.selected === undefined ||
364										(instOpts.overrideSource &&
365											node.selected === false)
366									) {
367										//									node.setSelected();
368										node.selected = true;
369										node.renderStatus();
370									}
371								} else {
372									// node is no longer member of the tree: remove from cookie also
373									local._appendKey(
374										SELECTED,
375										keyList[i],
376										false
377									);
378								}
379							}
380						}
381						// In selectMode 3 we have to fix the child nodes, since we
382						// only stored the selected *top* nodes
383						if (tree.options.selectMode === 3) {
384							tree.visit(function (n) {
385								if (n.selected) {
386									n.fixSelection3AfterClick();
387									return "skip";
388								}
389							});
390						}
391					}
392					if (local.storeActive) {
393						cookie = local._data(local.cookiePrefix + ACTIVE);
394						if (
395							cookie &&
396							(opts.persist.overrideSource || !tree.activeNode)
397						) {
398							node = tree.getNodeByKey(cookie);
399							if (node) {
400								node.debug("persist: set active", cookie);
401								// We only want to set the focus if the container
402								// had the keyboard focus before
403								node.setActive(true, {
404									noFocus: true,
405									noEvents: noEvents,
406								});
407							}
408						}
409					}
410					if (local.storeFocus && prevFocus) {
411						node = tree.getNodeByKey(prevFocus);
412						if (node) {
413							// node.debug("persist: set focus", cookie);
414							if (tree.options.titlesTabbable) {
415								$(node.span).find(".fancytree-title").focus();
416							} else {
417								$(tree.$container).focus();
418							}
419							// node.setFocus();
420						}
421					}
422					tree._triggerTreeEvent("restore", null, {});
423				});
424			});
425			// Init the tree
426			return this._superApply(arguments);
427		},
428		nodeSetActive: function (ctx, flag, callOpts) {
429			var res,
430				local = this._local;
431
432			flag = flag !== false;
433			res = this._superApply(arguments);
434
435			if (local.storeActive) {
436				local._data(
437					local.cookiePrefix + ACTIVE,
438					this.activeNode ? this.activeNode.key : null
439				);
440			}
441			return res;
442		},
443		nodeSetExpanded: function (ctx, flag, callOpts) {
444			var res,
445				node = ctx.node,
446				local = this._local;
447
448			flag = flag !== false;
449			res = this._superApply(arguments);
450
451			if (local.storeExpanded) {
452				local._appendKey(EXPANDED, node.key, flag);
453			}
454			return res;
455		},
456		nodeSetFocus: function (ctx, flag) {
457			var res,
458				local = this._local;
459
460			flag = flag !== false;
461			res = this._superApply(arguments);
462
463			if (local.storeFocus) {
464				local._data(
465					local.cookiePrefix + FOCUS,
466					this.focusNode ? this.focusNode.key : null
467				);
468			}
469			return res;
470		},
471		nodeSetSelected: function (ctx, flag, callOpts) {
472			var res,
473				selNodes,
474				tree = ctx.tree,
475				node = ctx.node,
476				local = this._local;
477
478			flag = flag !== false;
479			res = this._superApply(arguments);
480
481			if (local.storeSelected) {
482				if (tree.options.selectMode === 3) {
483					// In selectMode 3 we only store the the selected *top* nodes.
484					// De-selecting a node may also de-select some parents, so we
485					// calculate the current status again
486					selNodes = $.map(tree.getSelectedNodes(true), function (n) {
487						return n.key;
488					});
489					selNodes = selNodes.join(
490						ctx.options.persist.cookieDelimiter
491					);
492					local._data(local.cookiePrefix + SELECTED, selNodes);
493				} else {
494					// beforeSelect can prevent the change - flag doesn't reflect the node.selected state
495					local._appendKey(SELECTED, node.key, node.selected);
496				}
497			}
498			return res;
499		},
500	});
501	// Value returned by `require('jquery.fancytree..')`
502	return $.ui.fancytree;
503}); // End of closure
504