layout.js 15 KB

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