jqtree 1.8.7 → 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,1211 +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;
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
- $treeElement,
258
- closedIconElement,
259
- getScrollLeft,
260
- node,
261
- openedIconElement,
262
- tabIndex,
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;
147
+ this.refreshElements(parentNode);
273
148
 
274
- return new NodeElement({
275
- $treeElement,
276
- getScrollLeft,
277
- node,
278
- tabIndex,
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
331
 
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
-
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
-
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
335
 
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);
594
-
595
- this.scrollHandler.checkScrolling(positionInfo);
596
- return result;
597
- } else {
598
- return false;
599
- }
399
+ public refresh(): JQuery {
400
+ this.refreshElements(null);
401
+ return this.element;
600
402
  }
601
403
 
602
- private mouseStart(positionInfo: PositionInfo): boolean {
603
- if (this.options.dragAndDrop) {
604
- return this.dndHandler.mouseStart(positionInfo);
605
- } else {
606
- return false;
607
- }
404
+ public refreshHitAreas(): JQuery {
405
+ this.dndHandler.refresh();
406
+ return this.element;
608
407
  }
609
408
 
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;
616
- }
409
+ public reload(onFinished: HandleFinishedLoading | null): JQuery {
410
+ this.doLoadDataFromUrl(null, null, onFinished);
411
+ return this.element;
617
412
  }
618
413
 
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
- const folderElement = this.createFolderElement(_node);
630
- folderElement.open(
631
- _onFinished,
632
- _slide,
633
- this.options.animationSpeed,
634
- );
635
- };
414
+ public removeFromSelection(node?: Node): JQuery {
415
+ if (!node) {
416
+ throw Error(NODE_PARAM_IS_EMPTY);
417
+ }
636
418
 
637
- if (node.isFolder() || node.isEmptyFolder) {
638
- if (node.load_on_demand) {
639
- this.loadFolderOnDemand(node, slide, onFinished);
640
- } else {
641
- let parent = node.parent;
419
+ this.selectNodeHandler.removeFromSelection(node);
642
420
 
643
- while (parent) {
644
- // nb: do not open root element
645
- if (parent.parent) {
646
- doOpenNode(parent, false);
647
- }
648
- parent = parent.parent;
649
- }
421
+ this.getNodeElementForNode(node).deselect();
422
+ this.saveState();
650
423
 
651
- doOpenNode(node, slide, onFinished);
652
- this.saveState();
653
- }
654
- }
424
+ return this.element;
655
425
  }
656
426
 
