FlexLayout.js 8.09 KB
/**
 * FlexLayout.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This layout manager works similar to the CSS flex box.
 *
 * @setting {String} direction row|row-reverse|column|column-reverse
 * @setting {Number} flex A positive-number to flex by.
 * @setting {String} align start|end|center|stretch
 * @setting {String} pack start|end|justify
 *
 * @class tinymce.ui.FlexLayout
 * @extends tinymce.ui.AbsoluteLayout
 */
define("tinymce/ui/FlexLayout", [
	"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
	"use strict";

	return AbsoluteLayout.extend({
		/**
		 * Recalculates the positions of the controls in the specified container.
		 *
		 * @method recalc
		 * @param {tinymce.ui.Container} container Container instance to recalc.
		 */
		recalc: function(container) {
			// A ton of variables, needs to be in the same scope for performance
			var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
			var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos;
			var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
			var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
			var alignDeltaSizeName, alignContentSizeName;
			var max = Math.max, min = Math.min;

			// Get container items, properties and settings
			items = container.items().filter(':visible');
			contLayoutRect = container.layoutRect();
			contPaddingBox = container.paddingBox;
			contSettings = container.settings;
			direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
			align = contSettings.align;
			pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
			spacing = contSettings.spacing || 0;

			if (direction == "row-reversed" || direction == "column-reverse") {
				items = items.set(items.toArray().reverse());
				direction = direction.split('-')[0];
			}

			// Setup axis variable name for row/column direction since the calculations is the same
			if (direction == "column") {
				posName = "y";
				sizeName = "h";
				minSizeName = "minH";
				maxSizeName = "maxH";
				innerSizeName = "innerH";
				beforeName = 'top';
				deltaSizeName = "deltaH";
				contentSizeName = "contentH";

				alignBeforeName = "left";
				alignSizeName = "w";
				alignAxisName = "x";
				alignInnerSizeName = "innerW";
				alignMinSizeName = "minW";
				alignAfterName = "right";
				alignDeltaSizeName = "deltaW";
				alignContentSizeName = "contentW";
			} else {
				posName = "x";
				sizeName = "w";
				minSizeName = "minW";
				maxSizeName = "maxW";
				innerSizeName = "innerW";
				beforeName = 'left';
				deltaSizeName = "deltaW";
				contentSizeName = "contentW";

				alignBeforeName = "top";
				alignSizeName = "h";
				alignAxisName = "y";
				alignInnerSizeName = "innerH";
				alignMinSizeName = "minH";
				alignAfterName = "bottom";
				alignDeltaSizeName = "deltaH";
				alignContentSizeName = "contentH";
			}

			// Figure out total flex, availableSpace and collect any max size elements
			availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
			maxAlignEndPos = totalFlex = 0;
			for (i = 0, l = items.length; i < l; i++) {
				ctrl = items[i];
				ctrlLayoutRect = ctrl.layoutRect();
				ctrlSettings = ctrl.settings;
				flex = ctrlSettings.flex;
				availableSpace -= (i < l - 1 ? spacing : 0);

				if (flex > 0) {
					totalFlex += flex;

					// Flexed item has a max size then we need to check if we will hit that size
					if (ctrlLayoutRect[maxSizeName]) {
						maxSizeItems.push(ctrl);
					}

					ctrlLayoutRect.flex = flex;
				}

				availableSpace -= ctrlLayoutRect[minSizeName];

				// Calculate the align end position to be used to check for overflow/underflow
				size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
				if (size > maxAlignEndPos) {
					maxAlignEndPos = size;
				}
			}

			// Calculate minW/minH
			rect = {};
			if (availableSpace < 0) {
				rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
			} else {
				rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
			}

			rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];

			rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
			rect[alignContentSizeName] = maxAlignEndPos;
			rect.minW = min(rect.minW, contLayoutRect.maxW);
			rect.minH = min(rect.minH, contLayoutRect.maxH);
			rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
			rect.minH = max(rect.minH, contLayoutRect.startMinHeight);

			// Resize container container if minSize was changed
			if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
				rect.w = rect.minW;
				rect.h = rect.minH;

				container.layoutRect(rect);
				this.recalc(container);

				// Forced recalc for example if items are hidden/shown
				if (container._lastRect === null) {
					var parentCtrl = container.parent();
					if (parentCtrl) {
						parentCtrl._lastRect = null;
						parentCtrl.recalc();
					}
				}

				return;
			}

			// Handle max size elements, check if they will become to wide with current options
			ratio = availableSpace / totalFlex;
			for (i = 0, l = maxSizeItems.length; i < l; i++) {
				ctrl = maxSizeItems[i];
				ctrlLayoutRect = ctrl.layoutRect();
				maxSize = ctrlLayoutRect[maxSizeName];
				size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;

				if (size > maxSize) {
					availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
					totalFlex -= ctrlLayoutRect.flex;
					ctrlLayoutRect.flex = 0;
					ctrlLayoutRect.maxFlexSize = maxSize;
				} else {
					ctrlLayoutRect.maxFlexSize = 0;
				}
			}

			// Setup new ratio, target layout rect, start position
			ratio = availableSpace / totalFlex;
			pos = contPaddingBox[beforeName];
			rect = {};

			// Handle pack setting moves the start position to end, center
			if (totalFlex === 0) {
				if (pack == "end") {
					pos = availableSpace + contPaddingBox[beforeName];
				} else if (pack == "center") {
					pos = Math.round(
						(contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
					) + contPaddingBox[beforeName];

					if (pos < 0) {
						pos = contPaddingBox[beforeName];
					}
				} else if (pack == "justify") {
					pos = contPaddingBox[beforeName];
					spacing = Math.floor(availableSpace / (items.length - 1));
				}
			}

			// Default aligning (start) the other ones needs to be calculated while doing the layout
			rect[alignAxisName] = contPaddingBox[alignBeforeName];

			// Start laying out controls
			for (i = 0, l = items.length; i < l; i++) {
				ctrl = items[i];
				ctrlLayoutRect = ctrl.layoutRect();
				size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];

				// Align the control on the other axis
				if (align === "center") {
					rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
				} else if (align === "stretch") {
					rect[alignSizeName] = max(
						ctrlLayoutRect[alignMinSizeName] || 0,
						contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
					);
					rect[alignAxisName] = contPaddingBox[alignBeforeName];
				} else if (align === "end") {
					rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
				}

				// Calculate new size based on flex
				if (ctrlLayoutRect.flex > 0) {
					size += ctrlLayoutRect.flex * ratio;
				}

				rect[sizeName] = size;
				rect[posName] = pos;
				ctrl.layoutRect(rect);

				// Recalculate containers
				if (ctrl.recalc) {
					ctrl.recalc();
				}

				// Move x/y position
				pos += size + spacing;
			}
		}
	});
});