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