You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

515 lines
13 KiB

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