657
- private openParents(node: Node) {
658
- const parent = node.parent;
659
-
660
- if (parent?.parent && !parent.is_open) {
661
- this.openNode(parent, false);
427
+ public removeNode(node?: Node): JQuery {
428
+ if (!node) {
429
+ throw Error(NODE_PARAM_IS_EMPTY);
662
430
  }
663
- }
664
431
 
665
- /*
666
- Redraw the tree or part of the tree.
667
- from_node: redraw this subtree
668
- */
669
- private refreshElements(fromNode: Node | null): void {
670
- const mustSetFocus = this.isFocusOnTree();
671
- const mustSelect = fromNode
672
- ? this.isSelectedNodeInSubtree(fromNode)
673
- : false;
432
+ if (!node.parent) {
433
+ throw Error("Node has no parent");
434
+ }
674
435
 
675
- this.renderer.render(fromNode);
436
+ this.selectNodeHandler.removeFromSelection(node, true); // including children
676
437
 
677
- if (mustSelect) {
678
- this.selectCurrentNode(mustSetFocus);
679
- }
438
+ const parent = node.parent;
439
+ node.remove();
440
+ this.refreshElements(parent);
680
441
 
681
- this.triggerEvent("tree.refresh");
442
+ return this.element;
682
443
  }
683
444
 
684
- private saveState(): void {
685
- if (this.options.saveState) {
686
- this.saveStateHandler.saveState();
445
+ public scrollToNode(node?: Node): JQuery {
446
+ if (!node) {
447
+ throw Error(NODE_PARAM_IS_EMPTY);
687
448
  }
688
- }
689
449
 
690
- private selectCurrentNode(mustSetFocus: boolean): void {
691
- const node = this.getSelectedNode();
692
- if (node) {
693
- const nodeElement = this.getNodeElementForNode(node);
694
- nodeElement.select(mustSetFocus);
450
+ if (!node.element) {
451
+ return this.element;
695
452
  }
453
+
454
+ const top =
455
+ getOffsetTop(node.element) -
456
+ getOffsetTop(this.$el.get(0) as HTMLElement);
457
+
458
+ this.scrollHandler.scrollToY(top);
459
+
460
+ return this.element;
696
461
  }
697
462
 
698
- // Set initial state, either by restoring the state or auto-opening nodes
699
- // result: must load nodes on demand?
700
- private setInitialState(): boolean {
701
- const restoreState = (): [boolean, boolean] => {
702
- // result: is state restored, must load on demand?
703
- if (!this.options.saveState) {
704
- return [false, false];
705
- } else {
706
- const state = this.saveStateHandler.getStateFromStorage();
463
+ public selectNode(
464
+ node: Node | null,
465
+ optionsParam?: SelectNodeOptions,
466
+ ): JQuery {
467
+ this.doSelectNode(node, optionsParam);
468
+ return this.element;
469
+ }
707
470
 
708
- if (!state) {
709
- return [false, false];
710
- } else {
711
- const mustLoadOnDemand =
712
- this.saveStateHandler.setInitialState(state);
471
+ public setOption(option: string, value: unknown): JQuery {
472
+ (this.options as unknown as Record<string, unknown>)[option] = value;
473
+ return this.element;
474
+ }
713
475
 
714
- // return true: the state is restored
715
- return [true, mustLoadOnDemand];
716
- }
717
- }
718
- };
476
+ public setState(state?: SavedState): JQuery {
477
+ if (state) {
478
+ this.saveStateHandler.setInitialState(state);
479
+ this.refreshElements(null);
480
+ }
719
481
 
720
- const autoOpenNodes = (): boolean => {
721
- // result: must load on demand?
722
- if (this.options.autoOpen === false) {
723
- return false;
724
- }
482
+ return this.element;
483
+ }
725
484
 
726
- const maxLevel = this.getAutoOpenMaxLevel();
727
- let mustLoadOnDemand = false;
485
+ public toggle(node?: Node, slideParam: boolean | null = null): JQuery {
486
+ if (!node) {
487
+ throw Error(NODE_PARAM_IS_EMPTY);
488
+ }
728
489
 
729
- this.tree.iterate((node: Node, level: number) => {
730
- if (node.load_on_demand) {
731
- mustLoadOnDemand = true;
732
- return false;
733
- } else if (!node.hasChildren()) {
734
- return false;
735
- } else {
736
- node.is_open = true;
737
- return level !== maxLevel;
738
- }
739
- });
490
+ const slide = slideParam ?? this.options.slide;
740
491
 
741
- return mustLoadOnDemand;
742
- };
492
+ if (node.is_open) {
493
+ this.closeNode(node, slide);
494
+ } else {
495
+ this.openNode(node, slide);
496
+ }
743
497
 
744
- let [isRestored, mustLoadOnDemand] = restoreState(); // eslint-disable-line prefer-const
498
+ return this.element;
499
+ }
745
500
 
746
- if (!isRestored) {
747
- mustLoadOnDemand = autoOpenNodes();
501
+ public toJson(): string {
502
+ return JSON.stringify(this.tree.getData());
503
+ }
504
+
505
+ public updateNode(node?: Node, data?: NodeData): JQuery {
506
+ if (!node) {
507
+ throw Error(NODE_PARAM_IS_EMPTY);
748
508
  }
749
509
 
750
- return mustLoadOnDemand;
751
- }
510
+ if (!data) {
511
+ return this.element;
512
+ }
752
513
 
753
- // Set the initial state for nodes that are loaded on demand
754
- // Call cb_finished when done
755
- private setInitialStateOnDemand(cbFinished: () => void): void {
756
- const restoreState = (): boolean => {
757
- if (!this.options.saveState) {
758
- return false;
759
- } else {
760
- const state = this.saveStateHandler.getStateFromStorage();
514
+ const idIsChanged =
515
+ typeof data === "object" && data.id && data.id !== node.id;
761
516
 
762
- if (!state) {
763
- return false;
764
- } else {
765
- this.saveStateHandler.setInitialStateOnDemand(
766
- state,
767
- cbFinished,
768
- );
517
+ if (idIsChanged) {
518
+ this.tree.removeNodeFromIndex(node);
519
+ }
769
520
 
770
- return true;
771
- }
521
+ node.setData(data);
522
+
523
+ if (idIsChanged) {
524
+ this.tree.addNodeToIndex(node);
525
+ }
526
+
527
+ if (
528
+ typeof data === "object" &&
529
+ data.children &&
530
+ data.children instanceof Array
531
+ ) {
532
+ node.removeChildren();
533
+
534
+ if (data.children.length) {
535
+ node.loadFromData(data.children as Node[]);
772
536
  }
773
- };
537
+ }
774
538
 
775
- const autoOpenNodes = (): void => {
776
- const maxLevel = this.getAutoOpenMaxLevel();
777
- let loadingCount = 0;
539
+ this.refreshElements(node);
778
540
 
779
- const loadAndOpenNode = (node: Node): void => {
780
- loadingCount += 1;
781
- this.openNodeInternal(node, false, () => {
782
- loadingCount -= 1;
783
- openNodes();
784
- });
785
- };
541
+ return this.element;
542
+ }
786
543
 
787
- const openNodes = (): void => {
788
- this.tree.iterate((node: Node, level: number) => {
789
- if (node.load_on_demand) {
790
- if (!node.is_loading) {
791
- loadAndOpenNode(node);
792
- }
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;
793
570
 
794
- return false;
795
- } else {
796
- 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);
797
586
 
798
- return level !== maxLevel;
799
- }
800
- });
587
+ const selectNodeHandler = new SelectNodeHandler({
588
+ getNodeById,
589
+ });
801
590
 
802
- if (loadingCount === 0) {
803
- cbFinished();
804
- }
805
- };
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;
806
600
 
807
- openNodes();
808
- };
601
+ const dataLoader = new DataLoader({
602
+ dataFilter,
603
+ loadData,
604
+ onLoadFailed,
605
+ onLoading,
606
+ treeElement,
607
+ triggerEvent,
608
+ });
809
609
 
810
- if (!restoreState()) {
811
- autoOpenNodes();
812
- }
813
- }
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
+ });
814
622
 
815
- private triggerEvent(
816
- eventName: string,
817
- values?: Record<string, unknown>,
818
- ): JQuery.Event {
819
- const event = jQuery.Event(eventName, values);
820
- this.element.trigger(event);
821
- return event;
822
- }
623
+ const scrollHandler = new ScrollHandler({
624
+ refreshHitAreas,
625
+ treeElement,
626
+ });
823
627
 
824
- public addNodeAfter(
825
- newNodeInfo: NodeData,
826
- existingNode: Node,
827
- ): Node | null {
828
- const newNode = existingNode.addAfter(newNodeInfo);
628
+ const getScrollLeft = scrollHandler.getScrollLeft.bind(scrollHandler);
829
629
 
830
- if (newNode) {
831
- this.refreshElements(existingNode.parent);
832
- }
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
+ });
833
648
 
834
- return newNode;
835
- }
649
+ const keyHandler = new KeyHandler({
650
+ closeNode,
651
+ getSelectedNode,
652
+ isFocusOnTree,
653
+ keyboardSupport,
654
+ openNode,
655
+ selectNode,
656
+ });
836
657
 
837
- public addNodeBefore(
838
- newNodeInfo: NodeData,
839
- existingNode?: Node,
840
- ): Node | null {
841
- if (!existingNode) {
842
- throw Error(PARAM_IS_EMPTY + "existingNode");
843
- }
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
+ });
844
672
 
845
- 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
+ }
846
702
 
