Source: classes/elements/node.js

Syntree.config_maps.node = {};
Syntree.config_maps.node.accept_unmapped_config = false;
Syntree.config_maps.node.map = {
    /**
     * Session-unique identifier.
     *
     * @type {number}
     *
     * @see Syntree.Lib.genId
     *
     * @memberof Syntree.Node
     * @instance
     */
    id: {
        require: 'number',
        default_value: '#undefined',
    },
    /**
     * This Node's x coordinate.
     *
     * @type {number}
     *
     * @memberof Syntree.Node
     * @instance
     */
    x: {
        require: 'number',
        default_value: 0,
    },
    /**
     * This Node's y coordinate.
     *
     * @type {number}
     *
     * @memberof Syntree.Node
     * @instance
     */
    y: {
        require: 'number',
        default_value: 0,
    },
    /**
     * Label content.
     *
     * @type {string}
     *
     * @memberof Syntree.Node
     * @instance
     */
    labelContent: {
        require: 'string',
        default_value: '',
    },
}

/**
 * @class
 * @classdesc Nodes are the meat of Syntree. They act like items in a linked list, containing references to their parents and children.
 * @extends Syntree.Element
 * @extends Syntree.SelectableElement
 */
Syntree.Node = function(config_matrix) {
    Syntree.Lib.config(config_matrix,this);
    Syntree.Lib.extend(Syntree.SelectableElement, Syntree.Node, this);

    /**
     * The last position that was updated to the visual display.
     *
     * @type {object}
     */
    this.lastSyncedPosition = undefined;

    // Relationships
    /**
     * This Node's parent Node.
     *
     * @type {Syntree.Node}
     */
    this.parent = undefined;

    /**
     * This Node's child Nodes.
     *
     * @type {Syntree.Node[]}
     */
    this.children = [];

    // Branches
    /**
     * Branch connecting to this Node's parent.
     *
     * @type {Syntree.Branch}
     */
    this.parentBranch = undefined;

    /**
     * Branches connecting to this Node's children.
     *
     * @type {Syntree.Branch[]}
     */
    this.childBranches = [];

    // States
    /**
     * Is this Node being edited?
     *
     * @type {Boolean}
     */
    this.editing = false;

    /**
     * Has this Node been saved at least once?
     * Used so we can autodelete Nodes on reverse navigation.
     *
     * @type {Boolean}
     */
    this.real = false;

    /**
     * This Node's cached label BBox.
     *
     * @type {object}
     */
    this._labelbbox;

    this.updateGraphics();
}

/**
 * Build needed graphical objects and compile them into a new instance of Graphic.
 *
 * @see Syntree.Graphic
 */
