layout.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. // Layout helpers for each component positioning
  20. import * as zrUtil from 'zrender/src/core/util';
  21. import BoundingRect from 'zrender/src/core/BoundingRect';
  22. import {parsePercent} from './number';
  23. import * as formatUtil from './format';
  24. var each = zrUtil.each;
  25. /**
  26. * @public
  27. */
  28. export var LOCATION_PARAMS = [
  29. 'left', 'right', 'top', 'bottom', 'width', 'height'
  30. ];
  31. /**
  32. * @public
  33. */
  34. export var HV_NAMES = [
  35. ['width', 'left', 'right'],
  36. ['height', 'top', 'bottom']
  37. ];
  38. function boxLayout(orient, group, gap, maxWidth, maxHeight) {
  39. var x = 0;
  40. var y = 0;
  41. if (maxWidth == null) {
  42. maxWidth = Infinity;
  43. }
  44. if (maxHeight == null) {
  45. maxHeight = Infinity;
  46. }
  47. var currentLineMaxSize = 0;
  48. group.eachChild(function (child, idx) {
  49. var position = child.position;
  50. var rect = child.getBoundingRect();
  51. var nextChild = group.childAt(idx + 1);
  52. var nextChildRect = nextChild && nextChild.getBoundingRect();
  53. var nextX;
  54. var nextY;
  55. if (orient === 'horizontal') {
  56. var moveX = rect.width + (nextChildRect ? (-nextChildRect.x + rect.x) : 0);
  57. nextX = x + moveX;
  58. // Wrap when width exceeds maxWidth or meet a `newline` group
  59. // FIXME compare before adding gap?
  60. if (nextX > maxWidth || child.newline) {
  61. x = 0;
  62. nextX = moveX;
  63. y += currentLineMaxSize + gap;
  64. currentLineMaxSize = rect.height;
  65. }
  66. else {
  67. // FIXME: consider rect.y is not `0`?
  68. currentLineMaxSize = Math.max(currentLineMaxSize, rect.height);
  69. }
  70. }
  71. else {
  72. var moveY = rect.height + (nextChildRect ? (-nextChildRect.y + rect.y) : 0);
  73. nextY = y + moveY;
  74. // Wrap when width exceeds maxHeight or meet a `newline` group
  75. if (nextY > maxHeight || child.newline) {
  76. x += currentLineMaxSize + gap;
  77. y = 0;
  78. nextY = moveY;
  79. currentLineMaxSize = rect.width;
  80. }
  81. else {
  82. currentLineMaxSize = Math.max(currentLineMaxSize, rect.width);
  83. }
  84. }
  85. if (child.newline) {
  86. return;
  87. }
  88. position[0] = x;
  89. position[1] = y;
  90. orient === 'horizontal'
  91. ? (x = nextX + gap)
  92. : (y = nextY + gap);
  93. });
  94. }
  95. /**
  96. * VBox or HBox layouting
  97. * @param {string} orient
  98. * @param {module:zrender/container/Group} group
  99. * @param {number} gap
  100. * @param {number} [width=Infinity]
  101. * @param {number} [height=Infinity]
  102. */
  103. export var box = boxLayout;
  104. /**
  105. * VBox layouting
  106. * @param {module:zrender/container/Group} group
  107. * @param {number} gap
  108. * @param {number} [width=Infinity]
  109. * @param {number} [height=Infinity]
  110. */
  111. export var vbox = zrUtil.curry(boxLayout, 'vertical');
  112. /**
  113. * HBox layouting
  114. * @param {module:zrender/container/Group} group
  115. * @param {number} gap
  116. * @param {number} [width=Infinity]
  117. * @param {number} [height=Infinity]
  118. */
  119. export var hbox = zrUtil.curry(boxLayout, 'horizontal');
  120. /**
  121. * If x or x2 is not specified or 'center' 'left' 'right',
  122. * the width would be as long as possible.
  123. * If y or y2 is not specified or 'middle' 'top' 'bottom',
  124. * the height would be as long as possible.
  125. *
  126. * @param {Object} positionInfo
  127. * @param {number|string} [positionInfo.x]
  128. * @param {number|string} [positionInfo.y]
  129. * @param {number|string} [positionInfo.x2]
  130. * @param {number|string} [positionInfo.y2]
  131. * @param {Object} containerRect {width, height}
  132. * @param {string|number} margin
  133. * @return {Object} {width, height}
  134. */
  135. export function getAvailableSize(positionInfo, containerRect, margin) {
  136. var containerWidth = containerRect.width;
  137. var containerHeight = containerRect.height;
  138. var x = parsePercent(positionInfo.x, containerWidth);
  139. var y = parsePercent(positionInfo.y, containerHeight);
  140. var x2 = parsePercent(positionInfo.x2, containerWidth);
  141. var y2 = parsePercent(positionInfo.y2, containerHeight);
  142. (isNaN(x) || isNaN(parseFloat(positionInfo.x))) && (x = 0);
  143. (isNaN(x2) || isNaN(parseFloat(positionInfo.x2))) && (x2 = containerWidth);
  144. (isNaN(y) || isNaN(parseFloat(positionInfo.y))) && (y = 0);
  145. (isNaN(y2) || isNaN(parseFloat(positionInfo.y2))) && (y2 = containerHeight);
  146. margin = formatUtil.normalizeCssArray(margin || 0);
  147. return {
  148. width: Math.max(x2 - x - margin[1] - margin[3], 0),
  149. height: Math.max(y2 - y - margin[0] - margin[2], 0)
  150. };
  151. }
  152. /**
  153. * Parse position info.
  154. *
  155. * @param {Object} positionInfo
  156. * @param {number|string} [positionInfo.left]
  157. * @param {number|string} [positionInfo.top]
  158. * @param {number|string} [positionInfo.right]
  159. * @param {number|string} [positionInfo.bottom]
  160. * @param {number|string} [positionInfo.width]
  161. * @param {number|string} [positionInfo.height]
  162. * @param {number|string} [positionInfo.aspect] Aspect is width / height
  163. * @param {Object} containerRect
  164. * @param {string|number} [margin]
  165. *
  166. * @return {module:zrender/core/BoundingRect}
  167. */
  168. export function getLayoutRect(
  169. positionInfo, containerRect, margin
  170. ) {
  171. margin = formatUtil.normalizeCssArray(margin || 0);
  172. var containerWidth = containerRect.width;
  173. var containerHeight = containerRect.height;
  174. var left = parsePercent(positionInfo.left, containerWidth);
  175. var top = parsePercent(positionInfo.top, containerHeight);
  176. var right = parsePercent(positionInfo.right, containerWidth);
  177. var bottom = parsePercent(positionInfo.bottom, containerHeight);
  178. var width = parsePercent(positionInfo.width, containerWidth);
  179. var height = parsePercent(positionInfo.height, containerHeight);
  180. var verticalMargin = margin[2] + margin[0];
  181. var horizontalMargin = margin[1] + margin[3];
  182. var aspect = positionInfo.aspect;
  183. // If width is not specified, calculate width from left and right
  184. if (isNaN(width)) {
  185. width = containerWidth - right - horizontalMargin - left;
  186. }
  187. if (isNaN(height)) {
  188. height = containerHeight - bottom - verticalMargin - top;
  189. }
  190. if (aspect != null) {
  191. // If width and height are not given
  192. // 1. Graph should not exceeds the container
  193. // 2. Aspect must be keeped
  194. // 3. Graph should take the space as more as possible
  195. // FIXME
  196. // Margin is not considered, because there is no case that both
  197. // using margin and aspect so far.
  198. if (isNaN(width) && isNaN(height)) {
  199. if (aspect > containerWidth / containerHeight) {
  200. width = containerWidth * 0.8;
  201. }
  202. else {
  203. height = containerHeight * 0.8;
  204. }
  205. }
  206. // Calculate width or height with given aspect
  207. if (isNaN(width)) {
  208. width = aspect * height;
  209. }
  210. if (isNaN(height)) {
  211. height = width / aspect;
  212. }
  213. }
  214. // If left is not specified, calculate left from right and width
  215. if (isNaN(left)) {
  216. left = containerWidth - right - width - horizontalMargin;
  217. }
  218. if (isNaN(top)) {
  219. top = containerHeight - bottom - height - verticalMargin;
  220. }
  221. // Align left and top
  222. switch (positionInfo.left || positionInfo.right) {
  223. case 'center':
  224. left = containerWidth / 2 - width / 2 - margin[3];
  225. break;
  226. case 'right':
  227. left = containerWidth - width - horizontalMargin;
  228. break;
  229. }
  230. switch (positionInfo.top || positionInfo.bottom) {
  231. case 'middle':
  232. case 'center':
  233. top = containerHeight / 2 - height / 2 - margin[0];
  234. break;
  235. case 'bottom':
  236. top = containerHeight - height - verticalMargin;
  237. break;
  238. }
  239. // If something is wrong and left, top, width, height are calculated as NaN
  240. left = left || 0;
  241. top = top || 0;
  242. if (isNaN(width)) {
  243. // Width may be NaN if only one value is given except width
  244. width = containerWidth - horizontalMargin - left - (right || 0);
  245. }
  246. if (isNaN(height)) {
  247. // Height may be NaN if only one value is given except height
  248. height = containerHeight - verticalMargin - top - (bottom || 0);
  249. }
  250. var rect = new BoundingRect(left + margin[3], top + margin[0], width, height);
  251. rect.margin = margin;
  252. return rect;
  253. }
  254. /**
  255. * Position a zr element in viewport
  256. * Group position is specified by either
  257. * {left, top}, {right, bottom}
  258. * If all properties exists, right and bottom will be igonred.
  259. *
  260. * Logic:
  261. * 1. Scale (against origin point in parent coord)
  262. * 2. Rotate (against origin point in parent coord)
  263. * 3. Traslate (with el.position by this method)
  264. * So this method only fixes the last step 'Traslate', which does not affect
  265. * scaling and rotating.
  266. *
  267. * If be called repeatly with the same input el, the same result will be gotten.
  268. *
  269. * @param {module:zrender/Element} el Should have `getBoundingRect` method.
  270. * @param {Object} positionInfo
  271. * @param {number|string} [positionInfo.left]
  272. * @param {number|string} [positionInfo.top]
  273. * @param {number|string} [positionInfo.right]
  274. * @param {number|string} [positionInfo.bottom]
  275. * @param {number|string} [positionInfo.width] Only for opt.boundingModel: 'raw'
  276. * @param {number|string} [positionInfo.height] Only for opt.boundingModel: 'raw'
  277. * @param {Object} containerRect
  278. * @param {string|number} margin
  279. * @param {Object} [opt]
  280. * @param {Array.<number>} [opt.hv=[1,1]] Only horizontal or only vertical.
  281. * @param {Array.<number>} [opt.boundingMode='all']
  282. * Specify how to calculate boundingRect when locating.
  283. * 'all': Position the boundingRect that is transformed and uioned
  284. * both itself and its descendants.
  285. * This mode simplies confine the elements in the bounding
  286. * of their container (e.g., using 'right: 0').
  287. * 'raw': Position the boundingRect that is not transformed and only itself.
  288. * This mode is useful when you want a element can overflow its
  289. * container. (Consider a rotated circle needs to be located in a corner.)
  290. * In this mode positionInfo.width/height can only be number.
  291. */
  292. export function positionElement(el, positionInfo, containerRect, margin, opt) {
  293. var h = !opt || !opt.hv || opt.hv[0];
  294. var v = !opt || !opt.hv || opt.hv[1];
  295. var boundingMode = opt && opt.boundingMode || 'all';
  296. if (!h && !v) {
  297. return;
  298. }
  299. var rect;
  300. if (boundingMode === 'raw') {
  301. rect = el.type === 'group'
  302. ? new BoundingRect(0, 0, +positionInfo.width || 0, +positionInfo.height || 0)
  303. : el.getBoundingRect();
  304. }
  305. else {
  306. rect = el.getBoundingRect();
  307. if (el.needLocalTransform()) {
  308. var transform = el.getLocalTransform();
  309. // Notice: raw rect may be inner object of el,
  310. // which should not be modified.
  311. rect = rect.clone();
  312. rect.applyTransform(transform);
  313. }
  314. }
  315. // The real width and height can not be specified but calculated by the given el.
  316. positionInfo = getLayoutRect(
  317. zrUtil.defaults(
  318. {width: rect.width, height: rect.height},
  319. positionInfo
  320. ),
  321. containerRect,
  322. margin
  323. );
  324. // Because 'tranlate' is the last step in transform
  325. // (see zrender/core/Transformable#getLocalTransform),
  326. // we can just only modify el.position to get final result.
  327. var elPos = el.position;
  328. var dx = h ? positionInfo.x - rect.x : 0;
  329. var dy = v ? positionInfo.y - rect.y : 0;
  330. el.attr('position', boundingMode === 'raw' ? [dx, dy] : [elPos[0] + dx, elPos[1] + dy]);
  331. }
  332. /**
  333. * @param {Object} option Contains some of the properties in HV_NAMES.
  334. * @param {number} hvIdx 0: horizontal; 1: vertical.
  335. */
  336. export function sizeCalculable(option, hvIdx) {
  337. return option[HV_NAMES[hvIdx][0]] != null
  338. || (option[HV_NAMES[hvIdx][1]] != null && option[HV_NAMES[hvIdx][2]] != null);
  339. }
  340. /**
  341. * Consider Case:
  342. * When defulat option has {left: 0, width: 100}, and we set {right: 0}
  343. * through setOption or media query, using normal zrUtil.merge will cause
  344. * {right: 0} does not take effect.
  345. *
  346. * @example
  347. * ComponentModel.extend({
  348. * init: function () {
  349. * ...
  350. * var inputPositionParams = layout.getLayoutParams(option);
  351. * this.mergeOption(inputPositionParams);
  352. * },
  353. * mergeOption: function (newOption) {
  354. * newOption && zrUtil.merge(thisOption, newOption, true);
  355. * layout.mergeLayoutParam(thisOption, newOption);
  356. * }
  357. * });
  358. *
  359. * @param {Object} targetOption
  360. * @param {Object} newOption
  361. * @param {Object|string} [opt]
  362. * @param {boolean|Array.<boolean>} [opt.ignoreSize=false] Used for the components
  363. * that width (or height) should not be calculated by left and right (or top and bottom).
  364. */
  365. export function mergeLayoutParam(targetOption, newOption, opt) {
  366. !zrUtil.isObject(opt) && (opt = {});
  367. var ignoreSize = opt.ignoreSize;
  368. !zrUtil.isArray(ignoreSize) && (ignoreSize = [ignoreSize, ignoreSize]);
  369. var hResult = merge(HV_NAMES[0], 0);
  370. var vResult = merge(HV_NAMES[1], 1);
  371. copy(HV_NAMES[0], targetOption, hResult);
  372. copy(HV_NAMES[1], targetOption, vResult);
  373. function merge(names, hvIdx) {
  374. var newParams = {};
  375. var newValueCount = 0;
  376. var merged = {};
  377. var mergedValueCount = 0;
  378. var enoughParamNumber = 2;
  379. each(names, function (name) {
  380. merged[name] = targetOption[name];
  381. });
  382. each(names, function (name) {
  383. // Consider case: newOption.width is null, which is
  384. // set by user for removing width setting.
  385. hasProp(newOption, name) && (newParams[name] = merged[name] = newOption[name]);
  386. hasValue(newParams, name) && newValueCount++;
  387. hasValue(merged, name) && mergedValueCount++;
  388. });
  389. if (ignoreSize[hvIdx]) {
  390. // Only one of left/right is premitted to exist.
  391. if (hasValue(newOption, names[1])) {
  392. merged[names[2]] = null;
  393. }
  394. else if (hasValue(newOption, names[2])) {
  395. merged[names[1]] = null;
  396. }
  397. return merged;
  398. }
  399. // Case: newOption: {width: ..., right: ...},
  400. // or targetOption: {right: ...} and newOption: {width: ...},
  401. // There is no conflict when merged only has params count
  402. // little than enoughParamNumber.
  403. if (mergedValueCount === enoughParamNumber || !newValueCount) {
  404. return merged;
  405. }
  406. // Case: newOption: {width: ..., right: ...},
  407. // Than we can make sure user only want those two, and ignore
  408. // all origin params in targetOption.
  409. else if (newValueCount >= enoughParamNumber) {
  410. return newParams;
  411. }
  412. else {
  413. // Chose another param from targetOption by priority.
  414. for (var i = 0; i < names.length; i++) {
  415. var name = names[i];
  416. if (!hasProp(newParams, name) && hasProp(targetOption, name)) {
  417. newParams[name] = targetOption[name];
  418. break;
  419. }
  420. }
  421. return newParams;
  422. }
  423. }
  424. function hasProp(obj, name) {
  425. return obj.hasOwnProperty(name);
  426. }
  427. function hasValue(obj, name) {
  428. return obj[name] != null && obj[name] !== 'auto';
  429. }
  430. function copy(names, target, source) {
  431. each(names, function (name) {
  432. target[name] = source[name];
  433. });
  434. }
  435. }
  436. /**
  437. * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
  438. * @param {Object} source
  439. * @return {Object} Result contains those props.
  440. */
  441. export function getLayoutParams(source) {
  442. return copyLayoutParams({}, source);
  443. }
  444. /**
  445. * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
  446. * @param {Object} source
  447. * @return {Object} Result contains those props.
  448. */
  449. export function copyLayoutParams(target, source) {
  450. source && target && each(LOCATION_PARAMS, function (name) {
  451. source.hasOwnProperty(name) && (target[name] = source[name]);
  452. });
  453. return target;
  454. }