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