847
- if (newNode) {
848
- this.refreshElements(existingNode.parent);
849
- }
703
+ private containsElement(element: HTMLElement): boolean {
704
+ const node = this.getNode(element);
850
705
 
851
- return newNode;
706
+ return node != null && node.tree === this.tree;
852
707
  }
853
708
 
854
- public addParentNode(
855
- newNodeInfo: NodeData,
856
- existingNode?: Node,
857
- ): Node | null {
858
- if (!existingNode) {
859
- throw Error(PARAM_IS_EMPTY + "existingNode");
860
- }
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);
861
718
 
862
- 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
+ }
863
729
 
864
- if (newNode) {
865
- this.refreshElements(newNode.parent);
866
- }
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;
867
736
 
868
- return newNode;
737
+ return new NodeElement({
738
+ getScrollLeft,
739
+ node,
740
+ tabIndex,
741
+ treeElement,
742
+ });
869
743
  }
870
744
 
871
- public addToSelection(node?: Node, mustSetFocus?: boolean): JQuery {
872
- if (!node) {
873
- throw Error(NODE_PARAM_IS_EMPTY);
745
+ private deselectCurrentNode(): void {
746
+ const node = this.getSelectedNode();
747
+ if (node) {
748
+ this.removeFromSelection(node);
874
749
  }
750
+ }
875
751
 
876
- this.selectNodeHandler.addToSelection(node);
877
- 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
+ }
878
759
 
879
- 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
+ }
880
768
 
