Source: view/Layout.js

import React from "react";
import ReactDOM from "react-dom";
import Splitter from "./Splitter.js";
import Tab from "./Tab.js";
import TabSet from "./TabSet.js";
import DragDrop from "../DragDrop.js";
import Rect from "../Rect.js";
import DockLocation from "../DockLocation.js";
import TabNode from "../model/TabNode.js";
import TabSetNode from "../model/TabSetNode.js";
import SplitterNode from "../model/SplitterNode.js";
import Actions from "../model/Actions.js";
import Model from "../model/Model.js";

/**
 * A React component that hosts a multi-tabbed layout
 */
class Layout extends React.Component {

    /**
     * @private
     */
    constructor(props) {
        super(props);
        this.modelChanged = false;
        this.props.model.setListener(this.onModelChange.bind(this));
        this.state = {model: this.props.model, rect: new Rect(0, 0, 0, 0)};
    }

    onModelChange() {
        this.modelChanged = true;
        this.layout(this.state.model);
    }

    doAction(action) {
        if (this.props.onAction !== undefined) {
            this.props.onAction(action);
        }
        else {
            this.state.model.doAction(action);
        }
    }

    componentDidMount() {
        this.layout(this.state.model);

        // need to re-render if size changed
        window.addEventListener("resize", function (event) {
            this.layout(this.state.model);
        }.bind(this));
    }

