wunderbaum 0.0.1-0

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.
@@ -0,0 +1,336 @@
1
+ /*!
2
+ * Wunderbaum - ext-dnd
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+ import * as util from "./util";
7
+ import { EventCallbackType, onEvent } from "./util";
8
+ import { Wunderbaum } from "./wunderbaum";
9
+ import { WunderbaumExtension } from "./wb_extension_base";
10
+ import { WunderbaumNode } from "./wb_node";
11
+ import { ROW_HEIGHT } from "./common";
12
+
13
+ const nodeMimeType = "application/x-fancytree-node";
14
+ export type DropRegionType = "over" | "before" | "after";
15
+ type DropRegionTypeSet = Set<DropRegionType>;
16
+ // type AllowedDropRegionType =
17
+ // | "after"
18
+ // | "afterBefore"
19
+ // // | "afterBeforeOver" // == all == true
20
+ // | "afterOver"
21
+ // | "all" // == true
22
+ // | "before"
23
+ // | "beforeOver"
24
+ // | "none" // == false == "" == null
25
+ // | "over"; // == "child"
26
+
27
+ export class DndExtension extends WunderbaumExtension {
28
+ // public dropMarkerElem?: HTMLElement;
29
+ protected srcNode: WunderbaumNode | null = null;
30
+ protected lastTargetNode: WunderbaumNode | null = null;
31
+ protected lastEnterStamp = 0;
32
+ protected lastAllowedDropRegions: DropRegionTypeSet | null = null;
33
+ protected lastDropEffect: string | null = null;
34
+ protected lastDropRegion: DropRegionType | false = false;
35
+
36
+ constructor(tree: Wunderbaum) {
37
+ super(tree, "dnd", {
38
+ autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
39
+ // dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after"
40
+ // dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
41
+ // #1021 `document.body` is not available yet
42
+ // dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root)
43
+ multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed
44
+ effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event)
45
+ // dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string.
46
+ dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver).
47
+ preventForeignNodes: false, // Prevent dropping nodes from different Wunderbaum trees
48
+ preventLazyParents: true, // Prevent dropping items on unloaded lazy Wunderbaum tree nodes
49
+ preventNonNodes: false, // Prevent dropping items other than Wunderbaum tree nodes
50
+ preventRecursion: true, // Prevent dropping nodes on own descendants
51
+ preventSameParent: false, // Prevent dropping nodes under same direct parent
52
+ preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. (move only)
53
+ scroll: true, // Enable auto-scrolling while dragging
54
+ scrollSensitivity: 20, // Active top/bottom margin in pixel
55
+ scrollSpeed: 5, // Pixel per event
56
+ // setTextTypeJson: false, // Allow dragging of nodes to different IE windows
57
+ sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38
58
+ // Events (drag support)
59
+ dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag
60
+ dragDrag: null, // Callback(sourceNode, data)
61
+ dragEnd: null, // Callback(sourceNode, data)
62
+ // Events (drop support)
63
+ dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop
64
+ dragOver: null, // Callback(targetNode, data)
65
+ dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand
66
+ dragDrop: null, // Callback(targetNode, data)
67
+ dragLeave: null, // Callback(targetNode, data)
68
+ });
69
+ }
70
+
71
+ init() {
72
+ super.init();
73
+
74
+ // Store the current scroll parent, which may be the tree
75
+ // container, any enclosing div, or the document.
76
+ // #761: scrollParent() always needs a container child
77
+ // $temp = $("<span>").appendTo(this.$container);
78
+ // this.$scrollParent = $temp.scrollParent();
79
+ // $temp.remove();
80
+ const tree = this.tree;
81
+ const dndOpts = tree.options.dnd;
82
+
83
+ // Enable drag support if dragStart() is specified:
84
+ if (dndOpts.dragStart) {
85
+ onEvent(
86
+ tree.element,
87
+ "dragstart drag dragend",
88
+ (<EventCallbackType>this.onDragEvent).bind(this)
89
+ );
90
+ }
91
+ // Enable drop support if dragEnter() is specified:
92
+ if (dndOpts.dragEnter) {
93
+ onEvent(
94
+ tree.element,
95
+ "dragenter dragover dragleave drop",
96
+ (<EventCallbackType>this.onDropEvent).bind(this)
97
+ );
98
+ }
99
+ }
100
+
101
+ /** Cleanup classes after target node is no longer hovered. */
102
+ protected _leaveNode(): void {
103
+ // We remove the marker on dragenter from the previous target:
104
+ const ltn = this.lastTargetNode;
105
+ this.lastEnterStamp = 0;
106
+ if (ltn) {
107
+ ltn.removeClass(
108
+ "wb-drop-target wb-drop-over wb-drop-after wb-drop-before"
109
+ );
110
+ this.lastTargetNode = null;
111
+ }
112
+ }
113
+
114
+ /** */
115
+ protected unifyDragover(res: any): DropRegionTypeSet | false {
116
+ if (res === false) {
117
+ return false;
118
+ } else if (res instanceof Set) {
119
+ return res.size > 0 ? res : false;
120
+ } else if (res === true) {
121
+ return new Set<DropRegionType>(["over", "before", "after"]);
122
+ } else if (typeof res === "string" || util.isArray(res)) {
123
+ res = <DropRegionTypeSet>util.toSet(res);
124
+ return res.size > 0 ? res : false;
125
+ }
126
+ throw new Error("Unsupported drop region definition: " + res);
127
+ }
128
+
129
+ /** */
130
+ protected _calcDropRegion(
131
+ e: DragEvent,
132
+ allowed: DropRegionTypeSet | null
133
+ ): DropRegionType | false {
134
+ const dy = e.offsetY;
135
+
136
+ if (!allowed) {
137
+ return false;
138
+ } else if (allowed.size === 3) {
139
+ return dy < 0.25 * ROW_HEIGHT
140
+ ? "before"
141
+ : dy > 0.75 * ROW_HEIGHT
142
+ ? "after"
143
+ : "over";
144
+ } else if (allowed.size === 1 && allowed.has("over")) {
145
+ return "over";
146
+ } else {
147
+ // Only 'before' and 'after':
148
+ return dy > ROW_HEIGHT / 2 ? "after" : "before";
149
+ }
150
+ return "over";
151
+ }
152
+
153
+ /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
154
+ protected autoScroll(event: DragEvent): number {
155
+ let tree = this.tree,
156
+ dndOpts = tree.options.dnd,
157
+ sp = tree.scrollContainer,
158
+ sensitivity = dndOpts.scrollSensitivity,
159
+ speed = dndOpts.scrollSpeed,
160
+ scrolled = 0;
161
+
162
+ const scrollTop = sp.offsetTop;
163
+ if (scrollTop + sp.offsetHeight - event.pageY < sensitivity) {
164
+ const delta = sp.scrollHeight - sp.clientHeight - scrollTop;
165
+ if (delta > 0) {
166
+ sp.scrollTop = scrolled = scrollTop + speed;
167
+ }
168
+ } else if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
169
+ sp.scrollTop = scrolled = scrollTop - speed;
170
+ }
171
+ // if (scrolled) {
172
+ // tree.logDebug("autoScroll: " + scrolled + "px");
173
+ // }
174
+ return scrolled;
175
+ }
176
+
177
+ protected onDragEvent(e: DragEvent) {
178
+ // const tree = this.tree;
179
+ const dndOpts = this.treeOpts.dnd;
180
+ const srcNode = Wunderbaum.getNode(e);
181
+
182
+ if (!srcNode) {
183
+ return;
184
+ }
185
+ if (e.type !== "drag") {
186
+ this.tree.logDebug("onDragEvent." + e.type + ", srcNode: " + srcNode, e);
187
+ }
188
+
189
+ // --- dragstart ---
190
+ if (e.type === "dragstart") {
191
+ // Set a default definition of allowed effects
192
+ e.dataTransfer!.effectAllowed = dndOpts.effectAllowed; //"copyMove"; // "all";
193
+ // Let user cancel the drag operation, override effectAllowed, etc.:
194
+ const res = srcNode._callEvent("dnd.dragStart", { event: e });
195
+ if (!res) {
196
+ e.preventDefault();
197
+ return false;
198
+ }
199
+ let nodeData = srcNode.toDict(true, (n: any) => {
200
+ // We don't want to re-use the key on drop:
201
+ n._org_key = n.key;
202
+ delete n.key;
203
+ });
204
+ nodeData.treeId = srcNode.tree.id;
205
+
206
+ const json = JSON.stringify(nodeData);
207
+ e.dataTransfer!.setData(nodeMimeType, json);
208
+ // e.dataTransfer!.setData("text/html", $(node.span).html());
209
+ e.dataTransfer!.setData("text/plain", srcNode.title);
210
+ this.srcNode = srcNode;
211
+ setTimeout(() => {
212
+ // Decouple this call, so the CSS is applied to the node, but not to
213
+ // the system generated drag image
214
+ srcNode.addClass("wb-drag-source");
215
+ }, 0);
216
+
217
+ // --- drag ---
218
+ } else if (e.type === "drag") {
219
+ // This event occurs very often...
220
+ // --- dragend ---
221
+ } else if (e.type === "dragend") {
222
+ srcNode.removeClass("wb-drag-source");
223
+ this.srcNode = null;
224
+ if (this.lastTargetNode) {
225
+ this._leaveNode();
226
+ }
227
+ }
228
+ return true;
229
+ }
230
+
231
+ protected onDropEvent(e: DragEvent) {
232
+ // const isLink = event.dataTransfer.types.includes("text/uri-list");
233
+ const targetNode = Wunderbaum.getNode(e)!;
234
+ const dndOpts = this.treeOpts.dnd;
235
+ const dt = e.dataTransfer!;
236
+
237
+ if (!targetNode) {
238
+ this._leaveNode();
239
+ return;
240
+ }
241
+ if (e.type !== "dragover") {
242
+ this.tree.logDebug(
243
+ "onDropEvent." +
244
+ e.type +
245
+ " targetNode: " +
246
+ targetNode +
247
+ ", ea: " +
248
+ dt?.effectAllowed +
249
+ ", de: " +
250
+ dt?.dropEffect,
251
+ ", cy: " + e.offsetY,
252
+ ", r: " + this._calcDropRegion(e, this.lastAllowedDropRegions),
253
+ e
254
+ );
255
+ }
256
+
257
+ // --- dragenter ---
258
+ if (e.type === "dragenter") {
259
+ this.lastAllowedDropRegions = null;
260
+ // `dragleave` is not reliable with event delegation, so we generate it
261
+ // from dragenter:
262
+ if (this.lastTargetNode && this.lastTargetNode !== targetNode) {
263
+ this._leaveNode();
264
+ }
265
+ this.lastTargetNode = targetNode;
266
+ this.lastEnterStamp = Date.now();
267
+
268
+ if (
269
+ // Don't allow void operation ('drop on self')
270
+ (dndOpts.preventVoidMoves && targetNode === this.srcNode) ||
271
+ targetNode.isStatusNode()
272
+ ) {
273
+ dt.dropEffect = "none";
274
+ return true; // Prevent drop operation
275
+ }
276
+ // User may return a set of regions (or `false` to prevent drop)
277
+ let regionSet = targetNode._callEvent("dnd.dragEnter", { event: e });
278
+ //
279
+ regionSet = this.unifyDragover(regionSet);
280
+ if (!regionSet) {
281
+ dt.dropEffect = "none";
282
+ return true; // Prevent drop operation
283
+ }
284
+ this.lastAllowedDropRegions = regionSet;
285
+ this.lastDropEffect = dt.dropEffect;
286
+ targetNode.addClass("wb-drop-target");
287
+
288
+ e.preventDefault(); // Allow drop (Drop operation is denied by default)
289
+ return false;
290
+
291
+ // --- dragover ---
292
+ } else if (e.type === "dragover") {
293
+ this.autoScroll(e);
294
+
295
+ const region = this._calcDropRegion(e, this.lastAllowedDropRegions);
296
+
297
+ this.lastDropRegion = region;
298
+
299
+ if (
300
+ dndOpts.autoExpandMS > 0 &&
301
+ targetNode.isExpandable(true) &&
302
+ !targetNode._isLoading &&
303
+ Date.now() - this.lastEnterStamp > dndOpts.autoExpandMS &&
304
+ targetNode._callEvent("dnd.dragExpand", { event: e }) !== false
305
+ ) {
306
+ targetNode.setExpanded();
307
+ }
308
+
309
+ if (!region) {
310
+ return; // We already rejected in dragenter
311
+ }
312
+ targetNode.toggleClass("wb-drop-over", region === "over");
313
+ targetNode.toggleClass("wb-drop-before", region === "before");
314
+ targetNode.toggleClass("wb-drop-after", region === "after");
315
+ // console.log("dragover", e);
316
+
317
+ // dt.dropEffect = this.lastDropEffect!;
318
+ e.preventDefault(); // Allow drop (Drop operation is denied by default)
319
+ return false;
320
+
321
+ // --- dragleave ---
322
+ } else if (e.type === "dragleave") {
323
+ // NOTE: we cannot trust this event, since it is always fired,
324
+ // Instead we remove the marker on dragenter
325
+ // --- drop ---
326
+ } else if (e.type === "drop") {
327
+ e.stopPropagation(); // prevent browser from opening links?
328
+ this._leaveNode();
329
+ targetNode._callEvent("dnd.drop", {
330
+ event: e,
331
+ region: this.lastDropRegion,
332
+ sourceNode: this.srcNode,
333
+ });
334
+ }
335
+ }
336
+ }
@@ -0,0 +1,340 @@
1
+ /*!
2
+ * Wunderbaum - ext-edit
3
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
+ */
6
+
7
+ import { Wunderbaum } from "./wunderbaum";
8
+ import { WunderbaumExtension } from "./wb_extension_base";
9
+ import {
10
+ assert,
11
+ escapeHtml,
12
+ eventToString,
13
+ getValueFromElem,
14
+ isMac,
15
+ isPlainObject,
16
+ onEvent,
17
+ } from "./util";
18
+ import { debounce } from "./debounce";
19
+ import { WunderbaumNode } from "./wb_node";
20
+ import { AddNodeType, NavigationMode } from "./common";
21
+ import { WbNodeData } from "./wb_options";
22
+
23
+ // const START_MARKER = "\uFFF7";
24
+
25
+ export class EditExtension extends WunderbaumExtension {
26
+ protected debouncedOnChange: (e: Event) => void;
27
+ protected curEditNode: WunderbaumNode | null = null;
28
+ protected relatedNode: WunderbaumNode | null = null;
29
+
30
+ constructor(tree: Wunderbaum) {
31
+ super(tree, "edit", {
32
+ debounce: 100,
33
+ minlength: 1,
34
+ maxlength: null,
35
+ trigger: [], //["clickActive", "F2", "macEnter"],
36
+ trim: true,
37
+ select: true,
38
+ slowClickDelay: 1000, // Handle 'clickActive' only if last click is less than this old (0: always)
39
+ validity: true, //"Please enter a title",
40
+ // --- Events ---
41
+ // (note: there is also the `tree.change` event.)
42
+ beforeEdit: null,
43
+ edit: null,
44
+ apply: null,
45
+ });
46
+
47
+ this.debouncedOnChange = debounce(
48
+ this._onChange.bind(this),
49
+ this.getPluginOption("debounce")
50
+ );
51
+ }
52
+
53
+ /*
54
+ * Call an event handler, while marking the current node cell 'dirty'.
55
+ */
56
+ protected _applyChange(
57
+ eventName: string,
58
+ node: WunderbaumNode,
59
+ colElem: HTMLElement,
60
+ extra: any
61
+ ): Promise<any> {
62
+ let res;
63
+
64
+ node.log(`_applyChange(${eventName})`, extra);
65
+
66
+ colElem.classList.add("wb-dirty");
67
+ colElem.classList.remove("wb-error");
68
+ try {
69
+ res = node._callEvent(eventName, extra);
70
+ } catch (err) {
71
+ node.logError(`Error in ${eventName} event handler`, err);
72
+ colElem.classList.add("wb-error");
73
+ colElem.classList.remove("wb-dirty");
74
+ }
75
+ // Convert scalar return value to a resolved promise
76
+ if (!(res instanceof Promise)) {
77
+ res = Promise.resolve(res);
78
+ }
79
+ res
80
+ .catch((err) => {
81
+ node.logError(`Error in ${eventName} event promise`, err);
82
+ colElem.classList.add("wb-error");
83
+ })
84
+ .finally(() => {
85
+ colElem.classList.remove("wb-dirty");
86
+ });
87
+ return res;
88
+ }
89
+
90
+ /*
91
+ * Called for when a control that is embedded in a cell fires a `change` event.
92
+ */
93
+ protected _onChange(e: Event) {
94
+ // let res;
95
+ const info = Wunderbaum.getEventInfo(e);
96
+ const node = info.node!;
97
+ const colElem = <HTMLElement>info.colElem!;
98
+ if (!node || info.colIdx === 0) {
99
+ this.tree.log("Ignored change event for removed element or node title");
100
+ return;
101
+ }
102
+ this._applyChange("change", node, colElem, {
103
+ info: info,
104
+ event: e,
105
+ inputElem: e.target,
106
+ inputValue: Wunderbaum.util.getValueFromElem(e.target as HTMLElement),
107
+ });
108
+ }
109
+
110
+ // handleKey(e:KeyboardEvent):boolean {
111
+ // if(this.tree.cellNavMode )
112
+ // }
113
+
114
+ init() {
115
+ super.init();
116
+
117
+ onEvent(
118
+ this.tree.element,
119
+ "change", //"change input",
120
+ ".contenteditable,input,textarea,select",
121
+ (e) => {
122
+ this.debouncedOnChange(e);
123
+ }
124
+ );
125
+ }
126
+
127
+ /* Called by ext_keynav to pre-process input. */
128
+ _preprocessKeyEvent(data: any): boolean | undefined {
129
+ const event = data.event;
130
+ const eventName = eventToString(event);
131
+ const tree = this.tree;
132
+ const trigger = this.getPluginOption("trigger");
133
+ const inputElem =
134
+ event.target && event.target.closest("input,[contenteditable]");
135
+ // let handled = true;
136
+
137
+ tree.logDebug(`_preprocessKeyEvent: ${eventName}`);
138
+
139
+ // --- Title editing: apply/discard ---
140
+ if (inputElem) {
141
+ //this.isEditingTitle()) {
142
+ switch (eventName) {
143
+ case "Enter":
144
+ this._stopEditTitle(true, { event: event });
145
+ return false;
146
+ case "Escape":
147
+ this._stopEditTitle(false, { event: event });
148
+ return false;
149
+ }
150
+ // If the event target is an input element or `contenteditable="true"`,
151
+ // we ignore it as navigation command
152
+ return false;
153
+ }
154
+ // --- Trigger title editing
155
+ if (tree.navMode === NavigationMode.row || tree.activeColIdx === 0) {
156
+ switch (eventName) {
157
+ case "Enter":
158
+ if (trigger.indexOf("macEnter") >= 0 && isMac) {
159
+ this.startEditTitle();
160
+ return false;
161
+ }
162
+ break;
163
+ case "F2":
164
+ if (trigger.indexOf("F2") >= 0) {
165
+ // tree.setCellMode(NavigationMode.cellEdit);
166
+ this.startEditTitle();
167
+ return false;
168
+ }
169
+ break;
170
+ }
171
+ return true;
172
+ }
173
+ return true;
174
+ }
175
+
176
+ /** Return true if a title is currently being edited. */
177
+ isEditingTitle(node?: WunderbaumNode): boolean {
178
+ return node ? this.curEditNode === node : !!this.curEditNode;
179
+ }
180
+
181
+ /** Start renaming, i.e. replace the title with an embedded `<input>`. */
182
+ startEditTitle(node?: WunderbaumNode | null) {
183
+ node = node ?? this.tree.getActiveNode();
184
+ const validity = this.getPluginOption("validity");
185
+ const select = this.getPluginOption("select");
186
+
187
+ if (!node) {
188
+ return;
189
+ }
190
+ this.tree.logDebug(`startEditTitle(node=${node})`);
191
+ let inputHtml = node._callEvent("edit.beforeEdit");
192
+ if (inputHtml === false) {
193
+ node.logInfo("beforeEdit canceled operation.");
194
+ return;
195
+ }
196
+ // `beforeEdit(e)` may return an input HTML string. Otherwise use a default:
197
+ if (!inputHtml) {
198
+ const title = escapeHtml(node.title);
199
+ inputHtml = `<input type=text class="wb-input-edit" value="${title}" required autocorrect=off>`;
200
+ }
201
+ const titleSpan = node
202
+ .getColElem(0)!
203
+ .querySelector(".wb-title") as HTMLSpanElement;
204
+
205
+ titleSpan.innerHTML = inputHtml;
206
+ const inputElem = titleSpan.firstElementChild as HTMLInputElement;
207
+ if (validity) {
208
+ // Permanently apply input validations (CSS and tooltip)
209
+ inputElem.addEventListener("keydown", (e) => {
210
+ if (!inputElem.reportValidity()) {
211
+ // node?.logInfo(`Invalid input: '${inputElem.value}'`);
212
+ }
213
+ });
214
+ }
215
+ inputElem.focus();
216
+ if (select) {
217
+ inputElem.select();
218
+ }
219
+
220
+ this.curEditNode = node;
221
+ node._callEvent("edit.edit", {
222
+ inputElem: inputElem,
223
+ });
224
+ }
225
+ /**
226
+ *
227
+ * @param apply
228
+ * @returns
229
+ */
230
+ stopEditTitle(apply: boolean) {
231
+ return this._stopEditTitle(apply, {});
232
+ }
233
+ /*
234
+ *
235
+ * @param apply
236
+ * @param opts.canKeepOpen
237
+ */
238
+ _stopEditTitle(apply: boolean, opts: any) {
239
+ const focusElem = document.activeElement as HTMLInputElement;
240
+ let newValue = focusElem ? getValueFromElem(focusElem) : null;
241
+ const node = this.curEditNode;
242
+ const forceClose = !!opts.forceClose;
243
+ const validity = this.getPluginOption("validity");
244
+
245
+ if (newValue && this.getPluginOption("trim")) {
246
+ newValue = newValue.trim();
247
+ }
248
+ if (!node) {
249
+ this.tree.logDebug("stopEditTitle: not in edit mode.");
250
+ return;
251
+ }
252
+ node.logDebug(`stopEditTitle(${apply})`, opts, focusElem, newValue);
253
+
254
+ if (apply && newValue !== null && newValue !== node.title) {
255
+ const colElem = node.getColElem(0)!;
256
+
257
+ this._applyChange("edit.apply", node, colElem, {
258
+ oldValue: node.title,
259
+ newValue: newValue,
260
+ inputElem: focusElem,
261
+ })
262
+ .then((value) => {
263
+ const errMsg = focusElem.validationMessage;
264
+ if (validity && errMsg && value !== false) {
265
+ // Handler called 'inputElem.setCustomValidity()' to signal error
266
+ throw new Error(
267
+ `Edit apply validation failed for "${newValue}": ${errMsg}.`
268
+ );
269
+ }
270
+ // Discard the embedded `<input>`
271
+ // node.logDebug("applyChange:", value, forceClose)
272
+ if (!forceClose && value === false) {
273
+ // Keep open
274
+ return;
275
+ }
276
+ node?.setTitle(newValue);
277
+ this.curEditNode!.render();
278
+ this.curEditNode = null;
279
+ this.relatedNode = null;
280
+ this.tree.setFocus(); // restore focus that was in the input element
281
+ })
282
+ .catch((err) => {
283
+ // this.curEditNode!.render();
284
+ // this.curEditNode = null;
285
+ // this.relatedNode = null;
286
+ });
287
+ // Trigger 'change' event for embedded `<input>`
288
+ // focusElem.blur();
289
+ } else {
290
+ // Discard the embedded `<input>`
291
+ this.curEditNode!.render();
292
+ this.curEditNode = null;
293
+ this.relatedNode = null;
294
+ // We discarded the <input>, so we have to acquire keyboard focus again
295
+ this.tree.setFocus();
296
+ }
297
+ }
298
+ /**
299
+ * Create a new child or sibling node and start edit mode.
300
+ */
301
+ createNode(
302
+ mode: AddNodeType = "after",
303
+ node?: WunderbaumNode | null,
304
+ init?: string | WbNodeData
305
+ ) {
306
+ const tree = this.tree;
307
+ node = node ?? (tree.getActiveNode() as WunderbaumNode);
308
+ assert(node, "No node was passed, or no node is currently active.");
309
+ // const validity = this.getPluginOption("validity");
310
+
311
+ mode = mode || "prependChild";
312
+ if (init == null) {
313
+ init = { title: "" };
314
+ } else if (typeof init === "string") {
315
+ init = { title: init };
316
+ } else {
317
+ assert(isPlainObject(init));
318
+ }
319
+ // Make sure node is expanded (and loaded) in 'child' mode
320
+ if (
321
+ (mode === "prependChild" || mode === "appendChild") &&
322
+ node?.isExpandable(true)
323
+ ) {
324
+ node.setExpanded().then(() => {
325
+ this.createNode(mode, node, init);
326
+ });
327
+ return;
328
+ }
329
+ const newNode = node.addNode(init, mode);
330
+ newNode.addClass("wb-edit-new");
331
+ this.relatedNode = node;
332
+
333
+ // Don't filter new nodes:
334
+ newNode.match = true;
335
+
336
+ newNode.makeVisible({ noAnimation: true }).then(() => {
337
+ this.startEditTitle(newNode);
338
+ });
339
+ }
340
+ }