881
- this.saveState();
769
+ if (this.isDragging()) {
770
+ this.dndHandler.refresh();
771
+ }
772
+ }
882
773
 
883
- return this.element;
774
+ this.triggerEvent("tree.load_data", {
775
+ parent_node: parentNode,
776
+ tree_data: data,
777
+ });
884
778
  }
885
779
 
886
- public appendNode(newNodeInfo: NodeData, parentNodeParam?: Node): Node {
887
- const parentNode = parentNodeParam ?? this.tree;
888
-
889
- const node = parentNode.append(newNodeInfo);
890
-
891
- 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);
892
786
 
893
- return node;
787
+ this.dataLoader.loadFromUrl(urlInfo, parentNode, onFinished);
894
788
  }
895
789
 
896
- 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
+
897
800
  if (!node) {
898
- throw Error(NODE_PARAM_IS_EMPTY);
801
+ // Called with empty node -> deselect current node
802
+ this.deselectCurrentNode();
803
+ saveState();
804
+ return;
899
805
  }
806
+ const defaultOptions = { mustSetFocus: true, mustToggle: true };
807
+ const selectOptions = { ...defaultOptions, ...(optionsParam ?? {}) };
900
808
 
901
- 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
+ };
902
819
 
903
- if (node.isFolder() || node.isEmptyFolder) {
904
- this.createFolderElement(node).close(
905
- slide,
906
- this.options.animationSpeed,
907
- );
820
+ if (!canSelect()) {
821
+ return;
822
+ }
908
823
 
909
- 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);
910
842
  }
911
843
 
912
- return this.element;
844
+ saveState();
913
845
  }
914
846
 
915
- public deinit(): void {
916
- this.element.empty();
917
- 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
+ }
918
858
 
919
- this.keyHandler.deinit();
920
- 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);
921
862
 
922
- this.tree = new Node({}, true);
863
+ const getUrlFromString = (url: string): JQuery.AjaxSettings => {
864
+ const urlInfo: JQuery.AjaxSettings = { url };
923
865
 
924
- super.deinit();
925
- }
866
+ setUrlInfoData(urlInfo);
926
867
 
927
- public getNodeByCallback(callback: (node: Node) => boolean): Node | null {
928
- return this.tree.getNodeByCallback(callback);
929
- }
868
+ return urlInfo;
869
+ };
930
870
 
931
- public getNodeByHtmlElement(
932
- inputElement: HTMLElement | JQuery,
933
- ): Node | null {
934
- const element =
935
- inputElement instanceof HTMLElement
936
- ? inputElement
937
- : 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
+ };
938
885
 
939
- 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 {
940
894
  return null;
941
895
  }
942
-
943
- return this.getNode(element);
944
896
  }
945
897
 
946
- public getNodeById(nodeId: NodeId): Node | null {
947
- 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
+ }
948
906
  }
949
907
 
