Skip to content
Nathan Hennig edited this page Dec 22, 2021 · 39 revisions

About Fancytree edit extension.

Allow to change node titles or create new nodes using inline editing.

For ext-table users, the recommended way to edit columns other than title is by using input elements to hold the column values. See ext-table+more for an example.

Options

  • adjustWidthOfs, type: {int}, optional, default: 4
    When switching to edit mode, the title is replaced with an input control. The input width is calculated from the current title text width in pixels plus this increment.
    If null, don't adjust input size to content.
  • inputCss, type: {object}, optional, default: { minWidth: "3em" }
    Additional CSS style for the new input control.
  • triggerStart, type: {string[]}, optional, default: ["f2", "mac+enter", "shift+click"]
    Possible trigger values:
    • "clickActive": Click again into an already active node title.
    • "dblclick": Double-click a node.
    • "f2": Press F2.
    • "mac+enter": Press Enter (only on macOS).
    • "shift+click": Click a node while holding Shift.

Events

  • beforeEdit
    Fired before edit mode starts. Return false to prevent edit mode.
  • edit
    Fired when the editor is opened. The text input is available as a jQuery element in data.input.
  • beforeClose
    Fired before closing the editor. Return false to prevent cancel/save. data.input is available.
  • save
    Fired when the "beforeClose" event succeeds. Save data.input.val() or return false to keep the editor open.
  • close
    Fired after editor was removed.

Methods

  • {void} node.editStart()
    Start inline editing of current node title.

  • {void} node.editEnd(applyChanges)
    Stop inline editing.
    {boolean} [applyChanges=false]

  • {void} node.editCreateNode(mode, init)
    Create a new child or sibling node and start edit mode.
    {string} [mode='child'] 'before', 'after', or 'child'
    {object} [init] NodeData (or simple title string)

Example

In addition to jQuery, jQuery UI, and Fancytree, include jquery.fancytree.edit.js:

  <script src="//code.jquery.com/jquery-3.6.0.min.js"></script>

  <link href="skin-win8/ui.fancytree.css" rel="stylesheet">
  <script src="js/jquery-ui-dependencies/jquery.fancytree.ui-deps.js"></script>
  <script src="js/jquery.fancytree.js"></script>
  <script src="js/jquery.fancytree.edit.js"></script>

Enable edit extension and pass options:

$("#tree").fancytree({
  extensions: ["edit"],
  edit: {
    // Available options with their default:
    adjustWidthOfs: 4,   // null: don't adjust input size to content
    inputCss: { minWidth: "3em" },
    triggerStart: ["clickActive", "f2", "dblclick", "shift+click", "mac+enter"],
    beforeEdit: $.noop,   // Return false to prevent edit mode
    edit: $.noop,         // Editor was opened (available as data.input)
    beforeClose: $.noop,  // Return false to prevent cancel/save (data.input is available)
    save: $.noop,         // Save data.input.val() or return false to keep editor open
    close: $.noop,        // Editor was removed
  },
  [...]
});

For example:

$("#tree").fancytree({
  extensions: ["edit"],
  ...
  edit: {
    beforeEdit: function(event, data){
      // `data.node` is about to be edited.
      // Return false to prevent this.
    },
    edit: function(event, data){
      // `data.node` switched into edit mode.
      // The <input> element was created (available as jQuery object `data.input`)
      // and contains the original `data.node.title`.
    },
    beforeClose: function(event, data){
      // Editing is about to end (either cancel or save).
      // Additional information is available:
      // - `data.dirty`:    true if text was modified by user.
      // - `data.input`:    The input element (jQuery object).
      //                    `data.input.val()` returns the new node title.
      // - `data.isNew`:    true if this node was newly created using `editCreateNode()`.
      // - `data.orgTitle`: The previous node title text.
      // - `data.originalEvent`:
      //                    Contains the originating event (i.e. blur, mousdown, keydown).
      // - `data.save`:     false if saving is not required, i.e. user pressed  
      //                    cancel or text is unchanged.
      //                    This value may be changed to override default behavior.
      // Return false to prevent this (keep the editor open), for example when
      // validations fail.
    },
    save: function(event, data){
      // Only called when the text was modified and the user pressed enter or
      // the <input> lost focus.
      // Additional information is available (see `beforeClose`).
      // Return false to keep editor open, for example when validations fail.
      // Otherwise the user input is accepted as `node.title` and the <input>
      // is removed.
      // Typically we would also issue an Ajax request here to send the new data
      // to the server (and handle potential errors when the asynchronous request
      // returns).
    },
    close: function(event, data){
      // Editor was removed.
      // Additional information is available (see `beforeClose`).
    }
  }
});

