Source: classes/page.js

/**
 * @class
 * @classdesc Acts as an intermediate controller between [Workspace]{@link Syntree.Workspace} and elements of the tree.
 */
Syntree.Page = function() {

    var wWidth = $('#workspace').width();
    var wHeight = $('#workspace').height();
    /**
     * The <rect> which is the background of the page.
     */
    this.background = Syntree.snap.rect(
        -1 * wWidth,
        -1 * wHeight,
        wWidth * 4,
        wHeight * 4
    );
    this.background.attr({
        fill:'white',
        id:'page-background',
    });

    /**
     * An SVG group of all elements on the page. Used for panning.
     *
     * @type {object}
     *
     * @see Syntree.Page#_enablePanning
     */
    this.group = Syntree.snap.g();

    /**
     * All [Elements]{@link Syntree.Element} on the page, referenced by id.
     *
     * @type {object}
     */
    this.allElements = {};

    /**
     * The currently selected [SelectableElement]{@link Syntree.SelectableElement}.
     *
     * @type {Syntree.SelectableElement}
     */
    this.selectedElement = undefined;

    this._enablePanning();
}

/**
 * Get the panning transform matrix.
 *
 * @returns {object} - deltax, deltay, and the global transform matrix
 *
 * @see Syntree.Page#_enablePanning
 * @see Syntree.Page#pan
 */
Syntree.Page.prototype.getTransform = function() {
    var t = this.group.transform().globalMatrix;
    var dx = t.e;
    var dy = t.f;
    return {
        dx: dx,
        dy: dy,
        globalMatrix: t,
    }
}

/**
 * Add an Element to the list of all elements.
 *
 * @param {Syntree.Element} element - the element to register
 *
 * @see Syntree.Page#allElements
 */
Syntree.Page.prototype.register = function(element) {
    element = Syntree.Lib.checkArg(element, element.isElement);

    this.allElements[element.getId()] = element;
    for (l in element.graphic.getAllEls()) {
        var el = element.graphic.getAllEls()[l];
        var el_obj = el.el_obj;
        if (typeof el_obj.paper !== 'undefined') { // Ensure is a Snap Element
            this.group.add(el_obj);
        }
    }
}

/**
 * Remove the specified Element from the list of all elements.
 *
 * @param {Syntree.Element} element - the element to deregister
 *
 * @see Syntree.Page#register
 */
Syntree.Page.prototype.deregister = function(element) {
    element = Syntree.Lib.checkArg(element, element.isElement);
    delete this.allElements[element.getId()];
}

/**
 * Select the given Element. (And deselect the previous Element.)
 *
 * @param {Syntree.Element} element - the element to select
 *
 * @see Syntree.Page#selectedElement
 */
Syntree.Page.prototype.select = function(element) {
    element = Syntree.Lib.checkArg(element, element.isSelectable);

    if (!Syntree.Lib.checkType(this.selectedElement, 'undefined')) {
        this.deselect();
    }

    this.selectedElement = element;
    element.select();

    new Syntree.Action('select', {
        selected_obj: element,
    });
}

/**
 * Deselect the currently selected Element.
 *
 * @see Syntree.Page#select
 */
Syntree.Page.prototype.deselect = function() {
    if (Syntree.Lib.checkType(this.selectedElement, this.selectedElement.isSelectable)) {
        this.selectedElement.deselect();
        this.selectedElement = undefined;
    }
}

/**
 * A wrapper function around Node.delete, allowing us to easily delete a whole subtree.
 *
 * @see Syntree.Element#delete
 */
Syntree.Page.prototype.deleteTree = function(tree) {
    tree = Syntree.Lib.checkArg(tree, ['tree', 'node']);
    if (Syntree.Lib.checkType(tree, 'node')) {
        tree = new Syntree.Tree({
            root: this.allElements[tree.id],
        });
    }

    var treestring = tree.getTreestring();
    var parent = tree.getRoot().getParent();
    var index = parent.getChildren().indexOf(tree.getRoot());
    tree.delete();
    if (Syntree.Lib.checkType(parent, 'node')) {
        temptree = new Syntree.Tree({
            root: parent,
        });
        temptree.distribute();
    }
    new Syntree.Action('delete', {
        deleted_obj: tree,
        treestring: treestring,
        parent: parent,
        index: index,
    });
}

