1/*!
2 *
3 * jquery.fancytree.clones.js
4 * Support faster lookup of nodes by key and shared ref-ids.
5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
6 *
7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
8 *
9 * Released under the MIT license
10 * https://github.com/mar10/fancytree/wiki/LicenseInfo
11 *
12 * @version 2.38.3
13 * @date 2023-02-01T20:52:50Z
14 */
15
16(function (factory) {
17	if (typeof define === "function" && define.amd) {
18		// AMD. Register as an anonymous module.
19		define(["jquery", "./jquery.fancytree"], factory);
20	} else if (typeof module === "object" && module.exports) {
21		// Node/CommonJS
22		require("./jquery.fancytree");
23		module.exports = factory(require("jquery"));
24	} else {
25		// Browser globals
26		factory(jQuery);
27	}
28})(function ($) {
29	"use strict";
30
31	/*******************************************************************************
32	 * Private functions and variables
33	 */
34
35	var _assert = $.ui.fancytree.assert;
36
37	/* Return first occurrence of member from array. */
38	function _removeArrayMember(arr, elem) {
39		// TODO: use Array.indexOf for IE >= 9
40		var i;
41		for (i = arr.length - 1; i >= 0; i--) {
42			if (arr[i] === elem) {
43				arr.splice(i, 1);
44				return true;
45			}
46		}
47		return false;
48	}
49
50	/**
51	 * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
52	 *
53	 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
54	 * @see http://github.com/garycourt/murmurhash-js
55	 * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
56	 * @see http://sites.google.com/site/murmurhash/
57	 *
58	 * @param {string} key ASCII only
59	 * @param {boolean} [asString=false]
60	 * @param {number} seed Positive integer only
61	 * @return {number} 32-bit positive integer hash
62	 */
63	function hashMurmur3(key, asString, seed) {
64		/*eslint-disable no-bitwise */
65		var h1b,
66			k1,
67			remainder = key.length & 3,
68			bytes = key.length - remainder,
69			h1 = seed,
70			c1 = 0xcc9e2d51,
71			c2 = 0x1b873593,
72			i = 0;
73
74		while (i < bytes) {
75			k1 =
76				(key.charCodeAt(i) & 0xff) |
77				((key.charCodeAt(++i) & 0xff) << 8) |
78				((key.charCodeAt(++i) & 0xff) << 16) |
79				((key.charCodeAt(++i) & 0xff) << 24);
80			++i;
81
82			k1 =
83				((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
84				0xffffffff;
85			k1 = (k1 << 15) | (k1 >>> 17);
86			k1 =
87				((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
88				0xffffffff;
89
90			h1 ^= k1;
91			h1 = (h1 << 13) | (h1 >>> 19);
92			h1b =
93				((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) &
94				0xffffffff;
95			h1 =
96				(h1b & 0xffff) +
97				0x6b64 +
98				((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
99		}
100
101		k1 = 0;
102
103		switch (remainder) {
104			case 3:
105				k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
106			// fall through
107			case 2:
108				k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
109			// fall through
110			case 1:
111				k1 ^= key.charCodeAt(i) & 0xff;
112
113				k1 =
114					((k1 & 0xffff) * c1 +
115						((((k1 >>> 16) * c1) & 0xffff) << 16)) &
116					0xffffffff;
117				k1 = (k1 << 15) | (k1 >>> 17);
118				k1 =
119					((k1 & 0xffff) * c2 +
120						((((k1 >>> 16) * c2) & 0xffff) << 16)) &
121					0xffffffff;
122				h1 ^= k1;
123		}
124
125		h1 ^= key.length;
126
127		h1 ^= h1 >>> 16;
128		h1 =
129			((h1 & 0xffff) * 0x85ebca6b +
130				((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
131			0xffffffff;
132		h1 ^= h1 >>> 13;
133		h1 =
134			((h1 & 0xffff) * 0xc2b2ae35 +
135				((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
136			0xffffffff;
137		h1 ^= h1 >>> 16;
138
139		if (asString) {
140			// Convert to 8 digit hex string
141			return ("0000000" + (h1 >>> 0).toString(16)).substr(-8);
142		}
143		return h1 >>> 0;
144		/*eslint-enable no-bitwise */
145	}
146
147	/*
148	 * Return a unique key for node by calculating the hash of the parents refKey-list.
149	 */
150	function calcUniqueKey(node) {
151		var key,
152			h1,
153			path = $.map(node.getParentList(false, true), function (e) {
154				return e.refKey || e.key;
155			});
156
157		path = path.join("/");
158		// 32-bit has a high probability of collisions, so we pump up to 64-bit
159		// https://security.stackexchange.com/q/209882/207588
160
161		h1 = hashMurmur3(path, true);
162		key = "id_" + h1 + hashMurmur3(h1 + path, true);
163
164		return key;
165	}
166
167	/**
168	 * [ext-clones] Return a list of clone-nodes (i.e. same refKey) or null.
169	 * @param {boolean} [includeSelf=false]
170	 * @returns {FancytreeNode[] | null}
171	 *
172	 * @alias FancytreeNode#getCloneList
173	 * @requires jquery.fancytree.clones.js
174	 */
175	$.ui.fancytree._FancytreeNodeClass.prototype.getCloneList = function (
176		includeSelf
177	) {
178		var key,
179			tree = this.tree,
180			refList = tree.refMap[this.refKey] || null,
181			keyMap = tree.keyMap;
182
183		if (refList) {
184			key = this.key;
185			// Convert key list to node list
186			if (includeSelf) {
187				refList = $.map(refList, function (val) {
188					return keyMap[val];
189				});
190			} else {
191				refList = $.map(refList, function (val) {
192					return val === key ? null : keyMap[val];
193				});
194				if (refList.length < 1) {
195					refList = null;
196				}
197			}
198		}
199		return refList;
200	};
201
202	/**
203	 * [ext-clones] Return true if this node has at least another clone with same refKey.
204	 * @returns {boolean}
205	 *
206	 * @alias FancytreeNode#isClone
207	 * @requires jquery.fancytree.clones.js
208	 */
209	$.ui.fancytree._FancytreeNodeClass.prototype.isClone = function () {
210		var refKey = this.refKey || null,
211			refList = (refKey && this.tree.refMap[refKey]) || null;
212		return !!(refList && refList.length > 1);
213	};
214
215	/**
216	 * [ext-clones] Update key and/or refKey for an existing node.
217	 * @param {string} key
218	 * @param {string} refKey
219	 * @returns {boolean}
220	 *
221	 * @alias FancytreeNode#reRegister
222	 * @requires jquery.fancytree.clones.js
223	 */
224	$.ui.fancytree._FancytreeNodeClass.prototype.reRegister = function (
225		key,
226		refKey
227	) {
228		key = key == null ? null : "" + key;
229		refKey = refKey == null ? null : "" + refKey;
230		// this.debug("reRegister", key, refKey);
231
232		var tree = this.tree,
233			prevKey = this.key,
234			prevRefKey = this.refKey,
235			keyMap = tree.keyMap,
236			refMap = tree.refMap,
237			refList = refMap[prevRefKey] || null,
238			//		curCloneKeys = refList ? node.getCloneList(true),
239			modified = false;
240
241		// Key has changed: update all references
242		if (key != null && key !== this.key) {
243			if (keyMap[key]) {
244				$.error(
245					"[ext-clones] reRegister(" +
246						key +
247						"): already exists: " +
248						this
249				);
250			}
251			// Update keyMap
252			delete keyMap[prevKey];
253			keyMap[key] = this;
254			// Update refMap
255			if (refList) {
256				refMap[prevRefKey] = $.map(refList, function (e) {
257					return e === prevKey ? key : e;
258				});
259			}
260			this.key = key;
261			modified = true;
262		}
263
264		// refKey has changed
265		if (refKey != null && refKey !== this.refKey) {
266			// Remove previous refKeys
267			if (refList) {
268				if (refList.length === 1) {
269					delete refMap[prevRefKey];
270				} else {
271					refMap[prevRefKey] = $.map(refList, function (e) {
272						return e === prevKey ? null : e;
273					});
274				}
275			}
276			// Add refKey
277			if (refMap[refKey]) {
278				refMap[refKey].append(key);
279			} else {
280				refMap[refKey] = [this.key];
281			}
282			this.refKey = refKey;
283			modified = true;
284		}
285		return modified;
286	};
287
288	/**
289	 * [ext-clones] Define a refKey for an existing node.
290	 * @param {string} refKey
291	 * @returns {boolean}
292	 *
293	 * @alias FancytreeNode#setRefKey
294	 * @requires jquery.fancytree.clones.js
295	 * @since 2.16
296	 */
297	$.ui.fancytree._FancytreeNodeClass.prototype.setRefKey = function (refKey) {
298		return this.reRegister(null, refKey);
299	};
300
301	/**
302	 * [ext-clones] Return all nodes with a given refKey (null if not found).
303	 * @param {string} refKey
304	 * @param {FancytreeNode} [rootNode] optionally restrict results to descendants of this node
305	 * @returns {FancytreeNode[] | null}
306	 * @alias Fancytree#getNodesByRef
307	 * @requires jquery.fancytree.clones.js
308	 */
309	$.ui.fancytree._FancytreeClass.prototype.getNodesByRef = function (
310		refKey,
311		rootNode
312	) {
313		var keyMap = this.keyMap,
314			refList = this.refMap[refKey] || null;
315
316		if (refList) {
317			// Convert key list to node list
318			if (rootNode) {
319				refList = $.map(refList, function (val) {
320					var node = keyMap[val];
321					return node.isDescendantOf(rootNode) ? node : null;
322				});
323			} else {
324				refList = $.map(refList, function (val) {
325					return keyMap[val];
326				});
327			}
328			if (refList.length < 1) {
329				refList = null;
330			}
331		}
332		return refList;
333	};
334
335	/**
336	 * [ext-clones] Replace a refKey with a new one.
337	 * @param {string} oldRefKey
338	 * @param {string} newRefKey
339	 * @alias Fancytree#changeRefKey
340	 * @requires jquery.fancytree.clones.js
341	 */
342	$.ui.fancytree._FancytreeClass.prototype.changeRefKey = function (
343		oldRefKey,
344		newRefKey
345	) {
346		var i,
347			node,
348			keyMap = this.keyMap,
349			refList = this.refMap[oldRefKey] || null;
350
351		if (refList) {
352			for (i = 0; i < refList.length; i++) {
353				node = keyMap[refList[i]];
354				node.refKey = newRefKey;
355			}
356			delete this.refMap[oldRefKey];
357			this.refMap[newRefKey] = refList;
358		}
359	};
360
361	/*******************************************************************************
362	 * Extension code
363	 */
364	$.ui.fancytree.registerExtension({
365		name: "clones",
366		version: "2.38.3",
367		// Default options for this extension.
368		options: {
369			highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
370			highlightClones: false, // set 'fancytree-clone' class on any node that has at least one clone
371		},
372
373		treeCreate: function (ctx) {
374			this._superApply(arguments);
375			ctx.tree.refMap = {};
376			ctx.tree.keyMap = {};
377		},
378		treeInit: function (ctx) {
379			this.$container.addClass("fancytree-ext-clones");
380			_assert(ctx.options.defaultKey == null);
381			// Generate unique / reproducible default keys
382			ctx.options.defaultKey = function (node) {
383				return calcUniqueKey(node);
384			};
385			// The default implementation loads initial data
386			this._superApply(arguments);
387		},
388		treeClear: function (ctx) {
389			ctx.tree.refMap = {};
390			ctx.tree.keyMap = {};
391			return this._superApply(arguments);
392		},
393		treeRegisterNode: function (ctx, add, node) {
394			var refList,
395				len,
396				tree = ctx.tree,
397				keyMap = tree.keyMap,
398				refMap = tree.refMap,
399				key = node.key,
400				refKey = node && node.refKey != null ? "" + node.refKey : null;
401
402			//		ctx.tree.debug("clones.treeRegisterNode", add, node);
403
404			if (node.isStatusNode()) {
405				return this._super(ctx, add, node);
406			}
407
408			if (add) {
409				if (keyMap[node.key] != null) {
410					var other = keyMap[node.key],
411						msg =
412							"clones.treeRegisterNode: duplicate key '" +
413							node.key +
414							"': /" +
415							node.getPath(true) +
416							" => " +
417							other.getPath(true);
418					// Sometimes this exception is not visible in the console,
419					// so we also write it:
420					tree.error(msg);
421					$.error(msg);
422				}
423				keyMap[key] = node;
424
425				if (refKey) {
426					refList = refMap[refKey];
427					if (refList) {
428						refList.push(key);
429						if (
430							refList.length === 2 &&
431							ctx.options.clones.highlightClones
432						) {
433							// Mark peer node, if it just became a clone (no need to
434							// mark current node, since it will be rendered later anyway)
435							keyMap[refList[0]].renderStatus();
436						}
437					} else {
438						refMap[refKey] = [key];
439					}
440					// node.debug("clones.treeRegisterNode: add clone =>", refMap[refKey]);
441				}
442			} else {
443				if (keyMap[key] == null) {
444					$.error(
445						"clones.treeRegisterNode: node.key not registered: " +
446							node.key
447					);
448				}
449				delete keyMap[key];
450				if (refKey) {
451					refList = refMap[refKey];
452					// node.debug("clones.treeRegisterNode: remove clone BEFORE =>", refMap[refKey]);
453					if (refList) {
454						len = refList.length;
455						if (len <= 1) {
456							_assert(len === 1);
457							_assert(refList[0] === key);
458							delete refMap[refKey];
459						} else {
460							_removeArrayMember(refList, key);
461							// Unmark peer node, if this was the only clone
462							if (
463								len === 2 &&
464								ctx.options.clones.highlightClones
465							) {
466								//							node.debug("clones.treeRegisterNode: last =>", node.getCloneList());
467								keyMap[refList[0]].renderStatus();
468							}
469						}
470						// node.debug("clones.treeRegisterNode: remove clone =>", refMap[refKey]);
471					}
472				}
473			}
474			return this._super(ctx, add, node);
475		},
476		nodeRenderStatus: function (ctx) {
477			var $span,
478				res,
479				node = ctx.node;
480
481			res = this._super(ctx);
482
483			if (ctx.options.clones.highlightClones) {
484				$span = $(node[ctx.tree.statusClassPropName]);
485				// Only if span already exists
486				if ($span.length && node.isClone()) {
487					//				node.debug("clones.nodeRenderStatus: ", ctx.options.clones.highlightClones);
488					$span.addClass("fancytree-clone");
489				}
490			}
491			return res;
492		},
493		nodeSetActive: function (ctx, flag, callOpts) {
494			var res,
495				scpn = ctx.tree.statusClassPropName,
496				node = ctx.node;
497
498			res = this._superApply(arguments);
499
500			if (ctx.options.clones.highlightActiveClones && node.isClone()) {
501				$.each(node.getCloneList(true), function (idx, n) {
502					// n.debug("clones.nodeSetActive: ", flag !== false);
503					$(n[scpn]).toggleClass(
504						"fancytree-active-clone",
505						flag !== false
506					);
507				});
508			}
509			return res;
510		},
511	});
512	// Value returned by `require('jquery.fancytree..')`
513	return $.ui.fancytree;
514}); // End of closure
515