950
- public getNodeByName(name: string): Node | null {
951
- return this.tree.getNodeByName(name);
952
- }
908
+ private getNode(element: HTMLElement): Node | null {
909
+ const liElement = element.closest("li.jqtree_common");
953
910
 
954
- public getNodeByNameMustExist(name: string): Node {
955
- return this.tree.getNodeByNameMustExist(name);
911
+ if (liElement) {
912
+ return jQuery(liElement).data("node") as Node;
913
+ } else {
914
+ return null;
915
+ }
956
916
  }
957
917
 
958
- public getNodesByProperty(key: string, value: unknown): Node[] {
959
- 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
+ }
960
925
  }
961
926
 
962
- public getSelectedNode(): false | Node {
963
- 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
+ }
964
933
  }
965
934
 
966
- public getSelectedNodes(): Node[] {
967
- 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
+ }
968
941
  }
969
942
 
970
- public getState(): null | SavedState {
971
- return this.saveStateHandler.getState();
972
- }
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;
973
948
 
974
- public getStateFromStorage(): null | SavedState {
975
- 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
+ }
976
959
  }
977
960
 
978
- public getTree(): Node {
979
- return this.tree;
980
- }
961
+ private initData(): void {
962
+ if (this.options.data) {
963
+ this.doLoadData(this.options.data, null);
964
+ } else {
965
+ const dataUrl = this.getDataUrlInfo(null);
981
966
 
982
- public getVersion(): string {
983
- return __version__;
967
+ if (dataUrl) {
968
+ this.doLoadDataFromUrl(null, null, null);
969
+ } else {
970
+ this.doLoadData([], null);
971
+ }
972
+ }
984
973
  }
985
974
 
986
- public init(): void {
987
- super.init();
988
-
989
- this.element = this.$el;
990
- 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
+ };
991
982
 
992
- this.options.rtl = this.getRtlOption();
983
+ this.tree = new this.options.nodeClass(
984
+ null,
985
+ true,
986
+ this.options.nodeClass,
987
+ );
993
988
 
994
- if (this.options.closedIcon == null) {
995
- this.options.closedIcon = this.getDefaultClosedIcon();
996
- }
989
+ this.selectNodeHandler.clear();
997
990
 
998
- this.connectHandlers();
991
+ this.tree.loadFromData(data);
999
992
 
1000
- this.initData();
1001
- }
993
+ const mustLoadOnDemand = this.setInitialState();
1002
994
 
1003
- public isDragging(): boolean {
1004
- return this.dndHandler.isDragging;
1005
- }
995
+ this.refreshElements(null);
1006
996
 
1007
- public isNodeSelected(node?: Node): boolean {
1008
- if (!node) {
1009
- 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);
1010
1002
  }
1011
-
1012
- return this.selectNodeHandler.isNodeSelected(node);
1013
1003
  }
1014
1004
 
1015
- public loadData(data: NodeData[], parentNode: Node | null): JQuery {
1016
- this.doLoadData(data, parentNode);
1017
- 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
+ );
1018
1013
  }
1019
1014
 
