UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

582 lines (498 loc) 16.3 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) * Martin Wittemann (martinwittemann) * Christian Hagendorn (chris_schmidt) ************************************************************************ */ /** * Methods to place popup like widgets to other widgets, points, * pointer event coordinates, etc. */ qx.Mixin.define("qx.ui.core.MPlacement", { statics : { __visible : null, __direction : "left", /** * Set the always visible element. If an element is set, the * {@link #moveTo} method takes care of every move and tries not to cover * the given element with a movable widget like a popup or context menu. * * @param elem {qx.ui.core.Widget} The widget which should always be visible. */ setVisibleElement : function(elem) { this.__visible = elem; }, /** * Returns the given always visible element. See {@link #setVisibleElement} * for more details. * * @return {qx.ui.core.Widget|null} The given widget. */ getVisibleElement : function() { return this.__visible; }, /** * Set the move direction for an element which hides always visible element. * The value has only an effect when the {@link #setVisibleElement} is set. * * @param direction {String} The direction <code>left</code> or <code>top</code>. */ setMoveDirection : function(direction) { if (direction === "top" || direction === "left") { this.__direction = direction; } else { throw new Error("Invalid value for the parameter 'direction' " + "[qx.ui.core.MPlacement.setMoveDirection()], the value was '" + direction + "' " + "but 'top' or 'left' are allowed."); } }, /** * Returns the move direction for an element which hides always visible element. * See {@link #setMoveDirection} for more details. * * @return {String} The move direction. */ getMoveDirection : function() { return this.__direction; } }, properties : { /** * Position of the aligned object in relation to the opener. * * Please note than changes to this property are only applied * when re-aligning the widget. * * The first part of the value is the edge to attach to. The second * part the alignment of the orthogonal edge after the widget * has been attached. * * The default value "bottom-left" for example means that the * widget should be shown directly under the given target and * then should be aligned to be left edge: * * <pre> * +--------+ * | target | * +--------+ * +-------------+ * | widget | * +-------------+ * </pre> */ position : { check : [ "top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right", "left-top", "left-middle", "left-bottom", "right-top", "right-middle", "right-bottom" ], init : "bottom-left", themeable : true }, /** * Whether the widget should be placed relative to an other widget or to * the pointer. */ placeMethod : { check : ["widget", "pointer"], init : "pointer", themeable: true }, /** Whether the widget should moved using DOM methods. */ domMove : { check : "Boolean", init : false }, /** * Selects the algorithm to place the widget horizontally. <code>direct</code> * uses {@link qx.util.placement.DirectAxis}, <code>keep-align</code> * uses {@link qx.util.placement.KeepAlignAxis} and <code>best-fit</code> * uses {@link qx.util.placement.BestFitAxis}. */ placementModeX : { check : ["direct", "keep-align", "best-fit"], init : "keep-align", themeable : true }, /** * Selects the algorithm to place the widget vertically. <code>direct</code> * uses {@link qx.util.placement.DirectAxis}, <code>keep-align</code> * uses {@link qx.util.placement.KeepAlignAxis} and <code>best-fit</code> * uses {@link qx.util.placement.BestFitAxis}. */ placementModeY : { check : ["direct", "keep-align", "best-fit"], init : "keep-align", themeable : true }, /** Left offset of the pointer (in pixel) */ offsetLeft : { check : "Integer", init : 0, themeable : true }, /** Top offset of the pointer (in pixel) */ offsetTop : { check : "Integer", init : 0, themeable : true }, /** Right offset of the pointer (in pixel) */ offsetRight : { check : "Integer", init : 0, themeable : true }, /** Bottom offset of the pointer (in pixel) */ offsetBottom : { check : "Integer", init : 0, themeable : true }, /** Offsets in one group */ offset : { group : [ "offsetTop", "offsetRight", "offsetBottom", "offsetLeft" ], mode : "shorthand", themeable : true } }, members : { __ptwLiveUpdater : null, __ptwLiveDisappearListener : null, __ptwLiveUpdateDisappearListener : null, /** * Returns the location data like {qx.bom.element.Location#get} does, * but does not rely on DOM elements coordinates to be rendered. Instead, * this method works with the available layout data available in the moment * when it is executed. * This works best when called in some type of <code>resize</code> or * <code>move</code> event which are supported by all widgets out of the * box. * * @param widget {qx.ui.core.Widget} Any widget * @return {Map|null} Returns a map with <code>left</code>, <code>top</code>, * <code>right</code> and <code>bottom</code> which contains the distance * of the widget relative coords the document. */ getLayoutLocation : function(widget) { // Use post-layout dimensions // which do not rely on the final rendered DOM element var insets, bounds, left, top; // Add bounds of the widget itself bounds = widget.getBounds(); if (!bounds) { return null; } left = bounds.left; top = bounds.top; // Keep size to protect it for loop var size = bounds; // Now loop up with parents until reaching the root widget = widget.getLayoutParent(); while (widget && !widget.isRootWidget()) { // Add coordinates bounds = widget.getBounds(); left += bounds.left; top += bounds.top; // Add insets insets = widget.getInsets(); left += insets.left; top += insets.top; // Next parent widget = widget.getLayoutParent(); } // Add the rendered location of the root widget if (widget && widget.isRootWidget()) { var rootCoords = widget.getContentLocation(); if (rootCoords) { left += rootCoords.left; top += rootCoords.top; } } // Build location data return { left : left, top : top, right : left + size.width, bottom : top + size.height }; }, /** * Sets the position. Uses low-level, high-performance DOM * methods when the property {@link #domMove} is enabled. * Checks if an always visible element is set and moves the widget to not * overlay the always visible widget if possible. The algorithm tries to * move the widget as far left as necessary but not of the screen. * ({@link #setVisibleElement}) * * @param left {Integer} The left position * @param top {Integer} The top position */ moveTo : function(left, top) { var visible = qx.ui.core.MPlacement.getVisibleElement(); // if we have an always visible element if (visible) { var bounds = this.getBounds(); var elemLocation = visible.getContentLocation(); // if we have bounds for both elements if (bounds && elemLocation) { var bottom = top + bounds.height; var right = left + bounds.width; // horizontal placement wrong // each number is for the upcomming check (huge element is // the always visible, eleme prefixed) // | 3 | // --------- // | |---| | // | | // --|-| |-|-- // 1 | | | | 2 // --|-| |-|-- // | | // | |---| | // --------- // | 4 | if ( (right > elemLocation.left && left < elemLocation.right) && (bottom > elemLocation.top && top < elemLocation.bottom) ) { var direction = qx.ui.core.MPlacement.getMoveDirection(); if (direction === "left") { left = Math.max(elemLocation.left - bounds.width, 0); } else { top = Math.max(elemLocation.top - bounds.height, 0); } } } } if (this.getDomMove()) { this.setDomPosition(left, top); } else { this.setLayoutProperties({left: left, top: top}); } }, /** * Places the widget to another (at least laid out) widget. The DOM * element is not needed, but the bounds are needed to compute the * location of the widget to align to. * * @param target {qx.ui.core.Widget} Target coords align coords * @param liveupdate {Boolean} Flag indicating if the position of the * widget should be checked and corrected automatically. * @return {Boolean} true if the widget was successfully placed */ placeToWidget : function(target, liveupdate) { // Use the idle event to make sure that the widget's position gets // updated automatically (e.g. the widget gets scrolled). if (liveupdate) { this.__cleanupFromLastPlaceToWidgetLiveUpdate(); // Bind target and livupdate to placeToWidget this.__ptwLiveUpdater = qx.lang.Function.bind(this.placeToWidget, this, target, false); qx.event.Idle.getInstance().addListener("interval", this.__ptwLiveUpdater); // Remove the listener when the element disappears. this.__ptwLiveUpdateDisappearListener = function() { this.__cleanupFromLastPlaceToWidgetLiveUpdate(); }; this.addListener("disappear", this.__ptwLiveUpdateDisappearListener, this); } var coords = target.getContentLocation() || this.getLayoutLocation(target); if(coords != null) { this._place(coords); return true; } else { return false; } }, /** * Removes all resources allocated by the last run of placeToWidget with liveupdate=true */ __cleanupFromLastPlaceToWidgetLiveUpdate : function() { if (this.__ptwLiveUpdater) { qx.event.Idle.getInstance().removeListener("interval", this.__ptwLiveUpdater); this.__ptwLiveUpdater = null; } if (this.__ptwLiveUpdateDisappearListener){ this.removeListener("disappear", this.__ptwLiveUpdateDisappearListener, this); this.__ptwLiveUpdateDisappearListener = null; } }, /** * Places the widget to the pointer position. * * @param event {qx.event.type.Pointer} Pointer event to align to */ placeToPointer : function(event) { var left = Math.round(event.getDocumentLeft()); var top = Math.round(event.getDocumentTop()); var coords = { left: left, top: top, right: left, bottom: top }; this._place(coords); }, /** * Places the widget to any (rendered) DOM element. * * @param elem {Element} DOM element to align to * @param liveupdate {Boolean} Flag indicating if the position of the * widget should be checked and corrected automatically. */ placeToElement : function(elem, liveupdate) { var location = qx.bom.element.Location.get(elem); var coords = { left: location.left, top: location.top, right: location.left + elem.offsetWidth, bottom: location.top + elem.offsetHeight }; // Use the idle event to make sure that the widget's position gets // updated automatically (e.g. the widget gets scrolled). if (liveupdate) { // Bind target and livupdate to placeToWidget this.__ptwLiveUpdater = qx.lang.Function.bind(this.placeToElement, this, elem, false); qx.event.Idle.getInstance().addListener("interval", this.__ptwLiveUpdater); // Remove the listener when the element disappears. this.addListener("disappear", function() { if (this.__ptwLiveUpdater) { qx.event.Idle.getInstance().removeListener("interval", this.__ptwLiveUpdater); this.__ptwLiveUpdater = null; } }, this); } this._place(coords); }, /** * Places the widget in relation to the given point * * @param point {Map} Coordinate of any point with the keys <code>left</code> * and <code>top</code>. */ placeToPoint : function(point) { var coords = { left: point.left, top: point.top, right: point.left, bottom: point.top }; this._place(coords); }, /** * Returns the placement offsets as a map * * @return {Map} The placement offsets */ _getPlacementOffsets : function() { return { left : this.getOffsetLeft(), top : this.getOffsetTop(), right : this.getOffsetRight(), bottom : this.getOffsetBottom() }; }, /** * Get the size of the object to place. The callback will be called with * the size as first argument. This methods works asynchronously. * * The size of the object to place is the size of the widget. If a widget * including this mixin needs a different size it can implement the method * <code>_computePlacementSize</code>, which returns the size. * * @param callback {Function} This function will be called with the size as * first argument */ __getPlacementSize : function(callback) { var size = null; if (this._computePlacementSize) { var size = this._computePlacementSize(); } else if (this.isVisible()) { var size = this.getBounds(); } if (size == null) { this.addListenerOnce("appear", function() { this.__getPlacementSize(callback); }, this); } else { callback.call(this, size); } }, /** * Internal method to read specific this properties and * apply the results to the this afterwards. * * @param coords {Map} Location of the object to align the this to. This map * should have the keys <code>left</code>, <code>top</code>, <code>right</code> * and <code>bottom</code>. */ _place : function(coords) { this.__getPlacementSize(function(size) { var result = qx.util.placement.Placement.compute( size, this.getLayoutParent().getBounds(), coords, this._getPlacementOffsets(), this.getPosition(), this.getPlacementModeX(), this.getPlacementModeY() ); // state handling for tooltips e.g. this.removeState("placementLeft"); this.removeState("placementRight"); this.addState(coords.left < result.left ? "placementRight" : "placementLeft"); this.moveTo(result.left, result.top); }); } }, destruct : function() { this.__cleanupFromLastPlaceToWidgetLiveUpdate(); } });