1/*!
2 * jquery.fancytree.edit.js
3 *
4 * Make node titles editable.
5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
6 *
7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
8 *
9 * Released under the MIT license
10 * https://github.com/mar10/fancytree/wiki/LicenseInfo
11 *
12 * @version 2.38.3
13 * @date 2023-02-01T20:52:50Z
14 */
15
16(function (factory) {
17	if (typeof define === "function" && define.amd) {
18		// AMD. Register as an anonymous module.
19		define(["jquery", "./jquery.fancytree"], factory);
20	} else if (typeof module === "object" && module.exports) {
21		// Node/CommonJS
22		require("./jquery.fancytree");
23		module.exports = factory(require("jquery"));
24	} else {
25		// Browser globals
26		factory(jQuery);
27	}
28})(function ($) {
29	"use strict";
30
31	/*******************************************************************************
32	 * Private functions and variables
33	 */
34
35	var isMac = /Mac/.test(navigator.platform),
36		escapeHtml = $.ui.fancytree.escapeHtml,
37		trim = $.ui.fancytree.trim,
38		unescapeHtml = $.ui.fancytree.unescapeHtml;
39
40	/**
41	 * [ext-edit] Start inline editing of current node title.
42	 *
43	 * @alias FancytreeNode#editStart
44	 * @requires Fancytree
45	 */
46	$.ui.fancytree._FancytreeNodeClass.prototype.editStart = function () {
47		var $input,
48			node = this,
49			tree = this.tree,
50			local = tree.ext.edit,
51			instOpts = tree.options.edit,
52			$title = $(".fancytree-title", node.span),
53			eventData = {
54				node: node,
55				tree: tree,
56				options: tree.options,
57				isNew: $(node[tree.statusClassPropName]).hasClass(
58					"fancytree-edit-new"
59				),
60				orgTitle: node.title,
61				input: null,
62				dirty: false,
63			};
64
65		// beforeEdit may want to modify the title before editing
66		if (
67			instOpts.beforeEdit.call(
68				node,
69				{ type: "beforeEdit" },
70				eventData
71			) === false
72		) {
73			return false;
74		}
75		$.ui.fancytree.assert(!local.currentNode, "recursive edit");
76		local.currentNode = this;
77		local.eventData = eventData;
78
79		// Disable standard Fancytree mouse- and key handling
80		tree.widget._unbind();
81
82		local.lastDraggableAttrValue = node.span.draggable;
83		if (local.lastDraggableAttrValue) {
84			node.span.draggable = false;
85		}
86
87		// #116: ext-dnd prevents the blur event, so we have to catch outer clicks
88		$(document).on("mousedown.fancytree-edit", function (event) {
89			if (!$(event.target).hasClass("fancytree-edit-input")) {
90				node.editEnd(true, event);
91			}
92		});
93
94		// Replace node with <input>
95		$input = $("<input />", {
96			class: "fancytree-edit-input",
97			type: "text",
98			value: tree.options.escapeTitles
99				? eventData.orgTitle
100				: unescapeHtml(eventData.orgTitle),
101		});
102		local.eventData.input = $input;
103		if (instOpts.adjustWidthOfs != null) {
104			$input.width($title.width() + instOpts.adjustWidthOfs);
105		}
106		if (instOpts.inputCss != null) {
107			$input.css(instOpts.inputCss);
108		}
109
110		$title.html($input);
111
112		// Focus <input> and bind keyboard handler
113		$input
114			.focus()
115			.change(function (event) {
116				$input.addClass("fancytree-edit-dirty");
117			})
118			.on("keydown", function (event) {
119				switch (event.which) {
120					case $.ui.keyCode.ESCAPE:
121						node.editEnd(false, event);
122						break;
123					case $.ui.keyCode.ENTER:
124						node.editEnd(true, event);
125						return false; // so we don't start editmode on Mac
126				}
127				event.stopPropagation();
128			})
129			.blur(function (event) {
130				return node.editEnd(true, event);
131			});
132
133		instOpts.edit.call(node, { type: "edit" }, eventData);
134	};
135
136	/**
137	 * [ext-edit] Stop inline editing.
138	 * @param {Boolean} [applyChanges=false] false: cancel edit, true: save (if modified)
139	 * @alias FancytreeNode#editEnd
140	 * @requires jquery.fancytree.edit.js
141	 */
142	$.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function (
143		applyChanges,
144		_event
145	) {
146		var newVal,
147			node = this,
148			tree = this.tree,
149			local = tree.ext.edit,
150			eventData = local.eventData,
151			instOpts = tree.options.edit,
152			$title = $(".fancytree-title", node.span),
153			$input = $title.find("input.fancytree-edit-input");
154
155		if (instOpts.trim) {
156			$input.val(trim($input.val()));
157		}
158		newVal = $input.val();
159
160		eventData.dirty = newVal !== node.title;
161		eventData.originalEvent = _event;
162
163		// Find out, if saving is required
164		if (applyChanges === false) {
165			// If true/false was passed, honor this (except in rename mode, if unchanged)
166			eventData.save = false;
167		} else if (eventData.isNew) {
168			// In create mode, we save everything, except for empty text
169			eventData.save = newVal !== "";
170		} else {
171			// In rename mode, we save everyting, except for empty or unchanged text
172			eventData.save = eventData.dirty && newVal !== "";
173		}
174		// Allow to break (keep editor open), modify input, or re-define data.save
175		if (
176			instOpts.beforeClose.call(
177				node,
178				{ type: "beforeClose" },
179				eventData
180			) === false
181		) {
182			return false;
183		}
184		if (
185			eventData.save &&
186			instOpts.save.call(node, { type: "save" }, eventData) === false
187		) {
188			return false;
189		}
190		$input.removeClass("fancytree-edit-dirty").off();
191		// Unbind outer-click handler
192		$(document).off(".fancytree-edit");
193
194		if (eventData.save) {
195			// # 171: escape user input (not required if global escaping is on)
196			node.setTitle(
197				tree.options.escapeTitles ? newVal : escapeHtml(newVal)
198			);
199			node.setFocus();
200		} else {
201			if (eventData.isNew) {
202				node.remove();
203				node = eventData.node = null;
204				local.relatedNode.setFocus();
205			} else {
206				node.renderTitle();
207				node.setFocus();
208			}
209		}
210		local.eventData = null;
211		local.currentNode = null;
212		local.relatedNode = null;
213		// Re-enable mouse and keyboard handling
214		tree.widget._bind();
215
216		if (node && local.lastDraggableAttrValue) {
217			node.span.draggable = true;
218		}
219
220		// Set keyboard focus, even if setFocus() claims 'nothing to do'
221		tree.$container.get(0).focus({ preventScroll: true });
222		eventData.input = null;
223		instOpts.close.call(node, { type: "close" }, eventData);
224		return true;
225	};
226
227	/**
228	 * [ext-edit] Create a new child or sibling node and start edit mode.
229	 *
230	 * @param {String} [mode='child'] 'before', 'after', or 'child'
231	 * @param {Object} [init] NodeData (or simple title string)
232	 * @alias FancytreeNode#editCreateNode
233	 * @requires jquery.fancytree.edit.js
234	 * @since 2.4
235	 */
236	$.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function (
237		mode,
238		init
239	) {
240		var newNode,
241			tree = this.tree,
242			self = this;
243
244		mode = mode || "child";
245		if (init == null) {
246			init = { title: "" };
247		} else if (typeof init === "string") {
248			init = { title: init };
249		} else {
250			$.ui.fancytree.assert($.isPlainObject(init));
251		}
252		// Make sure node is expanded (and loaded) in 'child' mode
253		if (
254			mode === "child" &&
255			!this.isExpanded() &&
256			this.hasChildren() !== false
257		) {
258			this.setExpanded().done(function () {
259				self.editCreateNode(mode, init);
260			});
261			return;
262		}
263		newNode = this.addNode(init, mode);
264
265		// #644: Don't filter new nodes.
266		newNode.match = true;
267		$(newNode[tree.statusClassPropName])
268			.removeClass("fancytree-hide")
269			.addClass("fancytree-match");
270
271		newNode.makeVisible(/*{noAnimation: true}*/).done(function () {
272			$(newNode[tree.statusClassPropName]).addClass("fancytree-edit-new");
273			self.tree.ext.edit.relatedNode = self;
274			newNode.editStart();
275		});
276	};
277
278	/**
279	 * [ext-edit] Check if any node in this tree  in edit mode.
280	 *
281	 * @returns {FancytreeNode | null}
282	 * @alias Fancytree#isEditing
283	 * @requires jquery.fancytree.edit.js
284	 */
285	$.ui.fancytree._FancytreeClass.prototype.isEditing = function () {
286		return this.ext.edit ? this.ext.edit.currentNode : null;
287	};
288
289	/**
290	 * [ext-edit] Check if this node is in edit mode.
291	 * @returns {Boolean} true if node is currently beeing edited
292	 * @alias FancytreeNode#isEditing
293	 * @requires jquery.fancytree.edit.js
294	 */
295	$.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function () {
296		return this.tree.ext.edit
297			? this.tree.ext.edit.currentNode === this
298			: false;
299	};
300
301	/*******************************************************************************
302	 * Extension code
303	 */
304	$.ui.fancytree.registerExtension({
305		name: "edit",
306		version: "2.38.3",
307		// Default options for this extension.
308		options: {
309			adjustWidthOfs: 4, // null: don't adjust input size to content
310			allowEmpty: false, // Prevent empty input
311			inputCss: { minWidth: "3em" },
312			// triggerCancel: ["esc", "tab", "click"],
313			triggerStart: ["f2", "mac+enter", "shift+click"],
314			trim: true, // Trim whitespace before save
315			// Events:
316			beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available)
317			beforeEdit: $.noop, // Return false to prevent edit mode
318			close: $.noop, // Editor was removed
319			edit: $.noop, // Editor was opened (available as data.input)
320			//		keypress: $.noop,    // Not yet implemented
321			save: $.noop, // Save data.input.val() or return false to keep editor open
322		},
323		// Local attributes
324		currentNode: null,
325
326		treeInit: function (ctx) {
327			var tree = ctx.tree;
328
329			this._superApply(arguments);
330
331			this.$container
332				.addClass("fancytree-ext-edit")
333				.on("fancytreebeforeupdateviewport", function (event, data) {
334					var editNode = tree.isEditing();
335					// When scrolling, the TR may be re-used by another node, so the
336					// active cell marker an
337					if (editNode) {
338						editNode.info("Cancel edit due to scroll event.");
339						editNode.editEnd(false, event);
340					}
341				});
342		},
343		nodeClick: function (ctx) {
344			var eventStr = $.ui.fancytree.eventToString(ctx.originalEvent),
345				triggerStart = ctx.options.edit.triggerStart;
346
347			if (
348				eventStr === "shift+click" &&
349				$.inArray("shift+click", triggerStart) >= 0
350			) {
351				if (ctx.originalEvent.shiftKey) {
352					ctx.node.editStart();
353					return false;
354				}
355			}
356			if (
357				eventStr === "click" &&
358				$.inArray("clickActive", triggerStart) >= 0
359			) {
360				// Only when click was inside title text (not aynwhere else in the row)
361				if (
362					ctx.node.isActive() &&
363					!ctx.node.isEditing() &&
364					$(ctx.originalEvent.target).hasClass("fancytree-title")
365				) {
366					ctx.node.editStart();
367					return false;
368				}
369			}
370			return this._superApply(arguments);
371		},
372		nodeDblclick: function (ctx) {
373			if ($.inArray("dblclick", ctx.options.edit.triggerStart) >= 0) {
374				ctx.node.editStart();
375				return false;
376			}
377			return this._superApply(arguments);
378		},
379		nodeKeydown: function (ctx) {
380			switch (ctx.originalEvent.which) {
381				case 113: // [F2]
382					if ($.inArray("f2", ctx.options.edit.triggerStart) >= 0) {
383						ctx.node.editStart();
384						return false;
385					}
386					break;
387				case $.ui.keyCode.ENTER:
388					if (
389						$.inArray("mac+enter", ctx.options.edit.triggerStart) >=
390							0 &&
391						isMac
392					) {
393						ctx.node.editStart();
394						return false;
395					}
396					break;
397			}
398			return this._superApply(arguments);
399		},
400	});
401	// Value returned by `require('jquery.fancytree..')`
402	return $.ui.fancytree;
403}); // End of closure
404