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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/wunderbaum.css +5 -0
- package/dist/wunderbaum.d.ts +1348 -0
- package/dist/wunderbaum.esm.js +5266 -0
- package/dist/wunderbaum.esm.min.js +69 -0
- package/dist/wunderbaum.esm.min.js.map +1 -0
- package/dist/wunderbaum.umd.js +5276 -0
- package/dist/wunderbaum.umd.min.js +71 -0
- package/dist/wunderbaum.umd.min.js.map +1 -0
- package/package.json +123 -0
- package/src/common.ts +198 -0
- package/src/debounce.ts +365 -0
- package/src/deferred.ts +50 -0
- package/src/util.ts +656 -0
- package/src/wb_ext_dnd.ts +336 -0
- package/src/wb_ext_edit.ts +340 -0
- package/src/wb_ext_filter.ts +370 -0
- package/src/wb_ext_keynav.ts +210 -0
- package/src/wb_ext_logger.ts +54 -0
- package/src/wb_extension_base.ts +76 -0
- package/src/wb_node.ts +1780 -0
- package/src/wb_options.ts +117 -0
- package/src/wunderbaum.scss +509 -0
- package/src/wunderbaum.ts +1819 -0
|
@@ -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
|
+
}
|