mar10/fancytree

View on GitHub
src/jquery.fancytree.edit.js

Summary

Maintainability
D
1 day
Test Coverage
/*!
 * 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
            .trigger("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