/**
 * Check if given Element is registered with Page.
 *
 * @param {Syntree.Element} element - the element to check
 *
 * @returns {boolean} - whether or not the element is registered
 */
Syntree.Page.prototype.isRegistered = function(element) {
    element = Syntree.Lib.checkArg(element, element.isElement);
    return !Syntree.Lib.checkType(this.allElements[element.getId()], 'undefined');
}

/**
 * Accessor function for Page.selectedElement.
 *
 * @see Syntree.Page#selectedElement
 */
Syntree.Page.prototype.getSelected = function() {
    return this.selectedElement;
}

/**
 * Get all Elements, filtered by given type.
 *
 * @param {string} type - a string representing the Element type to filter by
 *
 * @returns {object} - all matching Elements, referenced by id
 */
Syntree.Page.prototype.getElementsByType = function(type) {
    type = Syntree.Lib.checkArg(type,'string');

    var res = {};
    for (id in this.allElements) {
        if (Syntree.Lib.checkType(this.allElements[id], type)) {
            res[id] = this.allElements[id];
        }
    }
    return res;
}

/**
 * Create a movement arrow from the selected [Node]{@link Syntree.Node} to the clicked [Node]{@link Syntree.Node}.
 *
 * @param {Syntree.Node} node - the node that was clicked
 *
 * @returns {Syntree.Arrow} - the new Arrow
 */
Syntree.Page.prototype.createMovementArrow = function(node) {
    if (Syntree.Lib.checkType(this.getSelected(), 'node')) {
        var a = new Syntree.Arrow({
            fromNode: this.getSelected(),
            toNode: node,
        });
        return a;
    }
}

/**
 * Add a tree to the page.
 * If you do not provide a parent [Node]{@link Syntree.Node}, the main tree will be replaced.
 *
 * @param {Syntree.Tree} [tree] - the Tree object to add.
 * @param {Syntree.Node} [parent] - the Node to which the root of the Tree will be added
 * @param {number} [index=0] - the index at which to add the root of Tree
 */