Syntree.Node.prototype.createGraphic = function() {
    // Editor
    var editorid = 'editor-' + this.id;
    $('.editor_container').append('<input id="' + editorid + '" class="editor">');
    var editor = $('#' + editorid);
    editor.hide();

    // Highlight
    var highlight = Syntree.snap.rect(this.x, this.y, 0, 0);
    highlight.attr({
        class: 'highlight highlight-' + this.id,
    });

    // Delete button
    var deleteButton = Syntree.snap.image('/resources/delete_button.svg', this.x, this.y, 10, 10);
    deleteButton.attr({
        class: 'delete_button delete_button-' + this.id,
    });

    // Label
    var label = Syntree.snap.text(this.x,this.y,this.labelContent);
    label.attr({
        'id': 'label-'+this.id,
        'class':'node-label',
    });

    var config_matrix = {
        elements_to_add: {
            label: {
                el_obj: label,
                include_in_svg_string: true,
            },
            highlight: {
                el_obj: highlight,
            },
            deleteButton: {
                el_obj: deleteButton,
            },
            editor: {
                el_obj: editor,
                attr_handler: function(el,attrs) {
                    el.css(attrs);
                }
            },
        },
        states_synced: {
            selected: false,
            labelContent: false,
            position: false,
        },
        data_object: this,
        update_map: {
            selected: {
                handler: 'boolean',
                state_name: 'selected',
                elements: {
                    highlight: {
                        stateTrueAttrs: {
                            fill: 'rgba(0,0,0,0.2)',
                        },
                        stateFalseAttrs: {
                            fill: 'none',
                        },
                    },
                    deleteButton: {
                        stateTrueAttrs: {
                            width: 10,
                        },
                        stateFalseAttrs: {
                            width: 0,
                        },
                    },
                },
            },
            labelContent: {
                handler: function(d,g) {
                    if (d.labelContent === '') {
                        g.getEl('label').node.textContent = '   ';
                    } else {
                        g.getEl('label').node.textContent = d.labelContent;
                    }
                    var bbox = d.getLabelBBox();
                    g.getEl('highlight').attr({
                        width: bbox.w + 10,
                        height: bbox.h + 10,
                    });
                    g.getEl('editor').css({
                        'width': bbox.w,
                        'height': bbox.h,
                    });
                }
            },
            position: {
                handler: function(d,g) {
                    var bbox = d.getLabelBBox();
                    g.getEl('label').attr({
                        x: d.x-(bbox.w/2),
                        y: d.y+(bbox.h/2),
                    });
                    d._labelbbox = undefined;
                    bbox = d.getLabelBBox();
                    g.getEl('highlight').attr({
                        x: bbox.x - 5,
                        y: bbox.y - 5,
                    });
                    g.getEl('deleteButton').attr({
                        x: bbox.x2,
                        y: bbox.y - 10,
                    });
                    g.getEl('editor').css({
                        'left': bbox.x,// + groupXOffset,
                        'top': bbox.y,// + groupYOffset,
                    });
                    d.lastSyncedPosition = {
                        x: d.getPosition().x,
                        y: d.getPosition().y
                    };
                }
            }
        }
    }

    this.graphic = new Syntree.Graphic(config_matrix)
}

/**
 * Get the position of this Node.
 * Does not include panning transform.
 *
 * @returns {object} - x and y coordinates
 *
 * @see Syntree.Node#x
 * @see Syntree.Node#y
 */
Syntree.Node.prototype.getPosition = function() {
    return {
        x: this.x,
        y: this.y
    };
}


/**
 * Accessor function for Syntree.Node#labelContent.
 *
 * @see Syntree.Node#labelContent
 */
Syntree.Node.prototype.getLabelContent = function() {
    return this.labelContent;
}

/**
 * Setter function for Syntree.Node#labelContent.
 *
 * @see Syntree.Node#labelContent
 */
Syntree.Node.prototype.setLabelContent = function(content) {
    this.graphic.unsync('labelContent');
    this.labelContent = content;
}

/**
 * Get this Node label's BBox.
 * Returns cached BBox if exists.
 *
 * @returns {object} - border box
 *
 * @see Syntree.Node#_labelbbox
 */
Syntree.Node.prototype.getLabelBBox = function() {
    if (this.graphic.getEl('label').node.textContent === '' || $('#' + this.graphic.getEl('label').attr('id')).length === 0) {
        var fakeHeight = 15;
        var fakeWidth = 10;
        return {
            x: this.x - fakeWidth/2,
            x2: this.x + fakeWidth/2,
            y: this.y - fakeHeight/2,
            y2: this.y + fakeHeight/2,
            w: fakeWidth,
            width: fakeWidth,
            height: fakeHeight,
            h: fakeHeight
        }
    } else {
        if (!Syntree.Lib.checkType(this._labelbbox, 'object')) {
            this._labelbbox = this.graphic.getEl('label').getBBox();
        }
        return this._labelbbox;
    }
}


/**
 * Accessor function for Syntree.Node#parent.
 *
 * @see Syntree.Node#parent
 */
Syntree.Node.prototype.getParent = function() {
    return this.parent;
}

/**
 * Accessor function for Syntree.Node#children.
 *
 * @see Syntree.Node#children
 */
Syntree.Node.prototype.getChildren = function() {
    return this.children;
}

