jqtree 1.8.8 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,10 +6,9 @@ import { JQTreeOptions } from "./jqtreeOptions";
6
6
  import KeyHandler from "./keyHandler";
7
7
  import MouseHandler from "./mouseHandler";
8
8
  import { PositionInfo } from "./mouseUtils";
9
- import { Node } from "./node";
9
+ import { Node, Position } from "./node";
10
10
  import NodeElement from "./nodeElement";
11
11
  import FolderElement from "./nodeElement/folderElement";
12
- import { getPosition } from "./position";
13
12
  import SaveStateHandler, { SavedState } from "./saveStateHandler";
14
13
  import ScrollHandler from "./scrollHandler";
15
14
  import SelectNodeHandler from "./selectNodeHandler";
@@ -78,1215 +77,1215 @@ export class JqTreeWidget extends SimpleWidget<JQTreeOptions> {
78
77
  private selectNodeHandler: SelectNodeHandler;
79
78
  private tree: Node;
80
79
 
81
- private connectHandlers() {
82
- const {
83
- autoEscape,
84
- buttonLeft,
85
- closedIcon,
86
- dataFilter,
87
- dragAndDrop,
88
- keyboardSupport,
89
- onCanMove,
90
- onCanMoveTo,
91
- onCreateLi,
92
- onDragMove,
93
- onDragStop,
94
- onGetStateFromStorage,
95
- onIsMoveHandle,
96
- onLoadFailed,
97
- onLoading,
98
- onSetStateFromStorage,
99
- openedIcon,
100
- openFolderDelay,
101
- rtl,
102
- saveState,
103
- showEmptyFolder,
104
- slide,
105
- tabIndex,
106
- } = this.options;
80
+ public addNodeAfter(
81
+ newNodeInfo: NodeData,
82
+ existingNode: Node,
83
+ ): Node | null {
84
+ const newNode = existingNode.addAfter(newNodeInfo);
107
85
 
108
- const closeNode = this.closeNode.bind(this);
109
- const getNodeElement = this.getNodeElement.bind(this);
110
- const getNodeElementForNode = this.getNodeElementForNode.bind(this);
111
- const getNodeById = this.getNodeById.bind(this);
112
- const getSelectedNode = this.getSelectedNode.bind(this);
113
- const getTree = this.getTree.bind(this);
114
- const isFocusOnTree = this.isFocusOnTree.bind(this);
115
- const loadData = this.loadData.bind(this);
116
- const openNode = this.openNodeInternal.bind(this);
117
- const refreshElements = this.refreshElements.bind(this);
118
- const refreshHitAreas = this.refreshHitAreas.bind(this);
119
- const selectNode = this.selectNode.bind(this);
120
- const $treeElement = this.element;
121
- const treeElement = this.element.get(0) as HTMLElement;
122
- const triggerEvent = this.triggerEvent.bind(this);
86
+ if (newNode) {
87
+ this.refreshElements(existingNode.parent);
88
+ }
123
89
 
124
- const selectNodeHandler = new SelectNodeHandler({
125
- getNodeById,
126
- });
90
+ return newNode;
91
+ }
127
92
 
128
- const addToSelection =
129
- selectNodeHandler.addToSelection.bind(selectNodeHandler);
130
- const getSelectedNodes =
131
- selectNodeHandler.getSelectedNodes.bind(selectNodeHandler);
132
- const isNodeSelected =
133
- selectNodeHandler.isNodeSelected.bind(selectNodeHandler);
134
- const removeFromSelection =
135
- selectNodeHandler.removeFromSelection.bind(selectNodeHandler);
136
- const getMouseDelay = () => this.options.startDndDelay ?? 0;
93
+ public addNodeBefore(
94
+ newNodeInfo: NodeData,
95
+ existingNode?: Node,
96
+ ): Node | null {
97
+ if (!existingNode) {
98
+ throw Error(PARAM_IS_EMPTY + "existingNode");
99
+ }
137
100
 
138
- const dataLoader = new DataLoader({
139
- dataFilter,
140
- loadData,
141
- onLoadFailed,
142
- onLoading,
143
- treeElement,
144
- triggerEvent,
145
- });
101
+ const newNode = existingNode.addBefore(newNodeInfo);
146
102
 
147
- const saveStateHandler = new SaveStateHandler({
148
- addToSelection,
149
- getNodeById,
150
- getSelectedNodes,
151
- getTree,
152
- onGetStateFromStorage,
153
- onSetStateFromStorage,
154
- openNode,
155
- refreshElements,
156
- removeFromSelection,
157
- saveState,
158
- });
103
+ if (newNode) {
104
+ this.refreshElements(existingNode.parent);
105
+ }
159
106
 
160
- const scrollHandler = new ScrollHandler({
161
- refreshHitAreas,
162
- treeElement,
163
- });
107
+ return newNode;
108
+ }
164
109
 
165
- const getScrollLeft = scrollHandler.getScrollLeft.bind(scrollHandler);
110
+ public addParentNode(
111
+ newNodeInfo: NodeData,
112
+ existingNode?: Node,
113
+ ): Node | null {
114
+ if (!existingNode) {
115
+ throw Error(PARAM_IS_EMPTY + "existingNode");
116
+ }
166
117
 
167
- const dndHandler = new DragAndDropHandler({
168
- autoEscape,
169
- getNodeElement,
170
- getNodeElementForNode,
171
- getScrollLeft,
172
- getTree,
173
- onCanMove,
174
- onCanMoveTo,
175
- onDragMove,
176
- onDragStop,
177
- onIsMoveHandle,
178
- openFolderDelay,
179
- openNode,
180
- refreshElements,
181
- slide,
182
- treeElement,
183
- triggerEvent,
184
- });
118
+ const newNode = existingNode.addParent(newNodeInfo);
185
119
 
186
- const keyHandler = new KeyHandler({
187
- closeNode,
188
- getSelectedNode,
189
- isFocusOnTree,
190
- keyboardSupport,
191
- openNode,
192
- selectNode,
193
- });
120
+ if (newNode) {
121
+ this.refreshElements(newNode.parent);
122
+ }
194
123
 
195
- const renderer = new ElementsRenderer({
196
- $element: $treeElement,
197
- autoEscape,
198
- buttonLeft,
199
- closedIcon,
200
- dragAndDrop,
201
- getTree,
202
- isNodeSelected,
203
- onCreateLi,
204
- openedIcon,
205
- rtl,
206
- showEmptyFolder,
207
- tabIndex,
208
- });
124
+ return newNode;
125
+ }
209
126
 
210
- const getNode = this.getNode.bind(this);
211
- const onMouseCapture = this.mouseCapture.bind(this);
212
- const onMouseDrag = this.mouseDrag.bind(this);
213
- const onMouseStart = this.mouseStart.bind(this);
214
- const onMouseStop = this.mouseStop.bind(this);
127
+ public addToSelection(node?: Node, mustSetFocus?: boolean): JQuery {
128
+ if (!node) {
129
+ throw Error(NODE_PARAM_IS_EMPTY);
130
+ }
215
131
 
216
- const mouseHandler = new MouseHandler({
217
- element: treeElement,
218
- getMouseDelay,
219
- getNode,
220
- onClickButton: this.toggle.bind(this),
221
- onClickTitle: this.doSelectNode.bind(this),
222
- onMouseCapture,
223
- onMouseDrag,
224
- onMouseStart,
225
- onMouseStop,
226
- triggerEvent,
227
- useContextMenu: this.options.useContextMenu,
228
- });
132
+ this.selectNodeHandler.addToSelection(node);
133
+ this.openParents(node);
229
134
 
230
- this.dataLoader = dataLoader;
231
- this.dndHandler = dndHandler;
232
- this.keyHandler = keyHandler;
233
- this.mouseHandler = mouseHandler;
234
- this.renderer = renderer;
235
- this.saveStateHandler = saveStateHandler;
236
- this.scrollHandler = scrollHandler;
237
- this.selectNodeHandler = selectNodeHandler;
238
- }
135
+ this.getNodeElementForNode(node).select(mustSetFocus ?? true);
239
136
 
240
- private containsElement(element: HTMLElement): boolean {
241
- const node = this.getNode(element);
137
+ this.saveState();
242
138
 
243
- return node != null && node.tree === this.tree;
139
+ return this.element;
244
140
  }
245
141
 
246
- private createFolderElement(node: Node) {
247
- const closedIconElement = this.renderer.closedIconElement;
248
- const getScrollLeft = this.scrollHandler.getScrollLeft.bind(
249
- this.scrollHandler,
250
- );
251
- const openedIconElement = this.renderer.openedIconElement;
252
- const tabIndex = this.options.tabIndex;
253
- const treeElement = this.element.get(0) as HTMLElement;
254
- const triggerEvent = this.triggerEvent.bind(this);
142
+ public appendNode(newNodeInfo: NodeData, parentNodeParam?: Node): Node {
143
+ const parentNode = parentNodeParam ?? this.tree;
255
144
 
256
- return new FolderElement({
257
- closedIconElement,
258
- getScrollLeft,
259
- node,
260
- openedIconElement,
261
- tabIndex,
262
- treeElement,
263
- triggerEvent,
264
- });
265
- }
145
+ const node = parentNode.append(newNodeInfo);
266
146
 
267
- private createNodeElement(node: Node) {
268
- const getScrollLeft = this.scrollHandler.getScrollLeft.bind(
269
- this.scrollHandler,
270
- );
271
- const tabIndex = this.options.tabIndex;
272
- const treeElement = this.element.get(0) as HTMLElement;
147
+ this.refreshElements(parentNode);
273
148
 
274
- return new NodeElement({
275
- getScrollLeft,
276
- node,
277
- tabIndex,
278
- treeElement,
279
- });
149
+ return node;
280
150
  }
281
151
 
282
- private deselectCurrentNode(): void {
283
- const node = this.getSelectedNode();
284
- if (node) {
285
- this.removeFromSelection(node);
152
+ public closeNode(node?: Node, slideParam?: boolean | null): JQuery {
153
+ if (!node) {
154
+ throw Error(NODE_PARAM_IS_EMPTY);
286
155
  }
287
- }
288
156
 
289
- private deselectNodes(parentNode: Node): void {
290
- const selectedNodesUnderParent =
291
- this.selectNodeHandler.getSelectedNodesUnder(parentNode);
292
- for (const n of selectedNodesUnderParent) {
293
- this.selectNodeHandler.removeFromSelection(n);
157
+ const slide = slideParam ?? this.options.slide;
158
+
159
+ if (node.isFolder() || node.isEmptyFolder) {
160
+ this.createFolderElement(node).close(
161
+ slide,
162
+ this.options.animationSpeed,
163
+ );
164
+
165
+ this.saveState();
294
166
  }
167
+
168
+ return this.element;
295
169
  }
296
170
 
297
- private doLoadData(data: NodeData[] | null, parentNode: Node | null): void {
298
- if (data) {
299
- if (parentNode) {
300
- this.deselectNodes(parentNode);
301
- this.loadSubtree(data, parentNode);
302
- } else {
303
- this.initTree(data);
304
- }
171
+ public deinit(): void {
172
+ this.element.empty();
173
+ this.element.off();
305
174
 
306
- if (this.isDragging()) {
307
- this.dndHandler.refresh();
308
- }
309
- }
175
+ this.keyHandler.deinit();
176
+ this.mouseHandler.deinit();
310
177
 
311
- this.triggerEvent("tree.load_data", {
312
- parent_node: parentNode,
313
- tree_data: data,
314
- });
315
- }
178
+ this.tree = new Node({}, true);
316
179
 
317
- private doLoadDataFromUrl(
318
- urlInfoParam: JQuery.AjaxSettings | null | string,
319
- parentNode: Node | null,
320
- onFinished: HandleFinishedLoading | null,
321
- ): void {
322
- const urlInfo = urlInfoParam ?? this.getDataUrlInfo(parentNode);
180
+ super.deinit();
181
+ }
323
182
 
324
- this.dataLoader.loadFromUrl(urlInfo, parentNode, onFinished);
183
+ public getNodeByCallback(callback: (node: Node) => boolean): Node | null {
184
+ return this.tree.getNodeByCallback(callback);
325
185
  }
326
186
 
327
- private doSelectNode(
328
- node: Node | null,
329
- optionsParam?: SelectNodeOptions,
330
- ): void {
331
- const saveState = (): void => {
332
- if (this.options.saveState) {
333
- this.saveStateHandler.saveState();
334
- }
335
- };
187
+ public getNodeByHtmlElement(
188
+ inputElement: HTMLElement | JQuery,
189
+ ): Node | null {
190
+ const element =
191
+ inputElement instanceof HTMLElement
192
+ ? inputElement
193
+ : inputElement[0];
336
194
 
337
- if (!node) {
338
- // Called with empty node -> deselect current node
339
- this.deselectCurrentNode();
340
- saveState();
341
- return;
195
+ if (!element) {
196
+ return null;
342
197
  }
343
- const defaultOptions = { mustSetFocus: true, mustToggle: true };
344
- const selectOptions = { ...defaultOptions, ...(optionsParam ?? {}) };
345
-
346
- const canSelect = (): boolean => {
347
- if (this.options.onCanSelectNode) {
348
- return (
349
- this.options.selectable &&
350
- this.options.onCanSelectNode(node)
351
- );
352
- } else {
353
- return this.options.selectable;
354
- }
355
- };
356
198
 
357
- if (!canSelect()) {
358
- return;
359
- }
199
+ return this.getNode(element);
200
+ }
360
201
 
361
- if (this.selectNodeHandler.isNodeSelected(node)) {
362
- if (selectOptions.mustToggle) {
363
- this.deselectCurrentNode();
364
- this.triggerEvent("tree.select", {
365
- node: null,
366
- previous_node: node,
367
- });
368
- }
369
- } else {
370
- const deselectedNode = this.getSelectedNode() || null;
371
- this.deselectCurrentNode();
372
- this.addToSelection(node, selectOptions.mustSetFocus);
202
+ public getNodeById(nodeId: NodeId): Node | null {
203
+ return this.tree.getNodeById(nodeId);
204
+ }
373
205
 
374
- this.triggerEvent("tree.select", {
375
- deselected_node: deselectedNode,
376
- node,
377
- });
378
- this.openParents(node);
379
- }
206
+ public getNodeByName(name: string): Node | null {
207
+ return this.tree.getNodeByName(name);
208
+ }
380
209
 
381
- saveState();
210
+ public getNodeByNameMustExist(name: string): Node {
211
+ return this.tree.getNodeByNameMustExist(name);
382
212
  }
383
213
 
384
- private getAutoOpenMaxLevel(): number {
385
- if (this.options.autoOpen === true) {
386
- return -1;
387
- } else if (typeof this.options.autoOpen === "number") {
388
- return this.options.autoOpen;
389
- } else if (typeof this.options.autoOpen === "string") {
390
- return parseInt(this.options.autoOpen, 10);
391
- } else {
392
- return 0;
393
- }
214
+ public getNodesByProperty(key: string, value: unknown): Node[] {
215
+ return this.tree.getNodesByProperty(key, value);
394
216
  }
395
217
 
396
- private getDataUrlInfo(node: Node | null): JQuery.AjaxSettings | null {
397
- const dataUrl =
398
- this.options.dataUrl ?? (this.element.data("url") as null | string);
218
+ public getSelectedNode(): false | Node {
219
+ return this.selectNodeHandler.getSelectedNode();
220
+ }
399
221
 
400
- const getUrlFromString = (url: string): JQuery.AjaxSettings => {
401
- const urlInfo: JQuery.AjaxSettings = { url };
222
+ public getSelectedNodes(): Node[] {
223
+ return this.selectNodeHandler.getSelectedNodes();
224
+ }
402
225
 
403
- setUrlInfoData(urlInfo);
226
+ public getState(): null | SavedState {
227
+ return this.saveStateHandler.getState();
228
+ }
404
229
 
405
- return urlInfo;
406
- };
230
+ public getStateFromStorage(): null | SavedState {
231
+ return this.saveStateHandler.getStateFromStorage();
232
+ }
407
233
 
408
- const setUrlInfoData = (urlInfo: JQuery.AjaxSettings): void => {
409
- if (node?.id) {
410
- // Load on demand of a subtree; add node parameter
411
- const data = { node: node.id };
412
- urlInfo.data = data;
413
- } else {
414
- // Add selected_node parameter
415
- const selectedNodeId = this.getNodeIdToBeSelected();
416
- if (selectedNodeId) {
417
- const data = { selected_node: selectedNodeId };
418
- urlInfo.data = data;
419
- }
420
- }
421
- };
234
+ public getTree(): Node {
235
+ return this.tree;
236
+ }
422
237
 
423
- if (typeof dataUrl === "function") {
424
- return dataUrl(node);
425
- } else if (typeof dataUrl === "string") {
426
- return getUrlFromString(dataUrl);
427
- } else if (dataUrl && typeof dataUrl === "object") {
428
- setUrlInfoData(dataUrl);
429
- return dataUrl;
430
- } else {
431
- return null;
432
- }
238
+ public getVersion(): string {
239
+ return __version__;
433
240
  }
434
241
 
435
- private getDefaultClosedIcon(): string {
436
- if (this.options.rtl) {
437
- // triangle to the left
438
- return "&#x25c0;";
439
- } else {
440
- // triangle to the right
441
- return "&#x25ba;";
242
+ public init(): void {
243
+ super.init();
244
+
245
+ this.element = this.$el;
246
+ this.isInitialized = false;
247
+
248
+ this.options.rtl = this.getRtlOption();
249
+
250
+ if (this.options.closedIcon == null) {
251
+ this.options.closedIcon = this.getDefaultClosedIcon();
442
252
  }
253
+
254
+ this.connectHandlers();
255
+
256
+ this.initData();
443
257
  }
444
258
 
445
- private getNode(element: HTMLElement): Node | null {
446
- const liElement = element.closest("li.jqtree_common");
259
+ public isDragging(): boolean {
260
+ return this.dndHandler.isDragging;
261
+ }
447
262
 
448
- if (liElement) {
449
- return jQuery(liElement).data("node") as Node;
450
- } else {
451
- return null;
263
+ public isNodeSelected(node?: Node): boolean {
264
+ if (!node) {
265
+ throw Error(NODE_PARAM_IS_EMPTY);
452
266
  }
267
+
268
+ return this.selectNodeHandler.isNodeSelected(node);
453
269
  }
454
270
 
455
- private getNodeElement(element: HTMLElement): NodeElement | null {
456
- const node = this.getNode(element);
457
- if (node) {
458
- return this.getNodeElementForNode(node);
459
- } else {
460
- return null;
461
- }
271
+ public loadData(data: NodeData[], parentNode: Node | null): JQuery {
272
+ this.doLoadData(data, parentNode);
273
+ return this.element;
462
274
  }
463
275
 
464
- private getNodeElementForNode(node: Node): NodeElement {
465
- if (node.isFolder()) {
466
- return this.createFolderElement(node);
276
+ /*
277
+ signatures:
278
+ - loadDataFromUrl(url, parent_node=null, on_finished=null)
279
+ loadDataFromUrl('/my_data');
280
+ loadDataFromUrl('/my_data', node1);
281
+ loadDataFromUrl('/my_data', node1, function() { console.log('finished'); });
282
+ loadDataFromUrl('/my_data', null, function() { console.log('finished'); });
283
+
284
+ - loadDataFromUrl(parent_node=null, on_finished=null)
285
+ loadDataFromUrl();
286
+ loadDataFromUrl(node1);
287
+ loadDataFromUrl(null, function() { console.log('finished'); });
288
+ loadDataFromUrl(node1, function() { console.log('finished'); });
289
+ */
290
+ public loadDataFromUrl(
291
+ param1: Node | null | string,
292
+ param2?: HandleFinishedLoading | Node | null,
293
+ param3?: HandleFinishedLoading,
294
+ ): JQuery {
295
+ if (typeof param1 === "string") {
296
+ // first parameter is url
297
+ this.doLoadDataFromUrl(
298
+ param1,
299
+ param2 as Node | null,
300
+ param3 ?? null,
301
+ );
467
302
  } else {
468
- return this.createNodeElement(node);
303
+ // first parameter is not url
304
+ this.doLoadDataFromUrl(
305
+ null,
306
+ param1,
307
+ param2 as HandleFinishedLoading | null,
308
+ );
469
309
  }
310
+
311
+ return this.element;
470
312
  }
471
313
 
472
- private getNodeIdToBeSelected(): NodeId | null {
473
- if (this.options.saveState) {
474
- return this.saveStateHandler.getNodeIdToBeSelected();
475
- } else {
476
- return null;
314
+ public moveDown(): JQuery {
315
+ const selectedNode = this.getSelectedNode();
316
+ if (selectedNode) {
317
+ this.keyHandler.moveDown(selectedNode);
477
318
  }
319
+
320
+ return this.element;
478
321
  }
479
322
 
480
- private getRtlOption(): boolean {
481
- if (this.options.rtl != null) {
482
- return this.options.rtl;
483
- } else {
484
- const dataRtl = this.element.data("rtl") as unknown;
485
-
486
- if (
487
- dataRtl !== null &&
488
- dataRtl !== false &&
489
- dataRtl !== undefined
490
- ) {
491
- return true;
492
- } else {
493
- return false;
494
- }
323
+ public moveNode(
324
+ node?: Node,
325
+ targetNode?: Node,
326
+ position?: Position,
327
+ ): JQuery {
328
+ if (!node) {
329
+ throw Error(NODE_PARAM_IS_EMPTY);
495
330
  }
496
- }
497
-
498
- private initData(): void {
499
- if (this.options.data) {
500
- this.doLoadData(this.options.data, null);
501
- } else {
502
- const dataUrl = this.getDataUrlInfo(null);
503
331
 
504
- if (dataUrl) {
505
- this.doLoadDataFromUrl(null, null, null);
506
- } else {
507
- this.doLoadData([], null);
508
- }
332
+ if (!targetNode) {
333
+ throw Error(PARAM_IS_EMPTY + "targetNode");
509
334
  }
510
- }
511
335
 
512
- private initTree(data: NodeData[]): void {
513
- const doInit = (): void => {
514
- if (!this.isInitialized) {
515
- this.isInitialized = true;
516
- this.triggerEvent("tree.init");
517
- }
518
- };
519
-
520
- this.tree = new this.options.nodeClass(
521
- null,
522
- true,
523
- this.options.nodeClass,
524
- );
336
+ if (!position) {
337
+ throw Error(PARAM_IS_EMPTY + "position");
338
+ }
525
339
 
526
- this.selectNodeHandler.clear();
340
+ this.tree.moveNode(node, targetNode, position);
341
+ this.refreshElements(null);
527
342
 
528
- this.tree.loadFromData(data);
343
+ return this.element;
344
+ }
529
345
 
530
- const mustLoadOnDemand = this.setInitialState();
346
+ public moveUp(): JQuery {
347
+ const selectedNode = this.getSelectedNode();
348
+ if (selectedNode) {
349
+ this.keyHandler.moveUp(selectedNode);
350
+ }
531
351
 
532
- this.refreshElements(null);
352
+ return this.element;
353
+ }
533
354
 
534
- if (!mustLoadOnDemand) {
535
- doInit();
536
- } else {
537
- // Load data on demand and then init the tree
538
- this.setInitialStateOnDemand(doInit);
355
+ public openNode(
356
+ node?: Node,
357
+ param1?: boolean | OnFinishOpenNode,
358
+ param2?: OnFinishOpenNode,
359
+ ): JQuery {
360
+ if (!node) {
361
+ throw Error(NODE_PARAM_IS_EMPTY);
539
362
  }
540
- }
541
363
 
542
- private isFocusOnTree(): boolean {
543
- const activeElement = document.activeElement;
364
+ const parseParams = (): [boolean, OnFinishOpenNode | undefined] => {
365
+ let onFinished: null | OnFinishOpenNode;
366
+ let slide: boolean | null;
544
367
 
545
- return Boolean(
546
- activeElement &&
547
- activeElement.tagName === "SPAN" &&
548
- this.containsElement(activeElement as HTMLElement),
549
- );
550
- }
368
+ if (isFunction(param1)) {
369
+ onFinished = param1 as OnFinishOpenNode;
370
+ slide = null;
371
+ } else {
372
+ slide = param1 as boolean;
373
+ onFinished = param2 as OnFinishOpenNode;
374
+ }
551
375
 
552
- private isSelectedNodeInSubtree(subtree: Node): boolean {
553
- const selectedNode = this.getSelectedNode();
376
+ if (slide == null) {
377
+ slide = this.options.slide;
378
+ }
554
379
 
555
- if (!selectedNode) {
556
- return false;
557
- } else {
558
- return subtree === selectedNode || subtree.isParentOf(selectedNode);
559
- }
560
- }
380
+ return [slide, onFinished];
381
+ };
561
382
 
562
- private loadFolderOnDemand(
563
- node: Node,
564
- slide = true,
565
- onFinished?: OnFinishOpenNode,
566
- ): void {
567
- node.is_loading = true;
383
+ const [slide, onFinished] = parseParams();
568
384
 
569
- this.doLoadDataFromUrl(null, node, () => {
570
- this.openNodeInternal(node, slide, onFinished);
571
- });
385
+ this.openNodeInternal(node, slide, onFinished);
386
+ return this.element;
572
387
  }
573
388
 
574
- private loadSubtree(data: NodeData[], parentNode: Node): void {
575
- parentNode.loadFromData(data);
389
+ public prependNode(newNodeInfo: NodeData, parentNodeParam?: Node): Node {
390
+ const parentNode = parentNodeParam ?? this.tree;
576
391
 
577
- parentNode.load_on_demand = false;
578
- parentNode.is_loading = false;
392
+ const node = parentNode.prepend(newNodeInfo);
579
393
 
580
394
  this.refreshElements(parentNode);
581
- }
582
395
 
583
- private mouseCapture(positionInfo: PositionInfo): boolean | null {
584
- if (this.options.dragAndDrop) {
585
- return this.dndHandler.mouseCapture(positionInfo);
586
- } else {
587
- return false;
588
- }
396
+ return node;
589
397
  }
590
398
 
591
- private mouseDrag(positionInfo: PositionInfo): boolean {
592
- if (this.options.dragAndDrop) {
593
- const result = this.dndHandler.mouseDrag(positionInfo);
399
+ public refresh(): JQuery {
400
+ this.refreshElements(null);
401
+ return this.element;
402
+ }
594
403
 
595
- this.scrollHandler.checkScrolling(positionInfo);
596
- return result;
597
- } else {
598
- return false;
599
- }
404
+ public refreshHitAreas(): JQuery {
405
+ this.dndHandler.refresh();
406
+ return this.element;
600
407
  }
601
408
 
602
- private mouseStart(positionInfo: PositionInfo): boolean {
603
- if (this.options.dragAndDrop) {
604
- return this.dndHandler.mouseStart(positionInfo);
605
- } else {
606
- return false;
607
- }
409
+ public reload(onFinished: HandleFinishedLoading | null): JQuery {
410
+ this.doLoadDataFromUrl(null, null, onFinished);
411
+ return this.element;
608
412
  }
609
413
 
610
- private mouseStop(positionInfo: PositionInfo): boolean {
611
- if (this.options.dragAndDrop) {
612
- this.scrollHandler.stopScrolling();
613
- return this.dndHandler.mouseStop(positionInfo);
614
- } else {
615
- return false;
414
+ public removeFromSelection(node?: Node): JQuery {
415
+ if (!node) {
416
+ throw Error(NODE_PARAM_IS_EMPTY);
616
417
  }
617
- }
618
418
 
619
- private openNodeInternal(
620
- node: Node,
621
- slide = true,
622
- onFinished?: OnFinishOpenNode,
623
- ): void {
624
- const doOpenNode = (
625
- _node: Node,
626
- _slide: boolean,
627
- _onFinished?: OnFinishOpenNode,
628
- ): void => {
629
- if (!node.children.length) {
630
- return;
631
- }
419
+ this.selectNodeHandler.removeFromSelection(node);
632
420
 
633
- const folderElement = this.createFolderElement(_node);
634
- folderElement.open(
635
- _onFinished,
636
- _slide,
637
- this.options.animationSpeed,
638
- );
639
- };
421
+ this.getNodeElementForNode(node).deselect();
422
+ this.saveState();
640
423
 
641
- if (node.isFolder() || node.isEmptyFolder) {
642
- if (node.load_on_demand) {
643
- this.loadFolderOnDemand(node, slide, onFinished);
644
- } else {
645
- let parent = node.parent;
424
+ return this.element;
425
+ }
646
426
 
647
- while (parent) {
648
- // nb: do not open root element
649
- if (parent.parent) {
650
- doOpenNode(parent, false);
651
- }
652
- parent = parent.parent;
653
- }
427
+ public removeNode(node?: Node): JQuery {
428
+ if (!node) {
429
+ throw Error(NODE_PARAM_IS_EMPTY);
430
+ }
654
431
 
655
- doOpenNode(node, slide, onFinished);
656
- this.saveState();
657
- }
432
+ if (!node.parent) {
433
+ throw Error("Node has no parent");
658
434
  }
659
- }
660
435
 
661
- private openParents(node: Node) {
436
+ this.selectNodeHandler.removeFromSelection(node, true); // including children
437
+
662
438
  const parent = node.parent;
439
+ node.remove();
440
+ this.refreshElements(parent);
663
441
 
664
- if (parent?.parent && !parent.is_open) {
665
- this.openNode(parent, false);
666
- }
442
+ return this.element;
667
443
  }
668
444
 
669
- /*
670
- Redraw the tree or part of the tree.
671
- from_node: redraw this subtree
672
- */
673
- private refreshElements(fromNode: Node | null): void {
674
- const mustSetFocus = this.isFocusOnTree();
675
- const mustSelect = fromNode
676
- ? this.isSelectedNodeInSubtree(fromNode)
677
- : false;
678
-
679
- this.renderer.render(fromNode);
445
+ public scrollToNode(node?: Node): JQuery {
446
+ if (!node) {
447
+ throw Error(NODE_PARAM_IS_EMPTY);
448
+ }
680
449
 
681
- if (mustSelect) {
682
- this.selectCurrentNode(mustSetFocus);
450
+ if (!node.element) {
451
+ return this.element;
683
452
  }
684
453
 
685
- this.triggerEvent("tree.refresh");
686
- }
454
+ const top =
455
+ getOffsetTop(node.element) -
456
+ getOffsetTop(this.$el.get(0) as HTMLElement);
687
457
 
688
- private saveState(): void {
689
- if (this.options.saveState) {
690
- this.saveStateHandler.saveState();
691
- }
458
+ this.scrollHandler.scrollToY(top);
459
+
460
+ return this.element;
692
461
  }
693
462
 
694
- private selectCurrentNode(mustSetFocus: boolean): void {
695
- const node = this.getSelectedNode();
696
- if (node) {
697
- const nodeElement = this.getNodeElementForNode(node);
698
- nodeElement.select(mustSetFocus);
463
+ public selectNode(
464
+ node: Node | null,
465
+ optionsParam?: SelectNodeOptions,
466
+ ): JQuery {
467
+ this.doSelectNode(node, optionsParam);
468
+ return this.element;
469
+ }
470
+
471
+ public setOption(option: string, value: unknown): JQuery {
472
+ (this.options as unknown as Record<string, unknown>)[option] = value;
473
+ return this.element;
474
+ }
475
+
476
+ public setState(state?: SavedState): JQuery {
477
+ if (state) {
478
+ this.saveStateHandler.setInitialState(state);
479
+ this.refreshElements(null);
699
480
  }
481
+
482
+ return this.element;
700
483
  }
701
484
 
702
- // Set initial state, either by restoring the state or auto-opening nodes
703
- // result: must load nodes on demand?
704
- private setInitialState(): boolean {
705
- const restoreState = (): [boolean, boolean] => {
706
- // result: is state restored, must load on demand?
707
- if (!this.options.saveState) {
708
- return [false, false];
709
- } else {
710
- const state = this.saveStateHandler.getStateFromStorage();
485
+ public toggle(node?: Node, slideParam: boolean | null = null): JQuery {
486
+ if (!node) {
487
+ throw Error(NODE_PARAM_IS_EMPTY);
488
+ }
711
489
 
712
- if (!state) {
713
- return [false, false];
714
- } else {
715
- const mustLoadOnDemand =
716
- this.saveStateHandler.setInitialState(state);
490
+ const slide = slideParam ?? this.options.slide;
717
491
 
718
- // return true: the state is restored
719
- return [true, mustLoadOnDemand];
720
- }
721
- }
722
- };
492
+ if (node.is_open) {
493
+ this.closeNode(node, slide);
494
+ } else {
495
+ this.openNode(node, slide);
496
+ }
723
497
 
724
- const autoOpenNodes = (): boolean => {
725
- // result: must load on demand?
726
- if (this.options.autoOpen === false) {
727
- return false;
728
- }
498
+ return this.element;
499
+ }
729
500
 
730
- const maxLevel = this.getAutoOpenMaxLevel();
731
- let mustLoadOnDemand = false;
501
+ public toJson(): string {
502
+ return JSON.stringify(this.tree.getData());
503
+ }
732
504
 
733
- this.tree.iterate((node: Node, level: number) => {
734
- if (node.load_on_demand) {
735
- mustLoadOnDemand = true;
736
- return false;
737
- } else if (!node.hasChildren()) {
738
- return false;
739
- } else {
740
- node.is_open = true;
741
- return level !== maxLevel;
742
- }
743
- });
505
+ public updateNode(node?: Node, data?: NodeData): JQuery {
506
+ if (!node) {
507
+ throw Error(NODE_PARAM_IS_EMPTY);
508
+ }
744
509
 
745
- return mustLoadOnDemand;
746
- };
510
+ if (!data) {
511
+ return this.element;
512
+ }
747
513
 
748
- let [isRestored, mustLoadOnDemand] = restoreState(); // eslint-disable-line prefer-const
514
+ const idIsChanged =
515
+ typeof data === "object" && data.id && data.id !== node.id;
749
516
 
750
- if (!isRestored) {
751
- mustLoadOnDemand = autoOpenNodes();
517
+ if (idIsChanged) {
518
+ this.tree.removeNodeFromIndex(node);
752
519
  }
753
520
 
754
- return mustLoadOnDemand;
755
- }
521
+ node.setData(data);
756
522
 
757
- // Set the initial state for nodes that are loaded on demand
758
- // Call cb_finished when done
759
- private setInitialStateOnDemand(cbFinished: () => void): void {
760
- const restoreState = (): boolean => {
761
- if (!this.options.saveState) {
762
- return false;
763
- } else {
764
- const state = this.saveStateHandler.getStateFromStorage();
523
+ if (idIsChanged) {
524
+ this.tree.addNodeToIndex(node);
525
+ }
765
526
 
766
- if (!state) {
767
- return false;
768
- } else {
769
- this.saveStateHandler.setInitialStateOnDemand(
770
- state,
771
- cbFinished,
772
- );
527
+ if (
528
+ typeof data === "object" &&
529
+ data.children &&
530
+ data.children instanceof Array
531
+ ) {
532
+ node.removeChildren();
773
533
 
774
- return true;
775
- }
534
+ if (data.children.length) {
535
+ node.loadFromData(data.children as Node[]);
776
536
  }
777
- };
537
+ }
778
538
 
779
- const autoOpenNodes = (): void => {
780
- const maxLevel = this.getAutoOpenMaxLevel();
781
- let loadingCount = 0;
539
+ this.refreshElements(node);
782
540
 
783
- const loadAndOpenNode = (node: Node): void => {
784
- loadingCount += 1;
785
- this.openNodeInternal(node, false, () => {
786
- loadingCount -= 1;
787
- openNodes();
788
- });
789
- };
541
+ return this.element;
542
+ }
790
543
 
791
- const openNodes = (): void => {
792
- this.tree.iterate((node: Node, level: number) => {
793
- if (node.load_on_demand) {
794
- if (!node.is_loading) {
795
- loadAndOpenNode(node);
796
- }
544
+ private connectHandlers() {
545
+ const {
546
+ autoEscape,
547
+ buttonLeft,
548
+ closedIcon,
549
+ dataFilter,
550
+ dragAndDrop,
551
+ keyboardSupport,
552
+ onCanMove,
553
+ onCanMoveTo,
554
+ onCreateLi,
555
+ onDragMove,
556
+ onDragStop,
557
+ onGetStateFromStorage,
558
+ onIsMoveHandle,
559
+ onLoadFailed,
560
+ onLoading,
561
+ onSetStateFromStorage,
562
+ openedIcon,
563
+ openFolderDelay,
564
+ rtl,
565
+ saveState,
566
+ showEmptyFolder,
567
+ slide,
568
+ tabIndex,
569
+ } = this.options;
797
570
 
798
- return false;
799
- } else {
800
- this.openNodeInternal(node, false);
571
+ const closeNode = this.closeNode.bind(this);
572
+ const getNodeElement = this.getNodeElement.bind(this);
573
+ const getNodeElementForNode = this.getNodeElementForNode.bind(this);
574
+ const getNodeById = this.getNodeById.bind(this);
575
+ const getSelectedNode = this.getSelectedNode.bind(this);
576
+ const getTree = this.getTree.bind(this);
577
+ const isFocusOnTree = this.isFocusOnTree.bind(this);
578
+ const loadData = this.loadData.bind(this);
579
+ const openNode = this.openNodeInternal.bind(this);
580
+ const refreshElements = this.refreshElements.bind(this);
581
+ const refreshHitAreas = this.refreshHitAreas.bind(this);
582
+ const selectNode = this.selectNode.bind(this);
583
+ const $treeElement = this.element;
584
+ const treeElement = this.element.get(0) as HTMLElement;
585
+ const triggerEvent = this.triggerEvent.bind(this);
801
586
 
802
- return level !== maxLevel;
803
- }
804
- });
587
+ const selectNodeHandler = new SelectNodeHandler({
588
+ getNodeById,
589
+ });
805
590
 
806
- if (loadingCount === 0) {
807
- cbFinished();
808
- }
809
- };
591
+ const addToSelection =
592
+ selectNodeHandler.addToSelection.bind(selectNodeHandler);
593
+ const getSelectedNodes =
594
+ selectNodeHandler.getSelectedNodes.bind(selectNodeHandler);
595
+ const isNodeSelected =
596
+ selectNodeHandler.isNodeSelected.bind(selectNodeHandler);
597
+ const removeFromSelection =
598
+ selectNodeHandler.removeFromSelection.bind(selectNodeHandler);
599
+ const getMouseDelay = () => this.options.startDndDelay ?? 0;
810
600
 
811
- openNodes();
812
- };
601
+ const dataLoader = new DataLoader({
602
+ dataFilter,
603
+ loadData,
604
+ onLoadFailed,
605
+ onLoading,
606
+ treeElement,
607
+ triggerEvent,
608
+ });
813
609
 
814
- if (!restoreState()) {
815
- autoOpenNodes();
816
- }
817
- }
610
+ const saveStateHandler = new SaveStateHandler({
611
+ addToSelection,
612
+ getNodeById,
613
+ getSelectedNodes,
614
+ getTree,
615
+ onGetStateFromStorage,
616
+ onSetStateFromStorage,
617
+ openNode,
618
+ refreshElements,
619
+ removeFromSelection,
620
+ saveState,
621
+ });
818
622
 
819
- private triggerEvent(
820
- eventName: string,
821
- values?: Record<string, unknown>,
822
- ): JQuery.Event {
823
- const event = jQuery.Event(eventName, values);
824
- this.element.trigger(event);
825
- return event;
826
- }
623
+ const scrollHandler = new ScrollHandler({
624
+ refreshHitAreas,
625
+ treeElement,
626
+ });
827
627
 
828
- public addNodeAfter(
829
- newNodeInfo: NodeData,
830
- existingNode: Node,
831
- ): Node | null {
832
- const newNode = existingNode.addAfter(newNodeInfo);
628
+ const getScrollLeft = scrollHandler.getScrollLeft.bind(scrollHandler);
833
629
 
834
- if (newNode) {
835
- this.refreshElements(existingNode.parent);
836
- }
630
+ const dndHandler = new DragAndDropHandler({
631
+ autoEscape,
632
+ getNodeElement,
633
+ getNodeElementForNode,
634
+ getScrollLeft,
635
+ getTree,
636
+ onCanMove,
637
+ onCanMoveTo,
638
+ onDragMove,
639
+ onDragStop,
640
+ onIsMoveHandle,
641
+ openFolderDelay,
642
+ openNode,
643
+ refreshElements,
644
+ slide,
645
+ treeElement,
646
+ triggerEvent,
647
+ });
837
648
 
838
- return newNode;
839
- }
649
+ const keyHandler = new KeyHandler({
650
+ closeNode,
651
+ getSelectedNode,
652
+ isFocusOnTree,
653
+ keyboardSupport,
654
+ openNode,
655
+ selectNode,
656
+ });
840
657
 
841
- public addNodeBefore(
842
- newNodeInfo: NodeData,
843
- existingNode?: Node,
844
- ): Node | null {
845
- if (!existingNode) {
846
- throw Error(PARAM_IS_EMPTY + "existingNode");
847
- }
658
+ const renderer = new ElementsRenderer({
659
+ $element: $treeElement,
660
+ autoEscape,
661
+ buttonLeft,
662
+ closedIcon,
663
+ dragAndDrop,
664
+ getTree,
665
+ isNodeSelected,
666
+ onCreateLi,
667
+ openedIcon,
668
+ rtl,
669
+ showEmptyFolder,
670
+ tabIndex,
671
+ });
848
672
 
849
- const newNode = existingNode.addBefore(newNodeInfo);
673
+ const getNode = this.getNode.bind(this);
674
+ const onMouseCapture = this.mouseCapture.bind(this);
675
+ const onMouseDrag = this.mouseDrag.bind(this);
676
+ const onMouseStart = this.mouseStart.bind(this);
677
+ const onMouseStop = this.mouseStop.bind(this);
678
+
679
+ const mouseHandler = new MouseHandler({
680
+ element: treeElement,
681
+ getMouseDelay,
682
+ getNode,
683
+ onClickButton: this.toggle.bind(this),
684
+ onClickTitle: this.doSelectNode.bind(this),
685
+ onMouseCapture,
686
+ onMouseDrag,
687
+ onMouseStart,
688
+ onMouseStop,
689
+ triggerEvent,
690
+ useContextMenu: this.options.useContextMenu,
691
+ });
692
+
693
+ this.dataLoader = dataLoader;
694
+ this.dndHandler = dndHandler;
695
+ this.keyHandler = keyHandler;
696
+ this.mouseHandler = mouseHandler;
697
+ this.renderer = renderer;
698
+ this.saveStateHandler = saveStateHandler;
699
+ this.scrollHandler = scrollHandler;
700
+ this.selectNodeHandler = selectNodeHandler;
701
+ }
850
702
 
851
- if (newNode) {
852
- this.refreshElements(existingNode.parent);
853
- }
703
+ private containsElement(element: HTMLElement): boolean {
704
+ const node = this.getNode(element);
854
705
 
855
- return newNode;
706
+ return node != null && node.tree === this.tree;
856
707
  }
857
708
 
858
- public addParentNode(
859
- newNodeInfo: NodeData,
860
- existingNode?: Node,
861
- ): Node | null {
862
- if (!existingNode) {
863
- throw Error(PARAM_IS_EMPTY + "existingNode");
864
- }
709
+ private createFolderElement(node: Node) {
710
+ const closedIconElement = this.renderer.closedIconElement;
711
+ const getScrollLeft = this.scrollHandler.getScrollLeft.bind(
712
+ this.scrollHandler,
713
+ );
714
+ const openedIconElement = this.renderer.openedIconElement;
715
+ const tabIndex = this.options.tabIndex;
716
+ const treeElement = this.element.get(0) as HTMLElement;
717
+ const triggerEvent = this.triggerEvent.bind(this);
865
718
 
866
- const newNode = existingNode.addParent(newNodeInfo);
719
+ return new FolderElement({
720
+ closedIconElement,
721
+ getScrollLeft,
722
+ node,
723
+ openedIconElement,
724
+ tabIndex,
725
+ treeElement,
726
+ triggerEvent,
727
+ });
728
+ }
867
729
 
868
- if (newNode) {
869
- this.refreshElements(newNode.parent);
870
- }
730
+ private createNodeElement(node: Node) {
731
+ const getScrollLeft = this.scrollHandler.getScrollLeft.bind(
732
+ this.scrollHandler,
733
+ );
734
+ const tabIndex = this.options.tabIndex;
735
+ const treeElement = this.element.get(0) as HTMLElement;
871
736
 
872
- return newNode;
737
+ return new NodeElement({
738
+ getScrollLeft,
739
+ node,
740
+ tabIndex,
741
+ treeElement,
742
+ });
873
743
  }
874
744
 
875
- public addToSelection(node?: Node, mustSetFocus?: boolean): JQuery {
876
- if (!node) {
877
- throw Error(NODE_PARAM_IS_EMPTY);
745
+ private deselectCurrentNode(): void {
746
+ const node = this.getSelectedNode();
747
+ if (node) {
748
+ this.removeFromSelection(node);
878
749
  }
750
+ }
879
751
 
880
- this.selectNodeHandler.addToSelection(node);
881
- this.openParents(node);
752
+ private deselectNodes(parentNode: Node): void {
753
+ const selectedNodesUnderParent =
754
+ this.selectNodeHandler.getSelectedNodesUnder(parentNode);
755
+ for (const n of selectedNodesUnderParent) {
756
+ this.selectNodeHandler.removeFromSelection(n);
757
+ }
758
+ }
882
759
 
883
- this.getNodeElementForNode(node).select(mustSetFocus ?? true);
760
+ private doLoadData(data: NodeData[] | null, parentNode: Node | null): void {
761
+ if (data) {
762
+ if (parentNode) {
763
+ this.deselectNodes(parentNode);
764
+ this.loadSubtree(data, parentNode);
765
+ } else {
766
+ this.initTree(data);
767
+ }
884
768
 
885
- this.saveState();
769
+ if (this.isDragging()) {
770
+ this.dndHandler.refresh();
771
+ }
772
+ }
886
773
 
887
- return this.element;
774
+ this.triggerEvent("tree.load_data", {
775
+ parent_node: parentNode,
776
+ tree_data: data,
777
+ });
888
778
  }
889
779
 
890
- public appendNode(newNodeInfo: NodeData, parentNodeParam?: Node): Node {
891
- const parentNode = parentNodeParam ?? this.tree;
892
-
893
- const node = parentNode.append(newNodeInfo);
894
-
895
- this.refreshElements(parentNode);
780
+ private doLoadDataFromUrl(
781
+ urlInfoParam: JQuery.AjaxSettings | null | string,
782
+ parentNode: Node | null,
783
+ onFinished: HandleFinishedLoading | null,
784
+ ): void {
785
+ const urlInfo = urlInfoParam ?? this.getDataUrlInfo(parentNode);
896
786
 
897
- return node;
787
+ this.dataLoader.loadFromUrl(urlInfo, parentNode, onFinished);
898
788
  }
899
789
 
900
- public closeNode(node?: Node, slideParam?: boolean | null): JQuery {
790
+ private doSelectNode(
791
+ node: Node | null,
792
+ optionsParam?: SelectNodeOptions,
793
+ ): void {
794
+ const saveState = (): void => {
795
+ if (this.options.saveState) {
796
+ this.saveStateHandler.saveState();
797
+ }
798
+ };
799
+
901
800
  if (!node) {
902
- throw Error(NODE_PARAM_IS_EMPTY);
801
+ // Called with empty node -> deselect current node
802
+ this.deselectCurrentNode();
803
+ saveState();
804
+ return;
903
805
  }
806
+ const defaultOptions = { mustSetFocus: true, mustToggle: true };
807
+ const selectOptions = { ...defaultOptions, ...(optionsParam ?? {}) };
904
808
 
905
- const slide = slideParam ?? this.options.slide;
809
+ const canSelect = (): boolean => {
810
+ if (this.options.onCanSelectNode) {
811
+ return (
812
+ this.options.selectable &&
813
+ this.options.onCanSelectNode(node)
814
+ );
815
+ } else {
816
+ return this.options.selectable;
817
+ }
818
+ };
906
819
 
907
- if (node.isFolder() || node.isEmptyFolder) {
908
- this.createFolderElement(node).close(
909
- slide,
910
- this.options.animationSpeed,
911
- );
820
+ if (!canSelect()) {
821
+ return;
822
+ }
912
823
 
913
- this.saveState();
824
+ if (this.selectNodeHandler.isNodeSelected(node)) {
825
+ if (selectOptions.mustToggle) {
826
+ this.deselectCurrentNode();
827
+ this.triggerEvent("tree.select", {
828
+ node: null,
829
+ previous_node: node,
830
+ });
831
+ }
832
+ } else {
833
+ const deselectedNode = this.getSelectedNode() || null;
834
+ this.deselectCurrentNode();
835
+ this.addToSelection(node, selectOptions.mustSetFocus);
836
+
837
+ this.triggerEvent("tree.select", {
838
+ deselected_node: deselectedNode,
839
+ node,
840
+ });
841
+ this.openParents(node);
914
842
  }
915
843
 
916
- return this.element;
844
+ saveState();
917
845
  }
918
846
 
919
- public deinit(): void {
920
- this.element.empty();
921
- this.element.off();
847
+ private getAutoOpenMaxLevel(): number {
848
+ if (this.options.autoOpen === true) {
849
+ return -1;
850
+ } else if (typeof this.options.autoOpen === "number") {
851
+ return this.options.autoOpen;
852
+ } else if (typeof this.options.autoOpen === "string") {
853
+ return parseInt(this.options.autoOpen, 10);
854
+ } else {
855
+ return 0;
856
+ }
857
+ }
922
858
 
923
- this.keyHandler.deinit();
924
- this.mouseHandler.deinit();
859
+ private getDataUrlInfo(node: Node | null): JQuery.AjaxSettings | null {
860
+ const dataUrl =
861
+ this.options.dataUrl ?? (this.element.data("url") as null | string);
925
862
 
926
- this.tree = new Node({}, true);
863
+ const getUrlFromString = (url: string): JQuery.AjaxSettings => {
864
+ const urlInfo: JQuery.AjaxSettings = { url };
927
865
 
928
- super.deinit();
929
- }
866
+ setUrlInfoData(urlInfo);
930
867
 
931
- public getNodeByCallback(callback: (node: Node) => boolean): Node | null {
932
- return this.tree.getNodeByCallback(callback);
933
- }
868
+ return urlInfo;
869
+ };
934
870
 
935
- public getNodeByHtmlElement(
936
- inputElement: HTMLElement | JQuery,
937
- ): Node | null {
938
- const element =
939
- inputElement instanceof HTMLElement
940
- ? inputElement
941
- : inputElement[0];
871
+ const setUrlInfoData = (urlInfo: JQuery.AjaxSettings): void => {
872
+ if (node?.id) {
873
+ // Load on demand of a subtree; add node parameter
874
+ const data = { node: node.id };
875
+ urlInfo.data = data;
876
+ } else {
877
+ // Add selected_node parameter
878
+ const selectedNodeId = this.getNodeIdToBeSelected();
879
+ if (selectedNodeId) {
880
+ const data = { selected_node: selectedNodeId };
881
+ urlInfo.data = data;
882
+ }
883
+ }
884
+ };
942
885
 
943
- if (!element) {
886
+ if (typeof dataUrl === "function") {
887
+ return dataUrl(node);
888
+ } else if (typeof dataUrl === "string") {
889
+ return getUrlFromString(dataUrl);
890
+ } else if (dataUrl && typeof dataUrl === "object") {
891
+ setUrlInfoData(dataUrl);
892
+ return dataUrl;
893
+ } else {
944
894
  return null;
945
895
  }
946
-
947
- return this.getNode(element);
948
896
  }
949
897
 
950
- public getNodeById(nodeId: NodeId): Node | null {
951
- return this.tree.getNodeById(nodeId);
898
+ private getDefaultClosedIcon(): string {
899
+ if (this.options.rtl) {
900
+ // triangle to the left
901
+ return "&#x25c0;";
902
+ } else {
903
+ // triangle to the right
904
+ return "&#x25ba;";
905
+ }
952
906
  }
953
907
 
954
- public getNodeByName(name: string): Node | null {
955
- return this.tree.getNodeByName(name);
956
- }
908
+ private getNode(element: HTMLElement): Node | null {
909
+ const liElement = element.closest("li.jqtree_common");
957
910
 
958
- public getNodeByNameMustExist(name: string): Node {
959
- return this.tree.getNodeByNameMustExist(name);
911
+ if (liElement) {
912
+ return jQuery(liElement).data("node") as Node;
913
+ } else {
914
+ return null;
915
+ }
960
916
  }
961
917
 
962
- public getNodesByProperty(key: string, value: unknown): Node[] {
963
- return this.tree.getNodesByProperty(key, value);
918
+ private getNodeElement(element: HTMLElement): NodeElement | null {
919
+ const node = this.getNode(element);
920
+ if (node) {
921
+ return this.getNodeElementForNode(node);
922
+ } else {
923
+ return null;
924
+ }
964
925
  }
965
926
 
966
- public getSelectedNode(): false | Node {
967
- return this.selectNodeHandler.getSelectedNode();
927
+ private getNodeElementForNode(node: Node): NodeElement {
928
+ if (node.isFolder()) {
929
+ return this.createFolderElement(node);
930
+ } else {
931
+ return this.createNodeElement(node);
932
+ }
968
933
  }
969
934
 
970
- public getSelectedNodes(): Node[] {
971
- return this.selectNodeHandler.getSelectedNodes();
935
+ private getNodeIdToBeSelected(): NodeId | null {
936
+ if (this.options.saveState) {
937
+ return this.saveStateHandler.getNodeIdToBeSelected();
938
+ } else {
939
+ return null;
940
+ }
972
941
  }
973
942
 
974
- public getState(): null | SavedState {
975
- return this.saveStateHandler.getState();
976
- }
943
+ private getRtlOption(): boolean {
944
+ if (this.options.rtl != null) {
945
+ return this.options.rtl;
946
+ } else {
947
+ const dataRtl = this.element.data("rtl") as unknown;
977
948
 
978
- public getStateFromStorage(): null | SavedState {
979
- return this.saveStateHandler.getStateFromStorage();
949
+ if (
950
+ dataRtl !== null &&
951
+ dataRtl !== false &&
952
+ dataRtl !== undefined
953
+ ) {
954
+ return true;
955
+ } else {
956
+ return false;
957
+ }
958
+ }
980
959
  }
981
960
 
982
- public getTree(): Node {
983
- return this.tree;
984
- }
961
+ private initData(): void {
962
+ if (this.options.data) {
963
+ this.doLoadData(this.options.data, null);
964
+ } else {
965
+ const dataUrl = this.getDataUrlInfo(null);
985
966
 
986
- public getVersion(): string {
987
- return __version__;
967
+ if (dataUrl) {
968
+ this.doLoadDataFromUrl(null, null, null);
969
+ } else {
970
+ this.doLoadData([], null);
971
+ }
972
+ }
988
973
  }
989
974
 
990
- public init(): void {
991
- super.init();
992
-
993
- this.element = this.$el;
994
- this.isInitialized = false;
975
+ private initTree(data: NodeData[]): void {
976
+ const doInit = (): void => {
977
+ if (!this.isInitialized) {
978
+ this.isInitialized = true;
979
+ this.triggerEvent("tree.init");
980
+ }
981
+ };
995
982
 
996
- this.options.rtl = this.getRtlOption();
983
+ this.tree = new this.options.nodeClass(
984
+ null,
985
+ true,
986
+ this.options.nodeClass,
987
+ );
997
988
 
998
- if (this.options.closedIcon == null) {
999
- this.options.closedIcon = this.getDefaultClosedIcon();
1000
- }
989
+ this.selectNodeHandler.clear();
1001
990
 
1002
- this.connectHandlers();
991
+ this.tree.loadFromData(data);
1003
992
 
1004
- this.initData();
1005
- }
993
+ const mustLoadOnDemand = this.setInitialState();
1006
994
 
1007
- public isDragging(): boolean {
1008
- return this.dndHandler.isDragging;
1009
- }
995
+ this.refreshElements(null);
1010
996
 
1011
- public isNodeSelected(node?: Node): boolean {
1012
- if (!node) {
1013
- throw Error(NODE_PARAM_IS_EMPTY);
997
+ if (!mustLoadOnDemand) {
998
+ doInit();
999
+ } else {
1000
+ // Load data on demand and then init the tree
1001
+ this.setInitialStateOnDemand(doInit);
1014
1002
  }
1015
-
1016
- return this.selectNodeHandler.isNodeSelected(node);
1017
1003
  }
1018
1004
 
1019
- public loadData(data: NodeData[], parentNode: Node | null): JQuery {
1020
- this.doLoadData(data, parentNode);
1021
- return this.element;
1005
+ private isFocusOnTree(): boolean {
1006
+ const activeElement = document.activeElement;
1007
+
1008
+ return Boolean(
1009
+ activeElement &&
1010
+ activeElement.tagName === "SPAN" &&
1011
+ this.containsElement(activeElement as HTMLElement),
1012
+ );
1022
1013
  }
1023
1014
 
1024
- /*
1025
- signatures:
1026
- - loadDataFromUrl(url, parent_node=null, on_finished=null)
1027
- loadDataFromUrl('/my_data');
1028
- loadDataFromUrl('/my_data', node1);
1029
- loadDataFromUrl('/my_data', node1, function() { console.log('finished'); });
1030
- loadDataFromUrl('/my_data', null, function() { console.log('finished'); });
1015
+ private isSelectedNodeInSubtree(subtree: Node): boolean {
1016
+ const selectedNode = this.getSelectedNode();
1031
1017
 
1032
- - loadDataFromUrl(parent_node=null, on_finished=null)
1033
- loadDataFromUrl();
1034
- loadDataFromUrl(node1);
1035
- loadDataFromUrl(null, function() { console.log('finished'); });
1036
- loadDataFromUrl(node1, function() { console.log('finished'); });
1037
- */
1038
- public loadDataFromUrl(
1039
- param1: Node | null | string,
1040
- param2?: HandleFinishedLoading | Node | null,
1041
- param3?: HandleFinishedLoading,
1042
- ): JQuery {
1043
- if (typeof param1 === "string") {
1044
- // first parameter is url
1045
- this.doLoadDataFromUrl(
1046
- param1,
1047
- param2 as Node | null,
1048
- param3 ?? null,
1049
- );
1018
+ if (!selectedNode) {
1019
+ return false;
1050
1020
  } else {
1051
- // first parameter is not url
1052
- this.doLoadDataFromUrl(
1053
- null,
1054
- param1,
1055
- param2 as HandleFinishedLoading | null,
1056
- );
1021
+ return subtree === selectedNode || subtree.isParentOf(selectedNode);
1057
1022
  }
1058
-
1059
- return this.element;
1060
1023
  }
1061
1024
 
1062
- public moveDown(): JQuery {
1063
- const selectedNode = this.getSelectedNode();
1064
- if (selectedNode) {
1065
- this.keyHandler.moveDown(selectedNode);
1066
- }
1025
+ private loadFolderOnDemand(
1026
+ node: Node,
1027
+ slide = true,
1028
+ onFinished?: OnFinishOpenNode,
1029
+ ): void {
1030
+ node.is_loading = true;
1067
1031
 
1068
- return this.element;
1032
+ this.doLoadDataFromUrl(null, node, () => {
1033
+ this.openNodeInternal(node, slide, onFinished);
1034
+ });
1069
1035
  }
1070
1036
 
1071
- public moveNode(node?: Node, targetNode?: Node, position?: string): JQuery {
1072
- if (!node) {
1073
- throw Error(NODE_PARAM_IS_EMPTY);
1074
- }
1037
+ private loadSubtree(data: NodeData[], parentNode: Node): void {
1038
+ parentNode.loadFromData(data);
1075
1039
 
1076
- if (!targetNode) {
1077
- throw Error(PARAM_IS_EMPTY + "targetNode");
1078
- }
1040
+ parentNode.load_on_demand = false;
1041
+ parentNode.is_loading = false;
1079
1042
 
1080
- if (!position) {
1081
- throw Error(PARAM_IS_EMPTY + "position");
1043
+ this.refreshElements(parentNode);
1044
+ }
1045
+
1046
+ private mouseCapture(positionInfo: PositionInfo): boolean | null {
1047
+ if (this.options.dragAndDrop) {
1048
+ return this.dndHandler.mouseCapture(positionInfo);
1049
+ } else {
1050
+ return false;
1082
1051
  }
1052
+ }
1083
1053
 
1084
- const positionIndex = getPosition(position);
1054
+ private mouseDrag(positionInfo: PositionInfo): boolean {
1055
+ if (this.options.dragAndDrop) {
1056
+ const result = this.dndHandler.mouseDrag(positionInfo);
1085
1057
 
1086
- if (positionIndex !== undefined) {
1087
- this.tree.moveNode(node, targetNode, positionIndex);
1088
- this.refreshElements(null);
1058
+ this.scrollHandler.checkScrolling(positionInfo);
1059
+ return result;
1060
+ } else {
1061
+ return false;
1089
1062
  }
1090
-
1091
- return this.element;
1092
1063
  }
1093
1064
 
1094
- public moveUp(): JQuery {
1095
- const selectedNode = this.getSelectedNode();
1096
- if (selectedNode) {
1097
- this.keyHandler.moveUp(selectedNode);
1065
+ private mouseStart(positionInfo: PositionInfo): boolean {
1066
+ if (this.options.dragAndDrop) {
1067
+ return this.dndHandler.mouseStart(positionInfo);
1068
+ } else {
1069
+ return false;
1098
1070
  }
1099
-
1100
- return this.element;
1101
1071
  }
1102
1072
 
1103
- public openNode(
1104
- node?: Node,
1105
- param1?: boolean | OnFinishOpenNode,
1106
- param2?: OnFinishOpenNode,
1107
- ): JQuery {
1108
- if (!node) {
1109
- throw Error(NODE_PARAM_IS_EMPTY);
1073
+ private mouseStop(positionInfo: PositionInfo): boolean {
1074
+ if (this.options.dragAndDrop) {
1075
+ this.scrollHandler.stopScrolling();
1076
+ return this.dndHandler.mouseStop(positionInfo);
1077
+ } else {
1078
+ return false;
1110
1079
  }
1111
-
1112
- const parseParams = (): [boolean, OnFinishOpenNode | undefined] => {
1113
- let onFinished: null | OnFinishOpenNode;
1114
- let slide: boolean | null;
1115
-
1116
- if (isFunction(param1)) {
1117
- onFinished = param1 as OnFinishOpenNode;
1118
- slide = null;
1119
- } else {
1120
- slide = param1 as boolean;
1121
- onFinished = param2 as OnFinishOpenNode;
1122
- }
1123
-
1124
- if (slide == null) {
1125
- slide = this.options.slide;
1126
- }
1127
-
1128
- return [slide, onFinished];
1129
- };
1130
-
1131
- const [slide, onFinished] = parseParams();
1132
-
1133
- this.openNodeInternal(node, slide, onFinished);
1134
- return this.element;
1135
1080
  }
1136
1081
 
1137
- public prependNode(newNodeInfo: NodeData, parentNodeParam?: Node): Node {
1138
- const parentNode = parentNodeParam ?? this.tree;
1082
+ private openNodeInternal(
1083
+ node: Node,
1084
+ slide = true,
1085
+ onFinished?: OnFinishOpenNode,
1086
+ ): void {
1087
+ const doOpenNode = (
1088
+ _node: Node,
1089
+ _slide: boolean,
1090
+ _onFinished?: OnFinishOpenNode,
1091
+ ): void => {
1092
+ if (!node.children.length) {
1093
+ return;
1094
+ }
1139
1095
 
1140
- const node = parentNode.prepend(newNodeInfo);
1096
+ const folderElement = this.createFolderElement(_node);
1097
+ folderElement.open(
1098
+ _onFinished,
1099
+ _slide,
1100
+ this.options.animationSpeed,
1101
+ );
1102
+ };
1141
1103
 
1142
- this.refreshElements(parentNode);
1104
+ if (node.isFolder() || node.isEmptyFolder) {
1105
+ if (node.load_on_demand) {
1106
+ this.loadFolderOnDemand(node, slide, onFinished);
1107
+ } else {
1108
+ let parent = node.parent;
1143
1109
 
1144
- return node;
1145
- }
1110
+ while (parent) {
1111
+ // nb: do not open root element
1112
+ if (parent.parent) {
1113
+ doOpenNode(parent, false);
1114
+ }
1115
+ parent = parent.parent;
1116
+ }
1146
1117
 
1147
- public refresh(): JQuery {
1148
- this.refreshElements(null);
1149
- return this.element;
1118
+ doOpenNode(node, slide, onFinished);
1119
+ this.saveState();
1120
+ }
1121
+ }
1150
1122
  }
1151
1123
 
1152
- public refreshHitAreas(): JQuery {
1153
- this.dndHandler.refresh();
1154
- return this.element;
1155
- }
1124
+ private openParents(node: Node) {
1125
+ const parent = node.parent;
1156
1126
 
1157
- public reload(onFinished: HandleFinishedLoading | null): JQuery {
1158
- this.doLoadDataFromUrl(null, null, onFinished);
1159
- return this.element;
1127
+ if (parent?.parent && !parent.is_open) {
1128
+ this.openNode(parent, false);
1129
+ }
1160
1130
  }
1161
1131
 
1162
- public removeFromSelection(node?: Node): JQuery {
1163
- if (!node) {
1164
- throw Error(NODE_PARAM_IS_EMPTY);
1165
- }
1132
+ /*
1133
+ Redraw the tree or part of the tree.
1134
+ from_node: redraw this subtree
1135
+ */
1136
+ private refreshElements(fromNode: Node | null): void {
1137
+ const mustSetFocus = this.isFocusOnTree();
1138
+ const mustSelect = fromNode
1139
+ ? this.isSelectedNodeInSubtree(fromNode)
1140
+ : false;
1166
1141
 
1167
- this.selectNodeHandler.removeFromSelection(node);
1142
+ this.renderer.render(fromNode);
1168
1143
 
1169
- this.getNodeElementForNode(node).deselect();
1170
- this.saveState();
1144
+ if (mustSelect) {
1145
+ this.selectCurrentNode(mustSetFocus);
1146
+ }
1171
1147
 
1172
- return this.element;
1148
+ this.triggerEvent("tree.refresh");
1173
1149
  }
1174
1150
 
1175
- public removeNode(node?: Node): JQuery {
1176
- if (!node) {
1177
- throw Error(NODE_PARAM_IS_EMPTY);
1178
- }
1179
-
1180
- if (!node.parent) {
1181
- throw Error("Node has no parent");
1151
+ private saveState(): void {
1152
+ if (this.options.saveState) {
1153
+ this.saveStateHandler.saveState();
1182
1154
  }
1183
-
1184
- this.selectNodeHandler.removeFromSelection(node, true); // including children
1185
-
1186
- const parent = node.parent;
1187
- node.remove();
1188
- this.refreshElements(parent);
1189
-
1190
- return this.element;
1191
1155
  }
1192
1156
 
1193
- public scrollToNode(node?: Node): JQuery {
1194
- if (!node) {
1195
- throw Error(NODE_PARAM_IS_EMPTY);
1157
+ private selectCurrentNode(mustSetFocus: boolean): void {
1158
+ const node = this.getSelectedNode();
1159
+ if (node) {
1160
+ const nodeElement = this.getNodeElementForNode(node);
1161
+ nodeElement.select(mustSetFocus);
1196
1162
  }
1163
+ }
1197
1164
 
1198
- if (!node.element) {
1199
- return this.element;
1200
- }
1165
+ // Set initial state, either by restoring the state or auto-opening nodes
1166
+ // result: must load nodes on demand?
1167
+ private setInitialState(): boolean {
1168
+ const restoreState = (): [boolean, boolean] => {
1169
+ // result: is state restored, must load on demand?
1170
+ if (!this.options.saveState) {
1171
+ return [false, false];
1172
+ } else {
1173
+ const state = this.saveStateHandler.getStateFromStorage();
1201
1174
 
1202
- const top =
1203
- getOffsetTop(node.element) -
1204
- getOffsetTop(this.$el.get(0) as HTMLElement);
1175
+ if (!state) {
1176
+ return [false, false];
1177
+ } else {
1178
+ const mustLoadOnDemand =
1179
+ this.saveStateHandler.setInitialState(state);
1205
1180
 
1206
- this.scrollHandler.scrollToY(top);
1181
+ // return true: the state is restored
1182
+ return [true, mustLoadOnDemand];
1183
+ }
1184
+ }
1185
+ };
1207
1186
 
1208
- return this.element;
1209
- }
1187
+ const autoOpenNodes = (): boolean => {
1188
+ // result: must load on demand?
1189
+ if (this.options.autoOpen === false) {
1190
+ return false;
1191
+ }
1210
1192
 
1211
- public selectNode(
1212
- node: Node | null,
1213
- optionsParam?: SelectNodeOptions,
1214
- ): JQuery {
1215
- this.doSelectNode(node, optionsParam);
1216
- return this.element;
1217
- }
1193
+ const maxLevel = this.getAutoOpenMaxLevel();
1194
+ let mustLoadOnDemand = false;
1218
1195
 
1219
- public setOption(option: string, value: unknown): JQuery {
1220
- (this.options as unknown as Record<string, unknown>)[option] = value;
1221
- return this.element;
1222
- }
1196
+ this.tree.iterate((node: Node, level: number) => {
1197
+ if (node.load_on_demand) {
1198
+ mustLoadOnDemand = true;
1199
+ return false;
1200
+ } else if (!node.hasChildren()) {
1201
+ return false;
1202
+ } else {
1203
+ node.is_open = true;
1204
+ return level !== maxLevel;
1205
+ }
1206
+ });
1223
1207
 
1224
- public setState(state?: SavedState): JQuery {
1225
- if (state) {
1226
- this.saveStateHandler.setInitialState(state);
1227
- this.refreshElements(null);
1228
- }
1208
+ return mustLoadOnDemand;
1209
+ };
1229
1210
 
1230
- return this.element;
1231
- }
1211
+ let [isRestored, mustLoadOnDemand] = restoreState(); // eslint-disable-line prefer-const
1232
1212
 
1233
- public toggle(node?: Node, slideParam: boolean | null = null): JQuery {
1234
- if (!node) {
1235
- throw Error(NODE_PARAM_IS_EMPTY);
1213
+ if (!isRestored) {
1214
+ mustLoadOnDemand = autoOpenNodes();
1236
1215
  }
1237
1216
 
1238
- const slide = slideParam ?? this.options.slide;
1217
+ return mustLoadOnDemand;
1218
+ }
1239
1219
 
1240
- if (node.is_open) {
1241
- this.closeNode(node, slide);
1242
- } else {
1243
- this.openNode(node, slide);
1244
- }
1220
+ // Set the initial state for nodes that are loaded on demand
1221
+ // Call cb_finished when done
1222
+ private setInitialStateOnDemand(cbFinished: () => void): void {
1223
+ const restoreState = (): boolean => {
1224
+ if (!this.options.saveState) {
1225
+ return false;
1226
+ } else {
1227
+ const state = this.saveStateHandler.getStateFromStorage();
1245
1228
 
1246
- return this.element;
1247
- }
1229
+ if (!state) {
1230
+ return false;
1231
+ } else {
1232
+ this.saveStateHandler.setInitialStateOnDemand(
1233
+ state,
1234
+ cbFinished,
1235
+ );
1248
1236
 
1249
- public toJson(): string {
1250
- return JSON.stringify(this.tree.getData());
1251
- }
1237
+ return true;
1238
+ }
1239
+ }
1240
+ };
1252
1241
 
1253
- public updateNode(node?: Node, data?: NodeData): JQuery {
1254
- if (!node) {
1255
- throw Error(NODE_PARAM_IS_EMPTY);
1256
- }
1242
+ const autoOpenNodes = (): void => {
1243
+ const maxLevel = this.getAutoOpenMaxLevel();
1244
+ let loadingCount = 0;
1257
1245
 
1258
- if (!data) {
1259
- return this.element;
1260
- }
1246
+ const loadAndOpenNode = (node: Node): void => {
1247
+ loadingCount += 1;
1248
+ this.openNodeInternal(node, false, () => {
1249
+ loadingCount -= 1;
1250
+ openNodes();
1251
+ });
1252
+ };
1261
1253
 
1262
- const idIsChanged =
1263
- typeof data === "object" && data.id && data.id !== node.id;
1254
+ const openNodes = (): void => {
1255
+ this.tree.iterate((node: Node, level: number) => {
1256
+ if (node.load_on_demand) {
1257
+ if (!node.is_loading) {
1258
+ loadAndOpenNode(node);
1259
+ }
1264
1260
 
1265
- if (idIsChanged) {
1266
- this.tree.removeNodeFromIndex(node);
1267
- }
1261
+ return false;
1262
+ } else {
1263
+ this.openNodeInternal(node, false);
1268
1264
 
1269
- node.setData(data);
1265
+ return level !== maxLevel;
1266
+ }
1267
+ });
1270
1268
 
1271
- if (idIsChanged) {
1272
- this.tree.addNodeToIndex(node);
1273
- }
1269
+ if (loadingCount === 0) {
1270
+ cbFinished();
1271
+ }
1272
+ };
1274
1273
 
1275
- if (
1276
- typeof data === "object" &&
1277
- data.children &&
1278
- data.children instanceof Array
1279
- ) {
1280
- node.removeChildren();
1274
+ openNodes();
1275
+ };
1281
1276
 
1282
- if (data.children.length) {
1283
- node.loadFromData(data.children as Node[]);
1284
- }
1277
+ if (!restoreState()) {
1278
+ autoOpenNodes();
1285
1279
  }
1280
+ }
1286
1281
 
1287
- this.refreshElements(node);
1288
-
1289
- return this.element;
1282
+ private triggerEvent(
1283
+ eventName: string,
1284
+ values?: Record<string, unknown>,
1285
+ ): JQuery.Event {
1286
+ const event = jQuery.Event(eventName, values);
1287
+ this.element.trigger(event);
1288
+ return event;
1290
1289
  }
1291
1290
  }
1292
1291