1020
- /*
1021
- signatures:
1022
- - loadDataFromUrl(url, parent_node=null, on_finished=null)
1023
- loadDataFromUrl('/my_data');
1024
- loadDataFromUrl('/my_data', node1);
1025
- loadDataFromUrl('/my_data', node1, function() { console.log('finished'); });
1026
- loadDataFromUrl('/my_data', null, function() { console.log('finished'); });
1015
+ private isSelectedNodeInSubtree(subtree: Node): boolean {
1016
+ const selectedNode = this.getSelectedNode();
1027
1017
 
1028
- - loadDataFromUrl(parent_node=null, on_finished=null)
1029
- loadDataFromUrl();
1030
- loadDataFromUrl(node1);
1031
- loadDataFromUrl(null, function() { console.log('finished'); });
1032
- loadDataFromUrl(node1, function() { console.log('finished'); });
1033
- */
1034
- public loadDataFromUrl(
1035
- param1: Node | null | string,
1036
- param2?: HandleFinishedLoading | Node | null,
1037
- param3?: HandleFinishedLoading,
1038
- ): JQuery {
1039
- if (typeof param1 === "string") {
1040
- // first parameter is url
1041
- this.doLoadDataFromUrl(
1042
- param1,
1043
- param2 as Node | null,
1044
- param3 ?? null,
1045
- );
1018
+ if (!selectedNode) {
1019
+ return false;
1046
1020
  } else {
1047
- // first parameter is not url
1048
- this.doLoadDataFromUrl(
1049
- null,
1050
- param1,
1051
- param2 as HandleFinishedLoading | null,
1052
- );
1021
+ return subtree === selectedNode || subtree.isParentOf(selectedNode);
1053
1022
  }
1054
-
1055
- return this.element;
1056
1023
  }
1057
1024
 
1058
- public moveDown(): JQuery {
1059
- const selectedNode = this.getSelectedNode();
1060
- if (selectedNode) {
1061
- this.keyHandler.moveDown(selectedNode);
1062
- }
1025
+ private loadFolderOnDemand(
1026
+ node: Node,
1027
+ slide = true,
1028
+ onFinished?: OnFinishOpenNode,
1029
+ ): void {
1030
+ node.is_loading = true;
1063
1031
 
1064
- return this.element;
1032
+ this.doLoadDataFromUrl(null, node, () => {
1033
+ this.openNodeInternal(node, slide, onFinished);
1034
+ });
1065
1035
  }
1066
1036
 
1067
- public moveNode(node?: Node, targetNode?: Node, position?: string): JQuery {
1068
- if (!node) {
1069
- throw Error(NODE_PARAM_IS_EMPTY);
1070
- }
1037
+ private loadSubtree(data: NodeData[], parentNode: Node): void {
1038
+ parentNode.loadFromData(data);
1071
1039
 
1072
- if (!targetNode) {
1073
- throw Error(PARAM_IS_EMPTY + "targetNode");
1074
- }
1040
+ parentNode.load_on_demand = false;
1041
+ parentNode.is_loading = false;
1075
1042
 
1076
- if (!position) {
1077
- 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;
1078
1051
  }
1052
+ }
1079
1053
 
1080
- const positionIndex = getPosition(position);
1054
+ private mouseDrag(positionInfo: PositionInfo): boolean {
1055
+ if (this.options.dragAndDrop) {
1056
+ const result = this.dndHandler.mouseDrag(positionInfo);
1081
1057
 
1082
- if (positionIndex !== undefined) {
1083
- this.tree.moveNode(node, targetNode, positionIndex);
1084
- this.refreshElements(null);
1058
+ this.scrollHandler.checkScrolling(positionInfo);
1059
+ return result;
1060
+ } else {
1061
+ return false;
1085
1062
  }
1086
-
1087
- return this.element;
1088
1063
  }
1089
1064
 
1090
- public moveUp(): JQuery {
1091
- const selectedNode = this.getSelectedNode();
1092
- if (selectedNode) {
1093
- 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;
1094
1070
  }
1095
-
1096
- return this.element;
1097
1071
  }
1098
1072
 
1099
- public openNode(
1100
- node?: Node,
1101
- param1?: boolean | OnFinishOpenNode,
1102
- param2?: OnFinishOpenNode,
1103
- ): JQuery {
1104
- if (!node) {
1105
- 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;
1106
1079
  }
1107
-
1108
- const parseParams = (): [boolean, OnFinishOpenNode | undefined] => {
1109
- let onFinished: null | OnFinishOpenNode;
1110
- let slide: boolean | null;
1111
-
1112
- if (isFunction(param1)) {
1113
- onFinished = param1 as OnFinishOpenNode;
1114
- slide = null;
1115
- } else {
1116
- slide = param1 as boolean;
1117
- onFinished = param2 as OnFinishOpenNode;
1118
- }
1119
-
1120
- if (slide == null) {
1121
- slide = this.options.slide;
1122
- }
1123
-
1124
- return [slide, onFinished];
1125
- };
1126
-
1127
- const [slide, onFinished] = parseParams();
1128
-
1129
- this.openNodeInternal(node, slide, onFinished);
1130
- return this.element;
1131
1080
  }
1132
1081
 
1133
- public prependNode(newNodeInfo: NodeData, parentNodeParam?: Node): Node {
1134
- 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
+ }
1135
1095
 
1136
- const node = parentNode.prepend(newNodeInfo);
1096
+ const folderElement = this.createFolderElement(_node);
1097
+ folderElement.open(
1098
+ _onFinished,
1099
+ _slide,
1100
+ this.options.animationSpeed,
1101
+ );
1102
+ };
1137
1103
 
1138
- 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;
1139
1109
 
1140
- return node;
1141
- }
1110
+ while (parent) {
1111
+ // nb: do not open root element
1112
+ if (parent.parent) {
1113
+ doOpenNode(parent, false);
1114
+ }
1115
+ parent = parent.parent;
1116
+ }
1142
1117
 
1143
- public refresh(): JQuery {
1144
- this.refreshElements(null);
1145
- return this.element;
1118
+ doOpenNode(node, slide, onFinished);
1119
+ this.saveState();
1120
+ }
1121
+ }
1146
1122
  }
