layout.js 16 KB

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