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