1147
1123
 
1148
- public refreshHitAreas(): JQuery {
1149
- this.dndHandler.refresh();
1150
- return this.element;
1151
- }
1124
+ private openParents(node: Node) {
1125
+ const parent = node.parent;
1152
1126
 
1153
- public reload(onFinished: HandleFinishedLoading | null): JQuery {
1154
- this.doLoadDataFromUrl(null, null, onFinished);
1155
- return this.element;
1127
+ if (parent?.parent && !parent.is_open) {
1128
+ this.openNode(parent, false);
1129
+ }
1156
1130
  }
1157
1131
 
1158
- public removeFromSelection(node?: Node): JQuery {
1159
- if (!node) {
1160
- throw Error(NODE_PARAM_IS_EMPTY);
1161
- }
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;
1162
1141
 
1163
- this.selectNodeHandler.removeFromSelection(node);
1142
+ this.renderer.render(fromNode);
1164
1143
 
1165
- this.getNodeElementForNode(node).deselect();
1166
- this.saveState();
1144
+ if (mustSelect) {
1145
+ this.selectCurrentNode(mustSetFocus);
1146
+ }
1167
1147
 
1168
- return this.element;
1148
+ this.triggerEvent("tree.refresh");
1169
1149
  }
1170
1150
 
1171
- public removeNode(node?: Node): JQuery {
1172
- if (!node) {
1173
- throw Error(NODE_PARAM_IS_EMPTY);
1174
- }
1175
-
1176
- if (!node.parent) {
1177
- throw Error("Node has no parent");
1151
+ private saveState(): void {
1152
+ if (this.options.saveState) {
1153
+ this.saveStateHandler.saveState();
1178
1154
  }
1179
-
1180
- this.selectNodeHandler.removeFromSelection(node, true); // including children
1181
-
1182
- const parent = node.parent;
1183
- node.remove();
1184
- this.refreshElements(parent);
1185
-
1186
- return this.element;
1187
1155
  }
1188
1156
 
1189
- public scrollToNode(node?: Node): JQuery {
1190
- if (!node) {
1191
- 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);
1192
1162
  }
1163
+ }
1193
1164
 
1194
- if (!node.element) {
1195
- return this.element;
1196
- }
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();
1197
1174
 
1198
- const top =
1199
- getOffsetTop(node.element) -
1200
- getOffsetTop(this.$el.get(0) as HTMLElement);
1175
+ if (!state) {
1176
+ return [false, false];
1177
+ } else {
1178
+ const mustLoadOnDemand =
1179
+ this.saveStateHandler.setInitialState(state);
1201
1180
 
1202
- this.scrollHandler.scrollToY(top);
1181
+ // return true: the state is restored
1182
+ return [true, mustLoadOnDemand];
1183
+ }
1184
+ }
1185
+ };
1203
1186
 