Syntree.Page.prototype.addTree = function(tree,parent,index) {
    tree = Syntree.Lib.checkArg(tree, 'tree', '#undefined');
    parent = Syntree.Lib.checkArg(parent, 'node', '#undefined');
    index = Syntree.Lib.checkArg(index, 'number', 0);

    if (!Syntree.Lib.checkType(tree, 'tree')) {
        // Default tree
        var root = new Syntree.Node({
            x: $('#workspace').width() / 2,
            y: $('#toolbar').height() + 20,
            labelContent: 'S',
        });
        this.tree = new Syntree.Tree({
            // build_treestring: 'id:612|children:40,266|parent:undefined|labelContent:S|;id:40|children:undefined|parent:612|labelContent:Q|;id:266|children:460,170|parent:612|labelContent:Q|;id:460|children:911,884|parent:266|labelContent:Qlsfdksdfasdf|;id:911|children:undefined|parent:460|labelContent:Q|;id:884|children:undefined|parent:460|labelContent:Q|;id:170|children:undefined|parent:266|labelContent:Q|;',
            // build_treestring: 'id:47|children:336,250|parent:undefined|labelContent:S|;id:336|children:570,175|parent:47|labelContent:Q|;id:570|children:838,146|parent:336|labelContent:O|;id:838|children:126,716|parent:570|labelContent:C|;id:126|children:538|parent:838|labelContent:E|;id:538|children:undefined|parent:126|labelContent:B|;id:716|children:undefined|parent:838|labelContent:X|;id:146|children:911,337|parent:570|labelContent:V|;id:911|children:undefined|parent:146|labelContent:G|;id:337|children:undefined|parent:146|labelContent:H|;id:175|children:883,866|parent:336|labelContent:A|;id:883|children:956,748|parent:175|labelContent:R|;id:956|children:undefined|parent:883|labelContent:S|;id:748|children:undefined|parent:883|labelContent:U|;id:866|children:391,578|parent:175|labelContent:T|;id:391|children:undefined|parent:866|labelContent:K|;id:578|children:undefined|parent:866|labelContent:N|;id:250|children:8,863|parent:47|labelContent:Z|;id:8|children:483,514|parent:250|labelContent:x|;id:483|children:109,271|parent:8|labelContent:Z|;id:109|children:undefined|parent:483|labelContent:Y|;id:271|children:undefined|parent:483|labelContent:I|;id:514|children:378,168|parent:8|labelContent:P|;id:378|children:undefined|parent:514|labelContent:B|;id:168|children:undefined|parent:514|labelContent:V|;id:863|children:564,746|parent:250|labelContent:L|;id:564|children:300,349|parent:863|labelContent:K|;id:300|children:undefined|parent:564|labelContent:J|;id:349|children:undefined|parent:564|labelContent:F|;id:746|children:766,805|parent:863|labelContent:M|;id:766|children:undefined|parent:746|labelContent:W|;id:805|children:undefined|parent:746|labelContent:Q|;',
            // build_treestring: 'id:432|children:67,741|parent:undefined|labelContent:S|;id:67|children:undefined|parent:432|labelContent:Q|;id:741|children:578|parent:432|labelContent:Q|;id:578|children:737|parent:741|labelContent:Q|;id:737|children:0|parent:578|labelContent:Q|;id:0|children:61|parent:737|labelContent:Q|;id:61|children:134|parent:0|labelContent:Q|;id:134|children:undefined|parent:61|labelContent:[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO]|;',
            root: root,
        });
        this.tree.root.editingAction('save');
    } else {
        if (!Syntree.Lib.checkType(parent, 'node')) {
            this.tree.delete();
            this.tree = tree;
            tree.distribute();
        } else {
            index = Syntree.Lib.checkArg(index, 'number', 0);
            parent.addChild(tree.root, index);
            var temp = new Syntree.Tree({
                root: parent,
            });
            temp.distribute();
        }
    }
}

/**
 * Create a [Tree]{@link Syntree.Tree} from a treestring, and then add it to the page.
 * If you do not provide a parent [Node]{@link Syntree.Node}, the main tree will be replaced.
 *
 * @param {Syntree.Tree} treestring - the treestring which the Tree will build from
 * @param {Syntree.Node} [parent] - the Node to which the root of the Tree will be added
 * @param {number} [index=0] - the index at which to add the root of Tree
 */
Syntree.Page.prototype.openTree = function(treestring,parent,index) {
    treestring = Syntree.Lib.checkArg(treestring, 'string');
    parent = Syntree.Lib.checkArg(parent, 'node', '#undefined');
    index = Syntree.Lib.checkArg(index, 'number', 0);

    var newTree = new Syntree.Tree({
        build_treestring: treestring,
    });
    this.addTree(newTree,parent,index);
}

/**
 * Get a string of SVG markup representing all marked objects on the page.
 *
 * @returns {string} - the SVG string
 */
Syntree.Page.prototype.getSVGString = function() {
    var selected = this.getSelected();
    this.deselect();
    var bgsvg = this.background.node.outerHTML;
    var elementssvg = '';
    var elements = this.allElements;
    for (id in elements) {
        elementssvg += elements[id].graphic.getSVGString();
    }
    var style = '<style type="text/css">text{font-family:sans-serif;font-size:14px;}</style>';
    var marker = $('marker')[0].outerHTML;
    this.select(selected);
    return style + marker + bgsvg + elementssvg;
}

/**
 * Get the Node nearest to the given coordinates or Element.
 *
 * @param {number|Syntree.Element} a - x coordinate or Element to search from
 * @param {number} [b] - if a is a an x coordinate, the corresponding y coordinate
 * @param {function} [condition] - function that must return true for a Node to be considered in the search
 *
 * @returns {object|boolean} - data object on success, false on failure
 */