New nodes can be created in edit mode like this. Hitting Esc would cancel and Enter will accept:

node.editCreateNode("after", {
  title: "Node title",
  folder: true
});

Recipes

[Howto] Control closing/saving/discarding

By default, editing ends, when we get ESC, ENTER, blur, or outer mousedown events.
ESC will discard.

Apps may want to stop editing depending on the current input, mouse button, or even mouse pointer position. Also the decision whether to save or discard may depend on various conditions.
We can evaluate the source event in the beforeClose callback to override the default behavior, for example:

$("#tree").fancytree({
  extensions: ["edit"],
  ...
  edit: {
    beforeClose: function(event, data) {
      if( data.originalEvent.type === "mousedown" ) {
        // We could prevent the mouse click from generating a blur event
        // (which would then again close the editor) and return `false` to keep
        // the editor open:
//      data.originalEvent.preventDefault();
//      return false;
        // Or go on with closing the editor, but discard any changes:
        data.save = false;
      }
    },
    ...
  },
  ...
});
[Howto] Send changes to server (optimistic approach)

Here is an example for a robust handling of node renaming with user feedback.

$("#tree").fancytree({
  extensions: ["edit"],
  ...
  edit: {
    save: function(event, data){
      var node = data.node;

      // Save data.input.val() or return false to keep the editor open
      $.ajax({
        url: "saveNode",
        data: { key: node.key, title: data.input.val() }

      }).done(function(result){
        // Server might return an error or a modified title
        node.setTitle(result.acceptedTitle); // in case server modified it
        // Maybe also check for non-ajax errors, e.g. 'title invalid', ...

      }).fail(function(result){
        // Ajax error: reset title (and maybe issue a warning)
        node.setTitle(data.orgTitle);

      }).always(function(){
        data.input.removeClass("pending");
      });
      // Optimistically assume that save will succeed. Accept the user input
      return true;
    },
    close: function(event, data){
      // Editor was removed. If we started an async request, mark the node as pending
      if( data.save ) {
        $(data.node.span).addClass("pending");
      }
    },
  },
  ...
});

We also might add some CSS for a nice 'pending' style:

span.pending span.fancytree-title {
  font-style: italic;
}
span.pending span.fancytree-title:after {
  content: "\2026"; /* ellipsis */
}
[Howto] Send changes to server (pessimistic approach)

This approach pessimistically keeps the edit control open until the server responds 'ok'.

$("#tree").fancytree({
  extensions: ["edit"],
  ...
  edit: {
    ...
    save: function(event, data){
      var node = data.node,
          newTitle = data.input.val(),
          originalTitle = data.orgTitle;
      
      // Save data.input.val() asynchr. but also return false to keep the editor open
      // (optionally disable the input control)
      $.ajax({
        <your_ajax_call_to_validate_and_update_title_in_server|db)>
      }).done(function(result){
        // Server might return an application error
        if (result.retcode>0) { // validation error
          // Do something related to application error here
          // (optionally enable the input control again)
        } else {
          // server returned OK
          node.setTitle('<server_updated_value>');
          // Important: exit sucessfull editing
          node.editEnd(false);
        }
      }).fail(function(result){
        // maybe update title with original value
        node.setTitle(originalTitle);
        // (optionally enable the input control again)
      }).always(function(){
        $(data.node.span).removeClass("pending");
      });
      // Always return false except when receives server validation successfully
      return false;
    },
  },
});
[Howto] Use ext-edit together with ext-dnd

Fancytree uses the standard jQuery UI draggable which prevents mouse clicks from setting the focus (details).

In combination with the ext-edit extension, this can prevent that keyboard input (such as [F2]) is delivered to the tree.
Use the dnd.focusOnClick: true option in this case.

$("#tree").fancytree({
  extensions: ["dnd", "edit"],
  dnd: {
    focusOnClick: true,  // Focus, although draggable cancels mousedown event
    ...
  },
  edit: {
    ...
  },
  [...]
});