|  | /* | 
|  | * Licensed to the Apache Software Foundation (ASF) under one | 
|  | * or more contributor license agreements.  See the NOTICE file | 
|  | * distributed with this work for additional information | 
|  | * regarding copyright ownership.  The ASF licenses this file | 
|  | * to you under the Apache License, Version 2.0 (the | 
|  | * "License"); you may not use this file except in compliance | 
|  | * with the License.  You may obtain a copy of the License at | 
|  | * | 
|  | *   http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, | 
|  | * software distributed under the License is distributed on an | 
|  | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | * KIND, either express or implied.  See the License for the | 
|  | * specific language governing permissions and limitations | 
|  | * under the License. | 
|  | */ | 
|  |  | 
|  | // Layout helpers for each component positioning | 
|  |  | 
|  | import * as zrUtil from 'zrender/src/core/util'; | 
|  | import BoundingRect from 'zrender/src/core/BoundingRect'; | 
|  | import {parsePercent} from './number'; | 
|  | import * as formatUtil from './format'; | 
|  | import { BoxLayoutOptionMixin, ComponentLayoutMode } from './types'; | 
|  | import Group from 'zrender/src/graphic/Group'; | 
|  | import Element from 'zrender/src/Element'; | 
|  | import { Dictionary } from 'zrender/src/core/types'; | 
|  |  | 
|  | const each = zrUtil.each; | 
|  |  | 
|  | export interface LayoutRect extends BoundingRect { | 
|  | margin: number[] | 
|  | } | 
|  |  | 
|  | export interface NewlineElement extends Element { | 
|  | newline: boolean | 
|  | } | 
|  |  | 
|  | type BoxLayoutKeys = keyof BoxLayoutOptionMixin; | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | export const LOCATION_PARAMS = [ | 
|  | 'left', 'right', 'top', 'bottom', 'width', 'height' | 
|  | ] as const; | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | */ | 
|  | export const HV_NAMES = [ | 
|  | ['width', 'left', 'right'], | 
|  | ['height', 'top', 'bottom'] | 
|  | ] as const; | 
|  |  | 
|  | function boxLayout( | 
|  | orient: 'horizontal' | 'vertical', | 
|  | group: Group, | 
|  | gap: number, | 
|  | maxWidth?: number, | 
|  | maxHeight?: number | 
|  | ) { | 
|  | let x = 0; | 
|  | let y = 0; | 
|  |  | 
|  | if (maxWidth == null) { | 
|  | maxWidth = Infinity; | 
|  | } | 
|  | if (maxHeight == null) { | 
|  | maxHeight = Infinity; | 
|  | } | 
|  | let currentLineMaxSize = 0; | 
|  |  | 
|  | group.eachChild(function (child, idx) { | 
|  | const rect = child.getBoundingRect(); | 
|  | const nextChild = group.childAt(idx + 1); | 
|  | const nextChildRect = nextChild && nextChild.getBoundingRect(); | 
|  | let nextX: number; | 
|  | let nextY: number; | 
|  |  | 
|  | if (orient === 'horizontal') { | 
|  | const moveX = rect.width + (nextChildRect ? (-nextChildRect.x + rect.x) : 0); | 
|  | nextX = x + moveX; | 
|  | // Wrap when width exceeds maxWidth or meet a `newline` group | 
|  | // FIXME compare before adding gap? | 
|  | if (nextX > maxWidth || (child as NewlineElement).newline) { | 
|  | x = 0; | 
|  | nextX = moveX; | 
|  | y += currentLineMaxSize + gap; | 
|  | currentLineMaxSize = rect.height; | 
|  | } | 
|  | else { | 
|  | // FIXME: consider rect.y is not `0`? | 
|  | currentLineMaxSize = Math.max(currentLineMaxSize, rect.height); | 
|  | } | 
|  | } | 
|  | else { | 
|  | const moveY = rect.height + (nextChildRect ? (-nextChildRect.y + rect.y) : 0); | 
|  | nextY = y + moveY; | 
|  | // Wrap when width exceeds maxHeight or meet a `newline` group | 
|  | if (nextY > maxHeight || (child as NewlineElement).newline) { | 
|  | x += currentLineMaxSize + gap; | 
|  | y = 0; | 
|  | nextY = moveY; | 
|  | currentLineMaxSize = rect.width; | 
|  | } | 
|  | else { | 
|  | currentLineMaxSize = Math.max(currentLineMaxSize, rect.width); | 
|  | } | 
|  | } | 
|  |  | 
|  | if ((child as NewlineElement).newline) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | child.x = x; | 
|  | child.y = y; | 
|  | child.markRedraw(); | 
|  |  | 
|  | orient === 'horizontal' | 
|  | ? (x = nextX + gap) | 
|  | : (y = nextY + gap); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * VBox or HBox layouting | 
|  | * @param {string} orient | 
|  | * @param {module:zrender/graphic/Group} group | 
|  | * @param {number} gap | 
|  | * @param {number} [width=Infinity] | 
|  | * @param {number} [height=Infinity] | 
|  | */ | 
|  | export const box = boxLayout; | 
|  |  | 
|  | /** | 
|  | * VBox layouting | 
|  | * @param {module:zrender/graphic/Group} group | 
|  | * @param {number} gap | 
|  | * @param {number} [width=Infinity] | 
|  | * @param {number} [height=Infinity] | 
|  | */ | 
|  | export const vbox = zrUtil.curry(boxLayout, 'vertical'); | 
|  |  | 
|  | /** | 
|  | * HBox layouting | 
|  | * @param {module:zrender/graphic/Group} group | 
|  | * @param {number} gap | 
|  | * @param {number} [width=Infinity] | 
|  | * @param {number} [height=Infinity] | 
|  | */ | 
|  | export const hbox = zrUtil.curry(boxLayout, 'horizontal'); | 
|  |  | 
|  | /** | 
|  | * If x or x2 is not specified or 'center' 'left' 'right', | 
|  | * the width would be as long as possible. | 
|  | * If y or y2 is not specified or 'middle' 'top' 'bottom', | 
|  | * the height would be as long as possible. | 
|  | */ | 
|  | export function getAvailableSize( | 
|  | positionInfo: { | 
|  | left?: number | string | 
|  | top?: number | string | 
|  | right?: number | string | 
|  | bottom?: number | string | 
|  | }, | 
|  | containerRect: { width: number, height: number }, | 
|  | margin?: number[] | number | 
|  | ) { | 
|  | const containerWidth = containerRect.width; | 
|  | const containerHeight = containerRect.height; | 
|  |  | 
|  | let x = parsePercent(positionInfo.left, containerWidth); | 
|  | let y = parsePercent(positionInfo.top, containerHeight); | 
|  | let x2 = parsePercent(positionInfo.right, containerWidth); | 
|  | let y2 = parsePercent(positionInfo.bottom, containerHeight); | 
|  |  | 
|  | (isNaN(x) || isNaN(parseFloat(positionInfo.left as string))) && (x = 0); | 
|  | (isNaN(x2) || isNaN(parseFloat(positionInfo.right as string))) && (x2 = containerWidth); | 
|  | (isNaN(y) || isNaN(parseFloat(positionInfo.top as string))) && (y = 0); | 
|  | (isNaN(y2) || isNaN(parseFloat(positionInfo.bottom as string))) && (y2 = containerHeight); | 
|  |  | 
|  | margin = formatUtil.normalizeCssArray(margin || 0); | 
|  |  | 
|  | return { | 
|  | width: Math.max(x2 - x - margin[1] - margin[3], 0), | 
|  | height: Math.max(y2 - y - margin[0] - margin[2], 0) | 
|  | }; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Parse position info. | 
|  | */ | 
|  | export function getLayoutRect( | 
|  | positionInfo: BoxLayoutOptionMixin & { | 
|  | aspect?: number // aspect is width / height | 
|  | }, | 
|  | containerRect: {width: number, height: number}, | 
|  | margin?: number | number[] | 
|  | ): LayoutRect { | 
|  | margin = formatUtil.normalizeCssArray(margin || 0); | 
|  |  | 
|  | const containerWidth = containerRect.width; | 
|  | const containerHeight = containerRect.height; | 
|  |  | 
|  | let left = parsePercent(positionInfo.left, containerWidth); | 
|  | let top = parsePercent(positionInfo.top, containerHeight); | 
|  | const right = parsePercent(positionInfo.right, containerWidth); | 
|  | const bottom = parsePercent(positionInfo.bottom, containerHeight); | 
|  | let width = parsePercent(positionInfo.width, containerWidth); | 
|  | let height = parsePercent(positionInfo.height, containerHeight); | 
|  |  | 
|  | const verticalMargin = margin[2] + margin[0]; | 
|  | const horizontalMargin = margin[1] + margin[3]; | 
|  | const aspect = positionInfo.aspect; | 
|  |  | 
|  | // If width is not specified, calculate width from left and right | 
|  | if (isNaN(width)) { | 
|  | width = containerWidth - right - horizontalMargin - left; | 
|  | } | 
|  | if (isNaN(height)) { | 
|  | height = containerHeight - bottom - verticalMargin - top; | 
|  | } | 
|  |  | 
|  | if (aspect != null) { | 
|  | // If width and height are not given | 
|  | // 1. Graph should not exceeds the container | 
|  | // 2. Aspect must be keeped | 
|  | // 3. Graph should take the space as more as possible | 
|  | // FIXME | 
|  | // Margin is not considered, because there is no case that both | 
|  | // using margin and aspect so far. | 
|  | if (isNaN(width) && isNaN(height)) { | 
|  | if (aspect > containerWidth / containerHeight) { | 
|  | width = containerWidth * 0.8; | 
|  | } | 
|  | else { | 
|  | height = containerHeight * 0.8; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Calculate width or height with given aspect | 
|  | if (isNaN(width)) { | 
|  | width = aspect * height; | 
|  | } | 
|  | if (isNaN(height)) { | 
|  | height = width / aspect; | 
|  | } | 
|  | } | 
|  |  | 
|  | // If left is not specified, calculate left from right and width | 
|  | if (isNaN(left)) { | 
|  | left = containerWidth - right - width - horizontalMargin; | 
|  | } | 
|  | if (isNaN(top)) { | 
|  | top = containerHeight - bottom - height - verticalMargin; | 
|  | } | 
|  |  | 
|  | // Align left and top | 
|  | switch (positionInfo.left || positionInfo.right) { | 
|  | case 'center': | 
|  | left = containerWidth / 2 - width / 2 - margin[3]; | 
|  | break; | 
|  | case 'right': | 
|  | left = containerWidth - width - horizontalMargin; | 
|  | break; | 
|  | } | 
|  | switch (positionInfo.top || positionInfo.bottom) { | 
|  | case 'middle': | 
|  | case 'center': | 
|  | top = containerHeight / 2 - height / 2 - margin[0]; | 
|  | break; | 
|  | case 'bottom': | 
|  | top = containerHeight - height - verticalMargin; | 
|  | break; | 
|  | } | 
|  | // If something is wrong and left, top, width, height are calculated as NaN | 
|  | left = left || 0; | 
|  | top = top || 0; | 
|  | if (isNaN(width)) { | 
|  | // Width may be NaN if only one value is given except width | 
|  | width = containerWidth - horizontalMargin - left - (right || 0); | 
|  | } | 
|  | if (isNaN(height)) { | 
|  | // Height may be NaN if only one value is given except height | 
|  | height = containerHeight - verticalMargin - top - (bottom || 0); | 
|  | } | 
|  |  | 
|  | const rect = new BoundingRect(left + margin[3], top + margin[0], width, height) as LayoutRect; | 
|  | rect.margin = margin; | 
|  | return rect; | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Position a zr element in viewport | 
|  | *  Group position is specified by either | 
|  | *  {left, top}, {right, bottom} | 
|  | *  If all properties exists, right and bottom will be igonred. | 
|  | * | 
|  | * Logic: | 
|  | *     1. Scale (against origin point in parent coord) | 
|  | *     2. Rotate (against origin point in parent coord) | 
|  | *     3. Translate (with el.position by this method) | 
|  | * So this method only fixes the last step 'Translate', which does not affect | 
|  | * scaling and rotating. | 
|  | * | 
|  | * If be called repeatedly with the same input el, the same result will be gotten. | 
|  | * | 
|  | * Return true if the layout happened. | 
|  | * | 
|  | * @param el Should have `getBoundingRect` method. | 
|  | * @param positionInfo | 
|  | * @param positionInfo.left | 
|  | * @param positionInfo.top | 
|  | * @param positionInfo.right | 
|  | * @param positionInfo.bottom | 
|  | * @param positionInfo.width Only for opt.boundingModel: 'raw' | 
|  | * @param positionInfo.height Only for opt.boundingModel: 'raw' | 
|  | * @param containerRect | 
|  | * @param margin | 
|  | * @param opt | 
|  | * @param opt.hv Only horizontal or only vertical. Default to be [1, 1] | 
|  | * @param opt.boundingMode | 
|  | *        Specify how to calculate boundingRect when locating. | 
|  | *        'all': Position the boundingRect that is transformed and uioned | 
|  | *               both itself and its descendants. | 
|  | *               This mode simplies confine the elements in the bounding | 
|  | *               of their container (e.g., using 'right: 0'). | 
|  | *        'raw': Position the boundingRect that is not transformed and only itself. | 
|  | *               This mode is useful when you want a element can overflow its | 
|  | *               container. (Consider a rotated circle needs to be located in a corner.) | 
|  | *               In this mode positionInfo.width/height can only be number. | 
|  | */ | 
|  | export function positionElement( | 
|  | el: Element, | 
|  | positionInfo: BoxLayoutOptionMixin, | 
|  | containerRect: {width: number, height: number}, | 
|  | margin?: number[] | number, | 
|  | opt?: { | 
|  | hv: [1 | 0 | boolean, 1 | 0 | boolean], | 
|  | boundingMode: 'all' | 'raw' | 
|  | }, | 
|  | out?: { x?: number, y?: number } | 
|  | ): boolean { | 
|  | const h = !opt || !opt.hv || opt.hv[0]; | 
|  | const v = !opt || !opt.hv || opt.hv[1]; | 
|  | const boundingMode = opt && opt.boundingMode || 'all'; | 
|  | out = out || el; | 
|  |  | 
|  | out.x = el.x; | 
|  | out.y = el.y; | 
|  |  | 
|  | if (!h && !v) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | let rect; | 
|  | if (boundingMode === 'raw') { | 
|  | rect = el.type === 'group' | 
|  | ? new BoundingRect(0, 0, +positionInfo.width || 0, +positionInfo.height || 0) | 
|  | : el.getBoundingRect(); | 
|  | } | 
|  | else { | 
|  | rect = el.getBoundingRect(); | 
|  | if (el.needLocalTransform()) { | 
|  | const transform = el.getLocalTransform(); | 
|  | // Notice: raw rect may be inner object of el, | 
|  | // which should not be modified. | 
|  | rect = rect.clone(); | 
|  | rect.applyTransform(transform); | 
|  | } | 
|  | } | 
|  |  | 
|  | // The real width and height can not be specified but calculated by the given el. | 
|  | const layoutRect = getLayoutRect( | 
|  | zrUtil.defaults( | 
|  | {width: rect.width, height: rect.height}, | 
|  | positionInfo | 
|  | ), | 
|  | containerRect, | 
|  | margin | 
|  | ); | 
|  |  | 
|  | // Because 'tranlate' is the last step in transform | 
|  | // (see zrender/core/Transformable#getLocalTransform), | 
|  | // we can just only modify el.position to get final result. | 
|  | const dx = h ? layoutRect.x - rect.x : 0; | 
|  | const dy = v ? layoutRect.y - rect.y : 0; | 
|  |  | 
|  | if (boundingMode === 'raw') { | 
|  | out.x = dx; | 
|  | out.y = dy; | 
|  | } | 
|  | else { | 
|  | out.x += dx; | 
|  | out.y += dy; | 
|  | } | 
|  | if (out === el) { | 
|  | el.markRedraw(); | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param option Contains some of the properties in HV_NAMES. | 
|  | * @param hvIdx 0: horizontal; 1: vertical. | 
|  | */ | 
|  | export function sizeCalculable(option: BoxLayoutOptionMixin, hvIdx: number): boolean { | 
|  | return option[HV_NAMES[hvIdx][0]] != null | 
|  | || (option[HV_NAMES[hvIdx][1]] != null && option[HV_NAMES[hvIdx][2]] != null); | 
|  | } | 
|  |  | 
|  | export function fetchLayoutMode(ins: any): ComponentLayoutMode { | 
|  | const layoutMode = ins.layoutMode || ins.constructor.layoutMode; | 
|  | return zrUtil.isObject(layoutMode) | 
|  | ? layoutMode | 
|  | : layoutMode | 
|  | ? {type: layoutMode} | 
|  | : null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Consider Case: | 
|  | * When default option has {left: 0, width: 100}, and we set {right: 0} | 
|  | * through setOption or media query, using normal zrUtil.merge will cause | 
|  | * {right: 0} does not take effect. | 
|  | * | 
|  | * @example | 
|  | * ComponentModel.extend({ | 
|  | *     init: function () { | 
|  | *         ... | 
|  | *         let inputPositionParams = layout.getLayoutParams(option); | 
|  | *         this.mergeOption(inputPositionParams); | 
|  | *     }, | 
|  | *     mergeOption: function (newOption) { | 
|  | *         newOption && zrUtil.merge(thisOption, newOption, true); | 
|  | *         layout.mergeLayoutParam(thisOption, newOption); | 
|  | *     } | 
|  | * }); | 
|  | * | 
|  | * @param targetOption | 
|  | * @param newOption | 
|  | * @param opt | 
|  | */ | 
|  | export function mergeLayoutParam<T extends BoxLayoutOptionMixin>( | 
|  | targetOption: T, | 
|  | newOption: T, | 
|  | opt?: ComponentLayoutMode | 
|  | ) { | 
|  | let ignoreSize = opt && opt.ignoreSize; | 
|  | !zrUtil.isArray(ignoreSize) && (ignoreSize = [ignoreSize, ignoreSize]); | 
|  |  | 
|  | const hResult = merge(HV_NAMES[0], 0); | 
|  | const vResult = merge(HV_NAMES[1], 1); | 
|  |  | 
|  | copy(HV_NAMES[0], targetOption, hResult); | 
|  | copy(HV_NAMES[1], targetOption, vResult); | 
|  |  | 
|  | function merge(names: typeof HV_NAMES[number], hvIdx: number) { | 
|  | const newParams: BoxLayoutOptionMixin = {}; | 
|  | let newValueCount = 0; | 
|  | const merged: BoxLayoutOptionMixin = {}; | 
|  | let mergedValueCount = 0; | 
|  | const enoughParamNumber = 2; | 
|  |  | 
|  | each(names, function (name: BoxLayoutKeys) { | 
|  | merged[name] = targetOption[name]; | 
|  | }); | 
|  | each(names, function (name: BoxLayoutKeys) { | 
|  | // Consider case: newOption.width is null, which is | 
|  | // set by user for removing width setting. | 
|  | hasProp(newOption, name) && (newParams[name] = merged[name] = newOption[name]); | 
|  | hasValue(newParams, name) && newValueCount++; | 
|  | hasValue(merged, name) && mergedValueCount++; | 
|  | }); | 
|  |  | 
|  | if ((ignoreSize as [boolean, boolean])[hvIdx]) { | 
|  | // Only one of left/right is premitted to exist. | 
|  | if (hasValue(newOption, names[1])) { | 
|  | merged[names[2]] = null; | 
|  | } | 
|  | else if (hasValue(newOption, names[2])) { | 
|  | merged[names[1]] = null; | 
|  | } | 
|  | return merged; | 
|  | } | 
|  |  | 
|  | // Case: newOption: {width: ..., right: ...}, | 
|  | // or targetOption: {right: ...} and newOption: {width: ...}, | 
|  | // There is no conflict when merged only has params count | 
|  | // little than enoughParamNumber. | 
|  | if (mergedValueCount === enoughParamNumber || !newValueCount) { | 
|  | return merged; | 
|  | } | 
|  | // Case: newOption: {width: ..., right: ...}, | 
|  | // Than we can make sure user only want those two, and ignore | 
|  | // all origin params in targetOption. | 
|  | else if (newValueCount >= enoughParamNumber) { | 
|  | return newParams; | 
|  | } | 
|  | else { | 
|  | // Chose another param from targetOption by priority. | 
|  | for (let i = 0; i < names.length; i++) { | 
|  | const name = names[i]; | 
|  | if (!hasProp(newParams, name) && hasProp(targetOption, name)) { | 
|  | newParams[name] = targetOption[name]; | 
|  | break; | 
|  | } | 
|  | } | 
|  | return newParams; | 
|  | } | 
|  | } | 
|  |  | 
|  | function hasProp(obj: object, name: string): boolean { | 
|  | return obj.hasOwnProperty(name); | 
|  | } | 
|  |  | 
|  | function hasValue(obj: Dictionary<any>, name: string): boolean { | 
|  | return obj[name] != null && obj[name] !== 'auto'; | 
|  | } | 
|  |  | 
|  | function copy(names: readonly string[], target: Dictionary<any>, source: Dictionary<any>) { | 
|  | each(names, function (name) { | 
|  | target[name] = source[name]; | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object. | 
|  | */ | 
|  | export function getLayoutParams(source: BoxLayoutOptionMixin): BoxLayoutOptionMixin { | 
|  | return copyLayoutParams({}, source); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object. | 
|  | * @param {Object} source | 
|  | * @return {Object} Result contains those props. | 
|  | */ | 
|  | export function copyLayoutParams(target: BoxLayoutOptionMixin, source: BoxLayoutOptionMixin): BoxLayoutOptionMixin { | 
|  | source && target && each(LOCATION_PARAMS, function (name: BoxLayoutKeys) { | 
|  | source.hasOwnProperty(name) && (target[name] = source[name]); | 
|  | }); | 
|  | return target; | 
|  | } |