UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

586 lines (516 loc) 16.8 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(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() { 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(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() { 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, /**@type {Record<"top" | "right" | "bottom" | "left", number> | null}*/ __lastKnownCoords: 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(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(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(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) { if ( coords.top === this.__lastKnownCoords?.top && coords.right === this.__lastKnownCoords?.right && coords.bottom === this.__lastKnownCoords?.bottom && coords.left === this.__lastKnownCoords?.left ) { return true; } this.__lastKnownCoords = coords; this._place(coords); return true; } else { return false; } }, /** * Removes all resources allocated by the last run of placeToWidget with liveupdate=true */ __cleanupFromLastPlaceToWidgetLiveUpdate() { 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(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(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", () => { if (this.__ptwLiveUpdater) { qx.event.Idle.getInstance().removeListener( "interval", this.__ptwLiveUpdater ); this.__ptwLiveUpdater = null; } }); } 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(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() { 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(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", () => { this.__getPlacementSize(callback); }); } 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(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() { this.__cleanupFromLastPlaceToWidgetLiveUpdate(); } });