UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

588 lines (503 loc) 16.9 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2008 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Sebastian Werner (wpbasti) * Jonathan Weiß (jonathan_rass) ************************************************************************ */ /** * A split panes divides an area into two panes. The ratio between the two * panes is configurable by the user using the splitter. * * @childControl slider {qx.ui.splitpane.Slider} shown during resizing the splitpane * @childControl splitter {qx.ui.splitpane.Splitter} splitter to resize the splitpane */ qx.Class.define("qx.ui.splitpane.Pane", { extend: qx.ui.core.Widget, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * Creates a new instance of a SplitPane. It allows the user to dynamically * resize the areas dropping the border between. * * @param orientation {String} The orientation of the split pane control. * Allowed values are "horizontal" (default) and "vertical". */ construct(orientation) { super(); this.__children = []; // Initialize orientation if (orientation) { this.setOrientation(orientation); } else { this.initOrientation(); } // add all pointer listener to the blocker this.__blocker.addListener("pointerdown", this._onPointerDown, this); this.__blocker.addListener("pointerup", this._onPointerUp, this); this.__blocker.addListener("pointermove", this._onPointerMove, this); this.__blocker.addListener("pointerout", this._onPointerOut, this); this.__blocker.addListener("losecapture", this._onPointerUp, this); }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties: { // overridden appearance: { refine: true, init: "splitpane" }, /** * Distance between pointer and splitter when the cursor should change * and enable resizing. */ offset: { check: "Integer", init: 6, apply: "_applyOffset" }, /** * The orientation of the splitpane control. */ orientation: { init: "horizontal", check: ["horizontal", "vertical"], apply: "_applyOrientation" } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members: { __splitterOffset: null, __activeDragSession: false, __lastPointerX: null, __lastPointerY: null, __isHorizontal: null, __beginSize: null, __endSize: null, __children: null, __blocker: null, // overridden _createChildControlImpl(id, hash) { var control; switch (id) { // Create and add slider case "slider": control = new qx.ui.splitpane.Slider(this); control.exclude(); this._add(control, { type: id }); break; // Create splitter case "splitter": control = new qx.ui.splitpane.Splitter(this); this._add(control, { type: id }); control.addListener("move", this.__onSplitterMove, this); break; } return control || super._createChildControlImpl(id); }, /** * Move handler for the splitter which takes care of the external * triggered resize of children. * * @param e {qx.event.type.Data} The data even of move. */ __onSplitterMove(e) { this.__setBlockerPosition(e.getData()); }, /** * Creates a blocker for the splitter which takes all bouse events and * also handles the offset and cursor. * * @param orientation {String} The orientation of the pane. */ __createBlocker(orientation) { this.__blocker = new qx.ui.splitpane.Blocker(orientation); this.getContentElement().add(this.__blocker); var splitter = this.getChildControl("splitter"); var splitterWidth = splitter.getWidth(); if (!splitterWidth) { splitter.addListenerOnce("appear", () => { this.__setBlockerPosition(); }); } // resize listener to remove the blocker in case the splitter // is removed. splitter.addListener("resize", e => { var bounds = e.getData(); if ( this.getChildControl("splitter").isKnobVisible() && (bounds.height == 0 || bounds.width == 0) ) { this.__blocker.hide(); } else { this.__blocker.show(); } }); }, /** * Returns the blocker used over the splitter. this could be used for * adding event listeners like tap or dbltap. * * @return {qx.ui.splitpane.Blocker} The used blocker element. * * @internal */ getBlocker() { return this.__blocker; }, /* --------------------------------------------------------------------------- PROPERTY APPLY METHODS --------------------------------------------------------------------------- */ /** * Apply routine for the orientation property. * * Sets the pane's layout to vertical or horizontal split layout. * * @param value {String} The new value of the orientation property * @param old {String} The old value of the orientation property */ _applyOrientation(value, old) { // ARIA attrs this.getContentElement().setAttribute("aria-orientation", value); var slider = this.getChildControl("slider"); var splitter = this.getChildControl("splitter"); // Store boolean flag for faster access this.__isHorizontal = value === "horizontal"; if (!this.__blocker) { this.__createBlocker(value); } // update the blocker this.__blocker.setOrientation(value); // Dispose old layout var oldLayout = this._getLayout(); if (oldLayout) { oldLayout.dispose(); } // Create new layout var newLayout = value === "vertical" ? new qx.ui.splitpane.VLayout() : new qx.ui.splitpane.HLayout(); this._setLayout(newLayout); // Update states for splitter and slider splitter.removeState(old); splitter.addState(value); splitter.getChildControl("knob").removeState(old); splitter.getChildControl("knob").addState(value); slider.removeState(old); slider.addState(value); // flush (needs to be done for the blocker update) and update the blocker qx.ui.core.queue.Manager.flush(); this.__setBlockerPosition(); }, // property apply _applyOffset(value, old) { this.__setBlockerPosition(); }, /** * Helper for setting the blocker to the right position, which depends on * the offset, orientation and the current position of the splitter. * * @param bounds {Map?null} If the bounds of the splitter are known, * they can be added. */ __setBlockerPosition(bounds) { var splitter = this.getChildControl("splitter"); var offset = this.getOffset(); var splitterBounds = splitter.getBounds(); var splitterElem = splitter.getContentElement().getDomElement(); // do nothing if the splitter is not ready if (!splitterElem) { return; } // recalculate the dimensions of the blocker if (this.__isHorizontal) { // get the width either of the given bounds or of the read bounds var width = null; if (bounds) { width = bounds.width; } else if (splitterBounds) { width = splitterBounds.width; } var left = bounds && bounds.left; if (width || !this.getChildControl("splitter").isKnobVisible()) { if (isNaN(left)) { left = qx.bom.element.Location.getPosition(splitterElem).left; } this.__blocker.setWidth(offset, width || 6); this.__blocker.setLeft(offset, left); } // vertical case } else { // get the height either of the given bounds or of the read bounds var height = null; if (bounds) { height = bounds.height; } else if (splitterBounds) { height = splitterBounds.height; } var top = bounds && bounds.top; if (height || !this.getChildControl("splitter").isKnobVisible()) { if (isNaN(top)) { top = qx.bom.element.Location.getPosition(splitterElem).top; } this.__blocker.setHeight(offset, height || 6); this.__blocker.setTop(offset, top); } } }, /* --------------------------------------------------------------------------- PUBLIC METHODS --------------------------------------------------------------------------- */ /** * Adds a widget to the pane. * * Sets the pane's layout to vertical or horizontal split layout. Depending on the * pane's layout the first widget will be the left or top widget, the second one * the bottom or right widget. Adding more than two widgets will overwrite the * existing ones. * * @param widget {qx.ui.core.Widget} The widget to be inserted into pane. * @param flex {Number} The (optional) layout property for the widget's flex value. */ add(widget, flex) { if (flex === undefined) { this._add(widget); } else { this._add(widget, { flex: flex }); } this.__children.push(widget); }, /** * Removes the given widget from the pane. * * @param widget {qx.ui.core.Widget} The widget to be removed. */ remove(widget) { this._remove(widget); qx.lang.Array.remove(this.__children, widget); }, /** * Returns an array containing the pane's content. * * @return {qx.ui.core.Widget[]} The pane's child widgets */ getChildren() { return this.__children; }, /* --------------------------------------------------------------------------- POINTER LISTENERS --------------------------------------------------------------------------- */ /** * Handler for pointerdown event. * * Shows slider widget and starts drag session if pointer is near/on splitter widget. * * @param e {qx.event.type.Pointer} pointerdown event */ _onPointerDown(e) { // Only proceed if left pointer button is pressed and the splitter is active if (!e.isLeftPressed()) { return; } var splitter = this.getChildControl("splitter"); // Store offset between pointer event coordinates and splitter var splitterLocation = splitter.getContentLocation(); var paneLocation = this.getContentLocation(); this.__splitterOffset = this.__isHorizontal ? e.getDocumentLeft() - splitterLocation.left + paneLocation.left : e.getDocumentTop() - splitterLocation.top + paneLocation.top; // Synchronize slider to splitter size and show it var slider = this.getChildControl("slider"); var splitterBounds = splitter.getBounds(); slider.setUserBounds( splitterBounds.left, splitterBounds.top, splitterBounds.width || 6, splitterBounds.height || 6 ); slider.setZIndex(splitter.getZIndex() + 1); slider.show(); // Enable session this.__activeDragSession = true; this.__blocker.capture(); e.stop(); }, /** * Handler for pointermove event. * * @param e {qx.event.type.Pointer} pointermove event */ _onPointerMove(e) { this._setLastPointerPosition(e.getDocumentLeft(), e.getDocumentTop()); // Check if slider is already being dragged if (this.__activeDragSession) { // Compute new children sizes this.__computeSizes(); // Update slider position var slider = this.getChildControl("slider"); var pos = this.__beginSize; if (this.__isHorizontal) { slider.setDomLeft(pos); this.__blocker.setStyle("left", pos - this.getOffset() + "px"); } else { slider.setDomTop(pos); this.__blocker.setStyle("top", pos - this.getOffset() + "px"); } e.stop(); } }, /** * Handler for pointerout event * * @param e {qx.event.type.Pointer} pointerout event */ _onPointerOut(e) { this._setLastPointerPosition(e.getDocumentLeft(), e.getDocumentTop()); }, /** * Handler for pointerup event * * Sets widget sizes if dragging session has been active. * * @param e {qx.event.type.Pointer} pointerup event */ _onPointerUp(e) { if (!this.__activeDragSession) { return; } // Set sizes to both widgets this._finalizeSizes(); // Hide the slider var slider = this.getChildControl("slider"); slider.exclude(); // Cleanup this.__activeDragSession = false; this.releaseCapture(); e.stop(); }, /* --------------------------------------------------------------------------- INTERVAL HANDLING --------------------------------------------------------------------------- */ /** * Updates widgets' sizes based on the slider position. */ _finalizeSizes() { var beginSize = this.__beginSize; var endSize = this.__endSize; if (beginSize == null) { return; } var children = this._getChildren(); var firstWidget = children[2]; var secondWidget = children[3]; // Read widgets' flex values var firstFlexValue = firstWidget.getLayoutProperties().flex; var secondFlexValue = secondWidget.getLayoutProperties().flex; // Both widgets have flex values if (firstFlexValue != 0 && secondFlexValue != 0) { firstWidget.setLayoutProperties({ flex: beginSize }); secondWidget.setLayoutProperties({ flex: endSize }); } // Update both sizes else { // Set widths to static widgets if (this.__isHorizontal) { firstWidget.setWidth(beginSize); secondWidget.setWidth(endSize); } else { firstWidget.setHeight(beginSize); secondWidget.setHeight(endSize); } } }, /** * Computes widgets' sizes based on the pointer coordinate. */ __computeSizes() { if (this.__isHorizontal) { var min = "minWidth", size = "width", max = "maxWidth", pointer = this.__lastPointerX; } else { var min = "minHeight", size = "height", max = "maxHeight", pointer = this.__lastPointerY; } var children = this._getChildren(); var beginHint = children[2].getSizeHint(); var endHint = children[3].getSizeHint(); // Area given to both widgets var allocatedSize = children[2].getBounds()[size] + children[3].getBounds()[size]; // Calculate widget sizes var beginSize = pointer - this.__splitterOffset; var endSize = allocatedSize - beginSize; // Respect minimum limits if (beginSize < beginHint[min]) { endSize -= beginHint[min] - beginSize; beginSize = beginHint[min]; } else if (endSize < endHint[min]) { beginSize -= endHint[min] - endSize; endSize = endHint[min]; } // Respect maximum limits if (beginSize > beginHint[max]) { endSize += beginSize - beginHint[max]; beginSize = beginHint[max]; } else if (endSize > endHint[max]) { beginSize += endSize - endHint[max]; endSize = endHint[max]; } // Store sizes this.__beginSize = beginSize; this.__endSize = endSize; }, /** * Determines whether this is an active drag session * * @return {Boolean} True if active drag session, otherwise false. */ _isActiveDragSession() { return this.__activeDragSession; }, /** * Sets the last pointer position. * * @param x {Integer} the x position of the pointer. * @param y {Integer} the y position of the pointer. */ _setLastPointerPosition(x, y) { this.__lastPointerX = x; this.__lastPointerY = y; } }, destruct() { this.__children = null; } });