/**
 * Get a path that acts like a border box for this Node's label.
 * Used to get intersections for movement arrows.
 *
 * @returns {string} - path string
 *
 * @see Syntree.Arrow
 */
Syntree.Node.prototype.getPath = function() {
    var bbox = this.getLabelBBox();
    var p = 10;
    var s = 'M ' + (bbox.x-p) + ' ' + (bbox.y-p) + ', ';
    s += 'H ' + (bbox.x2+p) + ', ';
    s += 'V ' + (bbox.y2+p) + ', ';
    s += 'H ' + (bbox.x-p) + ', ';
    s += 'V ' + (bbox.y-p) + ', ';
    return s;
}

/**
 * Grouped accessor function for selected, editing, real, and deleted.
 * Mostly a leftover from when I didn't know what I was doing.
 * Should probably turn this into something tht makes more sense, someday.
 *
 * @param {string} [which] - which state to return
 *
 * @returns {boolean|object} - either the boolean value of the state, or an object of state names mapped to booleans (if no state name provided)
 *
 * @see Syntree.Node#selected
 * @see Syntree.Node#editing
 * @see Syntree.Node#real
 * @see Syntree.Node#deleted
 */
Syntree.Node.prototype.getState = function(which) {
    switch (which) {
        case 'selected':
            return this.selected;
            break;
        case 'editing':
            return this.editing;
            break;
        case 'real':
            return this.real;
            break;
        case 'deleted':
            return this.deleted;
            break;
        default:
            return {
                selected: this.selected,
                editing: this.editing,
                real: this.real,
                deleted: this.deleted,
            }
    }
}

/**
 * Set this Node's x and y coordinates.
 *
 * @param {number} x - x coordinate
 * @param {number} y - y coordinate
 * @param {boolean} [propogate=true] - set coordinates of descendant Nodes as well
 *
 * @returns {object} - new x and y coordinates
 *
 * @see Syntree.Node#x
 * @see Syntree.Node#y
 */
Syntree.Node.prototype.move = function(x,y,propagate) {
    x = Syntree.Lib.checkArg(x, 'number');
    y = Syntree.Lib.checkArg(y, 'number');
    propagate = Syntree.Lib.checkArg(propagate, 'boolean', true);

    var oldX = this.x;
    var oldY = this.y;

    this.x = x;
    this.y = y;
    if (Syntree.Lib.checkType(this.parentBranch, 'branch')) {
        this.parentBranch.graphic.unsync('childPosition');
    }

    if (this.lastSyncedPosition.x != x || this.lastSyncedPosition.y != y) {
        this.graphic.unsync('position');
        if (this.childBranches.length > 0) {
            this.childBranches.map(function(b) {
                b.graphic.unsync('parentPosition');
            })
        }
    } else {
        this.graphic.sync('position');
        if (Syntree.Lib.checkType(this.parentBranch, 'branch')) {
            // this.parentBranch.graphic.sync('childPosition');
        }
        if (this.childBranches.length > 0) {
            this.childBranches.map(function(b) {
                // b.graphic.sync('parentPosition');
            })
        }
    }

    this._labelbbox = undefined;
    if (propagate) {
        var c = 0;
        while (c < this.children.length) {
            var deltaX = this.x - oldX;
            var deltaY = this.y - oldY;
            var thisChild = this.children[c];
            var pos = thisChild.getPosition();
            thisChild.move(pos.x + deltaX,pos.y + deltaY);
            c++;
        }
    }
    return {
        x: this.x,
        y: this.y,
    }
}

/**
 * Custom addition to Syntree.Element#delete.
 * Deletes connected Branches and Arrows as well.
 *
 * @see Syntree.Element#delete
 */
Syntree.Node.prototype.__delete = function() {
    this.real = false;
    if (Syntree.Lib.checkType(this.parentBranch, 'branch')) {
        this.parentBranch.delete();
    }
    if (Syntree.Lib.checkType(this.fromArrow, 'arrow')) {
        this.fromArrow.delete();
    }
    if (Syntree.Lib.checkType(this.toArrow, 'arrow')) {
        this.toArrow.delete();
    }
    if (Syntree.Lib.checkType(this.parent, 'node')) {
        this.parent.children.splice(this.parent.children.indexOf(this), 1);
        var tree = new Syntree.Tree({
            root: this.parent,
        })
        tree.distribute();
    }
}

