Source: jquery.fancytree.edit.js

/*!
 * jquery.fancytree.edit.js
 *
 * Make node titles editable.
 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
 *
 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
 *
 * Released under the MIT license
 * https://github.com/mar10/fancytree/wiki/LicenseInfo
 *
 * @version @VERSION
 * @date @DATE
 */

(function (factory) {
	if (typeof define === "function" && define.amd) {
		// AMD. Register as an anonymous module.
		define(["jquery", "./jquery.fancytree"], factory);
	} else if (typeof module === "object" && module.exports) {
		// Node/CommonJS
		require("./jquery.fancytree");
		module.exports = factory(require("jquery"));
	} else {
		// Browser globals
		factory(jQuery);
	}
})(function ($) {
	"use strict";

	/*******************************************************************************
	 * Private functions and variables
	 */

	var isMac = /Mac/.test(navigator.platform),
		escapeHtml = $.ui.fancytree.escapeHtml,
		trim = $.ui.fancytree.trim,
		unescapeHtml = $.ui.fancytree.unescapeHtml;

	/**
	 * [ext-edit] Start inline editing of current node title.
	 *
	 * @alias FancytreeNode#editStart
	 * @requires Fancytree
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.editStart = function () {
		var $input,
			node = this,
			tree = this.tree,
			local = tree.ext.edit,
			instOpts = tree.options.edit,
			$title = $(".fancytree-title", node.span),
			eventData = {
				node: node,
				tree: tree,
				options: tree.options,
				isNew: $(node[tree.statusClassPropName]).hasClass(
					"fancytree-edit-new"
				),
				orgTitle: node.title,
				input: null,
				dirty: false,
			};

		// beforeEdit may want to modify the title before editing
		if (
			instOpts.beforeEdit.call(
				node,
				{ type: "beforeEdit" },
				eventData
			) === false
		) {
			return false;
		}
		$.ui.fancytree.assert(!local.currentNode, "recursive edit");
		local.currentNode = this;
		local.eventData = eventData;

		// Disable standard Fancytree mouse- and key handling
		tree.widget._unbind();

		local.lastDraggableAttrValue = node.span.draggable;
		if (local.lastDraggableAttrValue) {
			node.span.draggable = false;
		}

		// #116: ext-dnd prevents the blur event, so we have to catch outer clicks
		$(document).on("mousedown.fancytree-edit", function (event) {
			if (!$(event.target).hasClass("fancytree-edit-input")) {
				node.editEnd(true, event);
			}
		});

		// Replace node with <input>
		$input = $("<input />", {
			class: "fancytree-edit-input",
			type: "text",
			value: tree.options.escapeTitles
				? eventData.orgTitle
				: unescapeHtml(eventData.orgTitle),
		});
		local.eventData.input = $input;
		if (instOpts.adjustWidthOfs != null) {
			$input.width($title.width() + instOpts.adjustWidthOfs);
		}
		if (instOpts.inputCss != null) {
			$input.css(instOpts.inputCss);
		}

		$title.html($input);

		// Focus <input> and bind keyboard handler
		$input
			.focus()
			.change(function (event) {
				$input.addClass("fancytree-edit-dirty");
			})
			.on("keydown", function (event) {
				switch (event.which) {
					case $.ui.keyCode.ESCAPE:
						node.editEnd(false, event);
						break;
					case $.ui.keyCode.ENTER:
						node.editEnd(true, event);
						return false; // so we don't start editmode on Mac
				}
				event.stopPropagation();
			})
			.blur(function (event) {
				return node.editEnd(true, event);
			});

		instOpts.edit.call(node, { type: "edit" }, eventData);
	};

	/**
	 * [ext-edit] Stop inline editing.
	 * @param {Boolean} [applyChanges=false] false: cancel edit, true: save (if modified)
	 * @alias FancytreeNode#editEnd
	 * @requires jquery.fancytree.edit.js
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function (
		applyChanges,
		_event
	) {
		var newVal,
			node = this,
			tree = this.tree,
			local = tree.ext.edit,
			eventData = local.eventData,
			instOpts = tree.options.edit,
			$title = $(".fancytree-title", node.span),
			$input = $title.find("input.fancytree-edit-input");

		if (instOpts.trim) {
			$input.val(trim($input.val()));
		}
		newVal = $input.val();

		eventData.dirty = newVal !== node.title;
		eventData.originalEvent = _event;

		// Find out, if saving is required
		if (applyChanges === false) {
			// If true/false was passed, honor this (except in rename mode, if unchanged)
			eventData.save = false;
		} else if (eventData.isNew) {
			// In create mode, we save everything, except for empty text
			eventData.save = newVal !== "";
		} else {
			// In rename mode, we save everyting, except for empty or unchanged text
			eventData.save = eventData.dirty && newVal !== "";
		}
		// Allow to break (keep editor open), modify input, or re-define data.save
		if (
			instOpts.beforeClose.call(
				node,
				{ type: "beforeClose" },
				eventData
			) === false
		) {
			return false;
		}
		if (
			eventData.save &&
			instOpts.save.call(node, { type: "save" }, eventData) === false
		) {
			return false;
		}
		$input.removeClass("fancytree-edit-dirty").off();
		// Unbind outer-click handler
		$(document).off(".fancytree-edit");

		if (eventData.save) {
			// # 171: escape user input (not required if global escaping is on)
			node.setTitle(
				tree.options.escapeTitles ? newVal : escapeHtml(newVal)
			);
			node.setFocus();
		} else {
			if (eventData.isNew) {
				node.remove();
				node = eventData.node = null;
				local.relatedNode.setFocus();
			} else {
				node.renderTitle();
				node.setFocus();
			}
		}
		local.eventData = null;
		local.currentNode = null;
		local.relatedNode = null;
		// Re-enable mouse and keyboard handling
		tree.widget._bind();

		if (node && local.lastDraggableAttrValue) {
			node.span.draggable = true;
		}

		// Set keyboard focus, even if setFocus() claims 'nothing to do'
		tree.$container.get(0).focus({ preventScroll: true });
		eventData.input = null;
		instOpts.close.call(node, { type: "close" }, eventData);
		return true;
	};

	/**
	 * [ext-edit] Create a new child or sibling node and start edit mode.
	 *
	 * @param {String} [mode='child'] 'before', 'after', or 'child'
	 * @param {Object} [init] NodeData (or simple title string)
	 * @alias FancytreeNode#editCreateNode
	 * @requires jquery.fancytree.edit.js
	 * @since 2.4
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function (
		mode,
		init
	) {
		var newNode,
			tree = this.tree,
			self = this;

		mode = mode || "child";
		if (init == null) {
			init = { title: "" };
		} else if (typeof init === "string") {
			init = { title: init };
		} else {
			$.ui.fancytree.assert($.isPlainObject(init));
		}
		// Make sure node is expanded (and loaded) in 'child' mode
		if (
			mode === "child" &&
			!this.isExpanded() &&
			this.hasChildren() !== false
		) {
			this.setExpanded().done(function () {
				self.editCreateNode(mode, init);
			});
			return;
		}
		newNode = this.addNode(init, mode);

		// #644: Don't filter new nodes.
		newNode.match = true;
		$(newNode[tree.statusClassPropName])
			.removeClass("fancytree-hide")
			.addClass("fancytree-match");

		newNode.makeVisible(/*{noAnimation: true}*/).done(function () {
			$(newNode[tree.statusClassPropName]).addClass("fancytree-edit-new");
			self.tree.ext.edit.relatedNode = self;
			newNode.editStart();
		});
	};

	/**
	 * [ext-edit] Check if any node in this tree  in edit mode.
	 *
	 * @returns {FancytreeNode | null}
	 * @alias Fancytree#isEditing
	 * @requires jquery.fancytree.edit.js
	 */
	$.ui.fancytree._FancytreeClass.prototype.isEditing = function () {
		return this.ext.edit ? this.ext.edit.currentNode : null;
	};

	/**
	 * [ext-edit] Check if this node is in edit mode.
	 * @returns {Boolean} true if node is currently beeing edited
	 * @alias FancytreeNode#isEditing
	 * @requires jquery.fancytree.edit.js
	 */
	$.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function () {
		return this.tree.ext.edit
			? this.tree.ext.edit.currentNode === this
			: false;
	};

	/*******************************************************************************
	 * Extension code
	 */
	$.ui.fancytree.registerExtension({
		name: "edit",
		version: "@VERSION",
		// Default options for this extension.
		options: {
			adjustWidthOfs: 4, // null: don't adjust input size to content
			allowEmpty: false, // Prevent empty input
			inputCss: { minWidth: "3em" },
			// triggerCancel: ["esc", "tab", "click"],
			triggerStart: ["f2", "mac+enter", "shift+click"],
			trim: true, // Trim whitespace before save
			// Events:
			beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available)
			beforeEdit: $.noop, // Return false to prevent edit mode
			close: $.noop, // Editor was removed
			edit: $.noop, // Editor was opened (available as data.input)
			//		keypress: $.noop,    // Not yet implemented
			save: $.noop, // Save data.input.val() or return false to keep editor open
		},
		// Local attributes
		currentNode: null,

		treeInit: function (ctx) {
			var tree = ctx.tree;

			this._superApply(arguments);

			this.$container
				.addClass("fancytree-ext-edit")
				.on("fancytreebeforeupdateviewport", function (event, data) {
					var editNode = tree.isEditing();
					// When scrolling, the TR may be re-used by another node, so the
					// active cell marker an
					if (editNode) {
						editNode.info("Cancel edit due to scroll event.");
						editNode.editEnd(false, event);
					}
				});
		},
		nodeClick: function (ctx) {
			var eventStr = $.ui.fancytree.eventToString(ctx.originalEvent),
				triggerStart = ctx.options.edit.triggerStart;

			if (
				eventStr === "shift+click" &&
				$.inArray("shift+click", triggerStart) >= 0
			) {
				if (ctx.originalEvent.shiftKey) {
					ctx.node.editStart();
					return false;
				}
			}
			if (
				eventStr === "click" &&
				$.inArray("clickActive", triggerStart) >= 0
			) {
				// Only when click was inside title text (not aynwhere else in the row)
				if (
					ctx.node.isActive() &&
					!ctx.node.isEditing() &&
					$(ctx.originalEvent.target).hasClass("fancytree-title")
				) {
					ctx.node.editStart();
					return false;
				}
			}
			return this._superApply(arguments);
		},
		nodeDblclick: function (ctx) {
			if ($.inArray("dblclick", ctx.options.edit.triggerStart) >= 0) {
				ctx.node.editStart();
				return false;
			}
			return this._superApply(arguments);
		},
		nodeKeydown: function (ctx) {
			switch (ctx.originalEvent.which) {
				case 113: // [F2]
					if ($.inArray("f2", ctx.options.edit.triggerStart) >= 0) {
						ctx.node.editStart();
						return false;
					}
					break;
				case $.ui.keyCode.ENTER:
					if (
						$.inArray("mac+enter", ctx.options.edit.triggerStart) >=
							0 &&
						isMac
					) {
						ctx.node.editStart();
						return false;
					}
					break;
			}
			return this._superApply(arguments);
		},
	});
	// Value returned by `require('jquery.fancytree..')`
	return $.ui.fancytree;
}); // End of closure

Fork me on GitHub