    componentWillReceiveProps(newProps) {
        //this.log("componentWillReceiveProps");
        this.layout(newProps.model);
        if (this.props.model !== newProps.model) {
            if (this.props.model != null) {
                this.props.model.setListener(null); // stop listening to old model
            }

            newProps.model.setListener(this.onModelChange.bind(this));
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        //this.log("shouldComponentUpdate ");
        let update = this.state.model !== nextState.model
            || !this.state.rect.equals(nextState.rect)
            || this.modelChanged;

        //console.log("shouldComponentUpdate " + update);
        return update;
    }

    layout(model) {
        if (this.refs.self != undefined) {
            let domRect = this.refs.self.getBoundingClientRect();
            let rect = new Rect(0, 0, domRect.width, domRect.height);
            //this.log("layout " + rect);
            model._layout(rect);
            this.setState({model: model, rect: rect});
        }
    }

    renderChild(node, childComponents) {
        let drawChildren = node._getDrawChildren();
        for (let i = 0; i < drawChildren.length; i++) {
            let child = drawChildren[i];

            if (child.getType() === SplitterNode.TYPE) {
                childComponents.push(<Splitter key={child.getId()} layout={this} node={child}></Splitter>);
            }
            else if (child.getType() === TabSetNode.TYPE) {
                childComponents.push(<TabSet key={child.getId()} layout={this} node={child}></TabSet>);
                this.renderChild(child, childComponents);
                if (child.isMaximized()) {
                    this.maximized = true;
                }
            }
            else if (child.getType() === TabNode.TYPE) {
                let selectedTab = child.getParent().getChildren()[child.getParent().getSelected()];
                if (selectedTab == null) {
                    debugger; // this should not happen!
                }
                childComponents.push(<Tab
                    key={child.getId()}
                    layout={this}
                    node={child}
                    selected={child === selectedTab}
                    factory={this.props.factory}>
                </Tab>);
            }
            else {// is row
                this.renderChild(child, childComponents);
            }
        }
    }

    log(message) {
        console.log(message);
    }

    render() {
        //this.log("render " + this.state.rect.toString());
        let childComponents = [];

        if (this.state.rect != null) {
            this.maximized = false;
            this.renderChild(this.state.model.getRoot(), childComponents);
        }
        else {
            childComponents.push(<span key="loading">loading...</span>);
        }

        this.modelChanged = false;

        return <div ref="self" className="flexlayout__layout">{childComponents}</div>;
    }

    /**
     * Adds a new tab to the given tabset
     * @param tabsetId the id of the tabset where the new tab will be added
     * @param json the json for the new tab node
     */
    addTabToTabSet(tabsetId, json) {
        let tabsetNode = this.state.model.getNodeById(tabsetId);
        if (tabsetNode != null) {
            this.doAction(Actions.addNode(json, tabsetId, DockLocation.CENTER, -1));
        }
    }

    /**
     * Adds a new tab to the active tabset (if there is one)
     * @param json the json for the new tab node
     */
    addTabToActiveTabSet(json) {
        let tabsetNode = this.state.model.getActiveTabset();
        if (tabsetNode != null) {
            this.doAction(Actions.addNode(json, tabsetNode.getId(), DockLocation.CENTER, -1));
        }
    }

    /**
     * Adds a new tab by dragging a labeled panel to the drop location, dragging starts immediatelly
     * @param dragText the text to show on the drag panel
     * @param json the json for the new tab node
     * @param onDrop a callback to call when the drag is complete
     */
    addTabWithDragAndDrop(dragText, json, onDrop) {
        this.fnNewNodeDropped = onDrop;
        this.newTabJson = json;
        this.dragStart(null, dragText, new TabNode(this.state.model, json), null, null);
    }

    /**
     * Adds a new tab by dragging a labeled panel to the drop location, dragging starts when you
     * mouse down on the panel
     *
     * @param dragText the text to show on the drag panel
     * @param json the json for the new tab node
     * @param onDrop a callback to call when the drag is complete
     */
    addTabWithDragAndDropIndirect(dragText, json, onDrop) {
        this.fnNewNodeDropped = onDrop;
        this.newTabJson = json;

        DragDrop.instance.addGlass(this.onCancelAdd.bind(this));

        this.dragDivText = dragText;
        this.dragDiv = document.createElement("div");
        this.dragDiv.className = "flexlayout__drag_rect";
        this.dragDiv.innerHTML = this.dragDivText;
        this.dragDiv.addEventListener("mousedown", this.onDragDivMouseDown.bind(this));
        this.dragDiv.addEventListener("touchstart", this.onDragDivMouseDown.bind(this));

        let r = new Rect(10, 10, 150, 50);
        r.centerInRect(this.state.rect);
        r.positionElement(this.dragDiv);

        let rootdiv = ReactDOM.findDOMNode(this);
        rootdiv.appendChild(this.dragDiv);
    }

    onCancelAdd() {
        let rootdiv = ReactDOM.findDOMNode(this);
        rootdiv.removeChild(this.dragDiv);
        this.dragDiv = null;
        if (this.fnNewNodeDropped != null) {
            this.fnNewNodeDropped();
            this.fnNewNodeDropped = null;
        }
        DragDrop.instance.hideGlass();
    }

    onCancelDrag() {
        let rootdiv = ReactDOM.findDOMNode(this);

        try {
            rootdiv.removeChild(this.outlineDiv);
        } catch (e) {
        }

        try {
            rootdiv.removeChild(this.dragDiv);
        } catch (e) {
        }

        this.dragDiv = null;
        this.hideEdges(rootdiv);
        if (this.fnNewNodeDropped != null) {
            this.fnNewNodeDropped();
            this.fnNewNodeDropped = null;
        }
        DragDrop.instance.hideGlass();
    }

    onDragDivMouseDown(event) {
        event.preventDefault();
        this.dragStart(event, this.dragDivText, new TabNode(this.state.model, this.newTabJson), true, null, null);
    }

    dragStart(event, dragDivText, node, allowDrag, onClick, onDoubleClick) {
        if (this.maximized || !allowDrag) {
            DragDrop.instance.startDrag(event, null, null, null, null, onClick, onDoubleClick);
        }
        else {
            this.dragNode = node;
            this.dragDivText = dragDivText;
            DragDrop.instance.startDrag(event, this.onDragStart.bind(this), this.onDragMove.bind(this), this.onDragEnd.bind(this), this.onCancelDrag.bind(this), onClick, onDoubleClick);
        }
    }

    onDragStart(event) {
        let rootdiv = ReactDOM.findDOMNode(this);
        this.outlineDiv = document.createElement("div");
        this.outlineDiv.className = "flexlayout__outline_rect";
        rootdiv.appendChild(this.outlineDiv);

        if (this.dragDiv == null) {
            this.dragDiv = document.createElement("div");
            this.dragDiv.className = "flexlayout__drag_rect";
            this.dragDiv.innerHTML = this.dragDivText;
            rootdiv.appendChild(this.dragDiv);
        }
        // add edge indicators
        this.showEdges(rootdiv);

        if (this.dragNode != null && this.dragNode.getType() === TabNode.TYPE && this.dragNode.getTabRect() != null) {
            this.dragNode.getTabRect().positionElement(this.outlineDiv);
        }
        this.firstMove = true;

        return true;
    }

    onDragMove(event) {
        if (this.firstMove === false) {
            this.outlineDiv.style.transition = "top .3s, left .3s, width .3s, height .3s";
        }
        this.firstMove = false;
        let clientRect = this.refs.self.getBoundingClientRect();
        let pos = {
            x: event.clientX - clientRect.left,
            y: event.clientY - clientRect.top
        };

        this.dragDiv.style.left = (pos.x - 75) + "px";
        this.dragDiv.style.top = pos.y + "px";

        let root = this.state.model.getRoot();
        let dropInfo = root._findDropTargetNode(this.dragNode, pos.x, pos.y);
        if (dropInfo) {
            this.dropInfo = dropInfo;
            this.outlineDiv.className = dropInfo.className;
            dropInfo.rect.positionElement(this.outlineDiv);
        }
    }

    onDragEnd(event) {
        let rootdiv = ReactDOM.findDOMNode(this);
        rootdiv.removeChild(this.outlineDiv);
        rootdiv.removeChild(this.dragDiv);
        this.dragDiv = null;
        this.hideEdges(rootdiv);
        DragDrop.instance.hideGlass();


        if (this.dropInfo) {
            if (this.newTabJson != null) {
                this.doAction(Actions.addNode(this.newTabJson, this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index));

                if (this.fnNewNodeDropped != null) {
                    this.fnNewNodeDropped();
                    this.fnNewNodeDropped = null;
                }
                this.newTabJson = null;
            }
            else if (this.dragNode != null) {
                this.doAction(Actions.moveNode(this.dragNode.getId(), this.dropInfo.node.getId(), this.dropInfo.location, this.dropInfo.index));
            }

        }
    }

    showEdges(rootdiv) {
        if (this.state.model.isEnableEdgeDock()) {
            let domRect = rootdiv.getBoundingClientRect();
            let size = 100;
            let length = size + "px";
            let radius = "50px";
            let width = "10px";

            this.edgeTopDiv = document.createElement("div");
            this.edgeTopDiv.className = "flexlayout__edge_rect";
            this.edgeTopDiv.style.top = "0px";
            this.edgeTopDiv.style.left = (domRect.width - size) / 2 + "px";
            this.edgeTopDiv.style.width = length;
            this.edgeTopDiv.style.height = width;
            this.edgeTopDiv.style.borderBottomLeftRadius = radius;
            this.edgeTopDiv.style.borderBottomRightRadius = radius;

            this.edgeLeftDiv = document.createElement("div");
            this.edgeLeftDiv.className = "flexlayout__edge_rect";
            this.edgeLeftDiv.style.top = (domRect.height - size) / 2 + "px";
            this.edgeLeftDiv.style.left = "0px";
            this.edgeLeftDiv.style.width = width;
            this.edgeLeftDiv.style.height = length;
            this.edgeLeftDiv.style.borderTopRightRadius = radius;
            this.edgeLeftDiv.style.borderBottomRightRadius = radius;

            this.edgeBottomDiv = document.createElement("div");
            this.edgeBottomDiv.className = "flexlayout__edge_rect";
            this.edgeBottomDiv.style.bottom = "0px";
            this.edgeBottomDiv.style.left = (domRect.width - size) / 2 + "px";
            this.edgeBottomDiv.style.width = length;
            this.edgeBottomDiv.style.height = width;
            this.edgeBottomDiv.style.borderTopLeftRadius = radius;
            this.edgeBottomDiv.style.borderTopRightRadius = radius;

            this.edgeRightDiv = document.createElement("div");
            this.edgeRightDiv.className = "flexlayout__edge_rect";
            this.edgeRightDiv.style.top = (domRect.height - size) / 2 + "px";
            this.edgeRightDiv.style.right = "0px";
            this.edgeRightDiv.style.width = width;
            this.edgeRightDiv.style.height = length;
            this.edgeRightDiv.style.borderTopLeftRadius = radius;
            this.edgeRightDiv.style.borderBottomLeftRadius = radius;

            rootdiv.appendChild(this.edgeTopDiv);
            rootdiv.appendChild(this.edgeLeftDiv);
            rootdiv.appendChild(this.edgeBottomDiv);
            rootdiv.appendChild(this.edgeRightDiv);
        }
    }

    hideEdges(rootdiv) {
        if (this.state.model.isEnableEdgeDock()) {
            try {
                rootdiv.removeChild(this.edgeTopDiv);
                rootdiv.removeChild(this.edgeLeftDiv);
                rootdiv.removeChild(this.edgeBottomDiv);
                rootdiv.removeChild(this.edgeRightDiv);
            }
            catch (e) {
            }
        }
    }

    maximize(tabsetNode) {
        this.doAction(Actions.maximizeToggle(tabsetNode.getId()));
    }

    customizeTab(tabNode, renderValues) {
        if (this.props.onRenderTab) {
            this.props.onRenderTab(tabNode, renderValues);
        }
    }

    customizeTabSet(tabSetNode, renderValues) {
        if (this.props.onRenderTabSet) {
            this.props.onRenderTabSet(tabSetNode, renderValues);
        }
    }
}

Layout.propTypes = {
    model: React.PropTypes.instanceOf(Model).isRequired,
    factory: React.PropTypes.func.isRequired,

    onAction: React.PropTypes.func,

    onRenderTab: React.PropTypes.func,
    onRenderTabSet: React.PropTypes.func
};

export default Layout;