/**
 * Custom addition to Syntree.SelectElement#deselect.
 * Deletes if is not real.
 *
 * @see Syntree.SelectableElement#deselect
 * @see Syntree.Node#real
 */
Syntree.Node.prototype.__deselect = function() {
    this.editingAction('cancel');
    if (!this.real) {
        this.delete();
    }
}

/**
 * Perform an editing action.
 *
 * @param {string} action - type of editing action: init, update, save, or cancel
 *
 * @see Syntree.Page#nodeEditing
 */
Syntree.Node.prototype.editingAction = function(action) {
    switch(action) {
        case 'init':
            this.editing = true;
            this.beforeEditLabelContent = this.labelContent;
            this.updateGraphics(false);
            this.graphic.getEl('editor').val(this.labelContent);
            this.graphic.getEl('editor').show();
            Syntree.Lib.focusNoScroll(this.graphic.getEl('editor'));
            break;
        case 'update':
            if (this.editing) {
                this.graphic.unsync('position');
                this._labelbbox = undefined;
                this.setLabelContent(this.graphic.getEl('editor').val());
                this.updateGraphics(false);
            }
            break;
        case 'save':
            this._labelbbox = undefined;
            if (!this.real) {
                this.real = true;
            }
            if (this.editing) {
                this.graphic.unsync('position');
                this.editing = false;
                this.setLabelContent(this.graphic.getEl('editor').val());
                this.graphic.getEl('editor').hide();
                this.graphic.getEl('editor').blur();
                this.beforeEditLabelContent = undefined;
                this._labelbbox = undefined;
                this.positionUnsynced = true;
                this.updateGraphics(false);
            }
            break;
        case 'cancel':
            if (this.editing) {
                this.graphic.unsync('position');
                this.editing = false;
                this.graphic.getEl('editor').hide();
                this.setLabelContent(this.beforeEditLabelContent);
                this.beforeEditLabelContent = undefined;
                this._labelbbox = undefined;
                this.updateGraphics(false);
            }
            break;
    }
}

/**
 * Custom addition to Syntree.Element#updateGraphics
 *
 * @see Syntree.Element#updateGraphics
 */
Syntree.Node.prototype.__updateGraphics = function(propagate) {
    propagate = Syntree.Lib.checkArg(propagate, 'boolean', false);
    if (this.positionUnsynced) {
        propagate = true;
    }

    // Branches
    if (Syntree.Lib.checkType(this.parentBranch, 'branch')) {
        this.parentBranch.updateGraphics();
    }
    for (i=0;i<this.childBranches.length;i++) {
        this.childBranches[i].updateGraphics();
    }
    if (Syntree.Lib.checkType(this.fromArrow, 'arrow')) {
        this.fromArrow.updateGraphics();
    }
    if (Syntree.Lib.checkType(this.toArrow, 'arrow')) {
        this.toArrow.updateGraphics();
    }

    if (propagate) {
        var c = 0;
        while (c < this.children.length) {
            this.children[c].updateGraphics();
            c++;
        }
    }
}

/**
 * Add a child at the specified index.
 *
 * @param {Syntree.Node} newNode - the Node to attach
 * @param {number} [index=Syntree.Node#children.length] - index at which to attach
 *
 * @see Syntree.Node#children
 */
Syntree.Node.prototype.addChild = function(newNode, index) {
    newNode = Syntree.Lib.checkArg(newNode, 'node');
    index = Syntree.Lib.checkArg(index, 'number', this.children.length);
    index = Syntree.Lib.checkArg(index, 'number');

    if (!this.real) {
        return;
    }
    newNode.parent = this;

    this.children.splice(index,0,newNode);

    var branch = new Syntree.Branch(this,newNode);
}

Syntree.Node.prototype.toString = function() {
    return '[object Node]';
}