1204
- return this.element;
1205
- }
1187
+ const autoOpenNodes = (): boolean => {
1188
+ // result: must load on demand?
1189
+ if (this.options.autoOpen === false) {
1190
+ return false;
1191
+ }
1206
1192
 
1207
- public selectNode(
1208
- node: Node | null,
1209
- optionsParam?: SelectNodeOptions,
1210
- ): JQuery {
1211
- this.doSelectNode(node, optionsParam);
1212
- return this.element;
1213
- }
1193
+ const maxLevel = this.getAutoOpenMaxLevel();
1194
+ let mustLoadOnDemand = false;
1214
1195
 
1215
- public setOption(option: string, value: unknown): JQuery {
1216
- (this.options as unknown as Record<string, unknown>)[option] = value;
1217
- return this.element;
1218
- }
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
+ });
1219
1207
 
1220
- public setState(state?: SavedState): JQuery {
1221
- if (state) {
1222
- this.saveStateHandler.setInitialState(state);
1223
- this.refreshElements(null);
1224
- }
1208
+ return mustLoadOnDemand;
1209
+ };
1225
1210
 
1226
- return this.element;
1227
- }
1211
+ let [isRestored, mustLoadOnDemand] = restoreState(); // eslint-disable-line prefer-const
1228
1212
 
1229
- public toggle(node?: Node, slideParam: boolean | null = null): JQuery {
1230
- if (!node) {
1231
- throw Error(NODE_PARAM_IS_EMPTY);
1213
+ if (!isRestored) {
1214
+ mustLoadOnDemand = autoOpenNodes();
1232
1215
  }
1233
1216
 
1234
- const slide = slideParam ?? this.options.slide;
1217
+ return mustLoadOnDemand;
1218
+ }
1235
1219
 
1236
- if (node.is_open) {
1237
- this.closeNode(node, slide);
1238
- } else {
1239
- this.openNode(node, slide);
1240
- }
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();
1241
1228
 
1242
- return this.element;
1243
- }
1229
+ if (!state) {
1230
+ return false;
1231
+ } else {
1232
+ this.saveStateHandler.setInitialStateOnDemand(
1233
+ state,
1234
+ cbFinished,
1235
+ );
1244
1236
 
1245
- public toJson(): string {
1246
- return JSON.stringify(this.tree.getData());
1247
- }
1237
+ return true;
1238
+ }
1239
+ }
1240
+ };
1248
1241
 
1249
- public updateNode(node?: Node, data?: NodeData): JQuery {
1250
- if (!node) {
1251
- throw Error(NODE_PARAM_IS_EMPTY);
1252
- }
1242
+ const autoOpenNodes = (): void => {
1243
+ const maxLevel = this.getAutoOpenMaxLevel();
1244
+ let loadingCount = 0;
1253
1245
 
1254
- if (!data) {
1255
- return this.element;
1256
- }
1246
+ const loadAndOpenNode = (node: Node): void => {
1247
+ loadingCount += 1;
1248
+ this.openNodeInternal(node, false, () => {
1249
+ loadingCount -= 1;
1250
+ openNodes();
1251
+ });
1252
+ };
1257
1253
 
1258
- const idIsChanged =
1259
- 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
+ }
1260
1260
 
1261
- if (idIsChanged) {
1262
- this.tree.removeNodeFromIndex(node);
1263
- }
1261
+ return false;
1262
+ } else {
1263
+ this.openNodeInternal(node, false);
1264
1264
 
1265
- node.setData(data);
1265
+ return level !== maxLevel;
1266
+ }
1267
+ });
1266
1268
 
1267
- if (idIsChanged) {
1268
- this.tree.addNodeToIndex(node);
1269
- }
1269
+ if (loadingCount === 0) {
1270
+ cbFinished();
1271
+ }
1272
+ };
1270
1273
 
1271
- if (
1272
- typeof data === "object" &&
1273
- data.children &&
1274
- data.children instanceof Array
1275
- ) {
1276
- node.removeChildren();
1274
+ openNodes();
1275
+ };
1277
1276
 
1278
- if (data.children.length) {
1279
- node.loadFromData(data.children as Node[]);
1280
- }
1277
+ if (!restoreState()) {
1278
+ autoOpenNodes();
1281
1279
  }
1280
+ }
1282
1281
 
1283
- this.refreshElements(node);
1284
-
1285
- 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;
1286
1289
  }
1287
1290
  }
1288
1291