Syntree.Page.prototype.getNearestNode = function(a,b,condition) {
    condition = Syntree.Lib.checkArg(condition, 'function', function(){return true;});

    if (Syntree.Lib.checkType(a, 'number')) {
        var x = a;
        var y = Syntree.Lib.checkType(b, 'number');
    } else {
        a = Syntree.Lib.checkArg(a, a.isElement);
        var x = a.getPosition().x;
        var y = a.getPosition().y;
        var ignoreNode = a;
    }

    var allNodes = this.getElementsByType('node');
    var nearestNode;
    var leastDist = Number.POSITIVE_INFINITY;
    for (id in allNodes) {
        var node = allNodes[id];
        if (ignoreNode && node === ignoreNode) {
            continue;
        }
        var pos = node.getPosition();
        var distance = Syntree.Lib.distance({
            x1: pos.x,
            y1: pos.y,
            x2: x,
            y2: y,
        })
        if (distance < leastDist && condition(x, y, node)) {
            leastDist = distance;
            nearestNode = node;
        }
    }
    if (Syntree.Lib.checkType(nearestNode, 'node')) {
        return {
            node: nearestNode,
            dist: leastDist,
            deltaX: x - nearestNode.getPosition().x,
            deltaY: y - nearestNode.getPosition().y,
        }
    } else {
        return false;
    }
}

/**
 * Select the node in the specified direction, or create a node there if one does not exist.
 *
 * @param {string} direction - 'left' or 'right'
 * @param {boolean} [fcreate=false] - force create instead of navigate
 *
 * @see Syntree.Workspace._eventRight
 * @see Syntree.Workspace._eventLeft
 */
Syntree.Page.prototype.navigateHorizontal = function(direction, fcreate) {
    direction = Syntree.Lib.checkArg(direction, 'string');
    fcreate = Syntree.Lib.checkArg(fcreate, 'boolean', false);

    if (direction === 'left') {
        var left = true;
        var right = false;
        var n = 0;
    } else if (direction === 'right') {
        var right = true;
        var left = false;
        var n = 1;
    } else {
        return;
    }

    if (!Syntree.Lib.checkType(this.getSelected(), 'node')) {
        this.select(this.tree.getRoot);
    }

    if (Syntree.Lib.checkType(this.getSelected(), 'node') && Syntree.Lib.checkType(this.getSelected().getParent(), 'node')) {
        var off = this.tree.getNodeOffset(this.tree.getRoot(), this.getSelected());
        var rowNodes = this.tree.getNodesByOffset(off);
        var selectedIndex = rowNodes.indexOf(this.getSelected());
        var real = this.getSelected().getState('real');

        if (right) {
            if (selectedIndex === rowNodes.length - 1 || fcreate) {
                if (real) {
                    var siblingIndex = this.getSelected().getParent().getChildren().indexOf(this.getSelected());
                    var newNode = new Syntree.Node({});
                    this.getSelected().getParent().addChild(newNode,siblingIndex + 1);
                    var tree = new Syntree.Tree({
                        root:this.getSelected().getParent(),
                    });
                    tree.distribute();
                    this.select(newNode);
                    this.nodeEditing('init');
                } else {
                    return;
                }
            } else {
                this.select(rowNodes[selectedIndex + 1]);
            }
        } else {
            if (selectedIndex === 0 || fcreate) {
                if (real) {
                    var siblingIndex = this.getSelected().getParent().getChildren().indexOf(this.getSelected());
                    var newNode = new Syntree.Node({});
                    this.getSelected().getParent().addChild(newNode, siblingIndex);
                    var tree = new Syntree.Tree({
                        root:this.getSelected().getParent(),
                    });
                    tree.distribute();
                    this.select(newNode);
                    this.nodeEditing('init');
                } else {
                    return;
                }
            } else {
                this.select(rowNodes[selectedIndex - 1]);
            }
        }

    }
}

/**
 * Select the parent of the currently selected Node
 */
Syntree.Page.prototype.navigateUp = function() {
    if (!Syntree.Lib.checkType(this.getSelected(), 'node')) {
        this.select(this.tree.getRoot);
    }

    if (Syntree.Lib.checkType(this.getSelected(), 'node') && Syntree.Lib.checkType(this.getSelected().getParent(), 'node')) {
        this.select(this.getSelected().getParent());
    }
}

/**
 * Select the most recently selected child of the currently selected node, or creates a child if one does not exist.
 * Defaults to the left-most child if no most recently selected.
 *
 * @param {boolean} [fcreate=false] - force creation instead of navigation
 */
Syntree.Page.prototype.navigateDown = function(fcreate) {
    fcreate = Syntree.Lib.checkArg(fcreate, 'boolean', false);

    if (!Syntree.Lib.checkType(this.getSelected(), 'node')) {
        this.select(this.tree.getRoot());
    }

    if (Syntree.Lib.checkType(this.getSelected(), 'node')) {
        if (this.getSelected().getChildren().length > 0 && !fcreate) {
            var possibleSelects = this.getSelected().getChildren();
            var selectHistory = Syntree.History.getNodeSelects();
            for (i = 0; i < selectHistory.length; i++) {
                if (possibleSelects.indexOf(selectHistory[i].selected_obj) >= 0) {
                    this.select(this.allElements[selectHistory[i].selected_obj.id]);
                    return;
                }
            }
            this.select(this.getSelected().getChildren()[0]);
        } else if (this.getSelected().getState('real')) {
            var newNode = new Syntree.Node({
                x: 0,
                y: 0,
                labelContent: '',
            });
            this.getSelected().addChild(newNode);
            var tree = new Syntree.Tree({
                root: this.getSelected(),
            });
            tree.distribute();
            this.select(newNode);
            this.nodeEditing('init');
        }
    }
}

/**
 * Execute an editing action on given Node.
 *
 * @param {string} type - 'init', 'update', 'toggle', 'save', 'cancel'
 * @param {Syntree.Node} [node=Syntree.Page.selectedNode] - the node to target
 * @param
 */
Syntree.Page.prototype.nodeEditing = function(type, node) {
    type = Syntree.Lib.checkArg(type, 'string');
    node = Syntree.Lib.checkArg(node, 'node', this.getSelected());
    node = Syntree.Lib.checkArg(node, 'node');

    if (type === 'init') {
        node.editingAction('init');
    } else if (type === 'update') {
        node.editingAction('update');
    } else if (type === 'toggle') {
        if (node.getState('editing')) {
            this.nodeEditing('save');
        } else {
            this.nodeEditing('init');
        }
    } else if (type === 'save') {
        if (node.getState('real')) {
            var pre = node.beforeEditLabelContent;
            var post = node.getLabelContent();
            new Syntree.Action('save', {
                node: node,
                pre: pre,
                post: post,
            });
        } else {
            new Syntree.Action('create', {
                created_obj: node,
            });
        }
        node.editingAction('save');
        if (this.getSelected().getParent()) {
            var tree = new Syntree.Tree({
                root:this.getSelected().getParent(),
            });
            tree.distribute();
        }
    } else if (type === 'cancel') {
        if (node.getState('editing')) {
            node.editingAction('cancel');
            if (!node.getState('real')) {
                this.nodeDelete(node);
            }
        }
    }
}

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

/**
 * Make custom handlers and attach them for panning functionality.
 */
Syntree.Page.prototype._enablePanning = function() {
    this._move = function(dx,dy) {

        this.attr({
                    transform: this.data('origTransform') + (this.data('origTransform') ? 'T' : 't') + [dx, dy],
                });
        // This allows us to make page elements pan as well, but still make panning happen only on background click.
        Syntree.Workspace.page.group.attr({
                    transform: this.data('origTransform') + (this.data('origTransform') ? 'T' : 't') + [dx, dy],
                });

        this.data('oldDX', dx);
        this.data('oldDY', dy);
    }

    this._end = function(dx,dy) {
        var t = Syntree.Workspace.page.getTransform();
        var top = $('.editor_container').position().top;
        var left = $('.editor_container').position().left;
        $('.editor_container').css({
            'top': t.dy + 'px',
            'left': t.dx + 'px',
        });
    }

    this._start = function() {
        this.data('origTransform', this.transform().local);
    }

    this.background.drag(this._move, this._start, this._end);
}