UNPKG

@aurigma/design-atoms

Version:

Design Atoms is a part of Customer's Canvas SDK which allows for manipulating individual design elements through your code.

422 lines 20.5 kB
import { EventObject } from "@aurigma/design-atoms-model/EventObject"; import { PointF, RectangleF, RotatedRectangleF } from "@aurigma/design-atoms-model/Math"; import { AutoLayoutSettings, AutoLayoutAlignItems, AutoLayoutOrientation, AutoLayoutAnchorPoint } from "@aurigma/design-atoms-model/Product/Items"; import { BaseTextItemHandler, NewBaseTextItemHandler, PlaceholderItemHandler } from ".."; import { getTranslationTransform } from "../../Utils/Math"; import { LayoutItemHandler } from "../LayoutItemHandler"; import { GroupItemHandler } from "../GroupItemHandler"; export class AutoLayoutHandler { constructor(_item, _itemHandlers, _itemHandler, _history) { this._item = _item; this._itemHandlers = _itemHandlers; this._itemHandler = _itemHandler; this._history = _history; this._layoutEvent = new EventObject(); this._orderedItemHandlers = null; this._initialized = false; this._disposed = false; this._isApplyingChildrenTransforms = false; this._onChildItemHandlerChanging = (sender) => { this.applyLayout(); }; this._onChildItemHandlerPropertyChanged = async (sender, propertyName) => { switch (propertyName) { case "font": case "text": case "tracking": case "leading": case "shadow": case "stroke": case "underline": case "sourceRectangle": case "borderWidth": case "transform": if (this._isApplyingChildrenTransforms) return; await sender.waitUpdate(); this.applyLayout(); break; } }; this._onItemPropertyChanged = (sender, propertyName) => { switch (propertyName) { case "layoutSettings": this._orderedItemHandlers = null; this._updateItemsOrder(); this.applyLayout(); break; case "margin": this.applyLayout(); break; case "orientation": this.applyLayout(true); break; case "alignItems": case "justifyContent": this.applyLayout(); break; } }; this._ignoreLayoutApply = false; this._filterHiddenItems = (itemHandler) => !((itemHandler instanceof BaseTextItemHandler || itemHandler instanceof NewBaseTextItemHandler || itemHandler instanceof LayoutItemHandler) && itemHandler.isEmpty()); this._item.addPropertyChanged(this._onItemPropertyChanged); this._itemHandlers.add_itemAdded(this._onItemHandlerAdded.bind(this)); this._itemHandlers.add_itemRemoved(this._onItemHandlerRemoved.bind(this)); this._itemHandlers.forEach(itemHandler => itemHandler.addItemPropertyChanged(this._onChildItemHandlerPropertyChanged)); this._init(); } dispose() { this._disposed = true; this._item.removePropertyChanged(this._onItemPropertyChanged); this._itemHandlers.remove_itemAdded(this._onItemHandlerAdded.bind(this)); this._itemHandlers.remove_itemRemoved(this._onItemHandlerRemoved.bind(this)); this._itemHandlers.forEach(itemHandler => itemHandler.removeItemPropertyChanged(this._onChildItemHandlerPropertyChanged)); } getLayoutEvent() { return this._layoutEvent; } onItemRemoved(data) { this._updateItemsOrder({ removedItem: data.item }); this.applyLayout(); } onItemHandlersCollectionChanged() { this._orderedItemHandlers = null; } async _init() { if (this._initialized) return; var order = this._item.getItemsOrder(); await this._itemHandler.waitUpdate(); if (!order) this._updateItemsOrder(); this._initialized = true; this.applyLayout(); } _onItemHandlerAdded(data) { data.item.addItemPropertyChanged(this._onChildItemHandlerPropertyChanged); this._orderedItemHandlers = null; } _onItemHandlerRemoved(data) { data.item.removeItemPropertyChanged(this._onChildItemHandlerPropertyChanged); this._orderedItemHandlers = null; } _updateItemsOrder(params) { if (this._item.layoutSettings instanceof AutoLayoutSettings) { if (this._validateItemsOrder()) return; var itemsOrder = this._item.getItemsOrder(); if (itemsOrder == null) { this._initSortedItemHandlers(); var ids = this._getSortedItemHandlers().map(x => x.item.id); this._item.setItemsOrder(ids); } } } _initSortedItemHandlers() { const { center: groupCenter } = this._itemHandler.getTransformedRectangle(false, true); const { angle } = this._item.transform; const { orientation } = this._item.layoutSettings; const sortFunc = (a, b) => orientation === AutoLayoutOrientation.horizontal ? a.bounds.left - b.bounds.left : a.bounds.top - b.bounds.top; this._orderedItemHandlers = this._itemHandlers.toArray().sort((a, b) => sortFunc(a.getTransformedRectangle(false, true).rotateAt(-angle, groupCenter), b.getTransformedRectangle(false, true).rotateAt(-angle, groupCenter))); return this._orderedItemHandlers; } _validateItemsOrder() { const layoutItemsOrder = this._item.getItemsOrder(); if (layoutItemsOrder == null) return false; if (layoutItemsOrder.length !== this._item.items.length) return false; var itemIds = this._item.items.select(x => x.id).toArray(); return layoutItemsOrder.every(x => itemIds.includes(x)); } _getSortedItemHandlers() { if (this._orderedItemHandlers != null) return this._orderedItemHandlers; var itemsOrder = this._item.getItemsOrder(); if (!itemsOrder) return null; let sortedItemHandlers = []; var itemHandlers = this._itemHandlers.toArray(); if (itemHandlers.length != itemsOrder.length) { throw new Error("itemsOrder length does not match itemHandlers length"); } itemsOrder.forEach(itemsOrderElement => { var i = itemHandlers.findIndex(x => x.item.id == itemsOrderElement); if (i == -1) throw new Error(`itemsOrder: Element with id ${itemsOrderElement} was not found in itemHandlers`); sortedItemHandlers.push(itemHandlers[i]); itemHandlers.splice(i, 1); }); this._orderedItemHandlers = sortedItemHandlers; return this._orderedItemHandlers; } getOrderedItems() { var itemsOrder = this._item.getItemsOrder(); if (itemsOrder) { let result = this._item.items.toArray(); result.sort((x, y) => itemsOrder.indexOf(x.id) - itemsOrder.indexOf(y.id)); return result; } return null; } set ignoreLayoutApply(value) { this._ignoreLayoutApply = value; } applyLayout(fromCenter = false) { var _a, _b, _c, _d, _e; if (this._disposed) return; if (!this._initialized) return; if (this._ignoreLayoutApply) return; this._ignoreLayoutApply = true; try { const historyActive = !((_a = this._history) === null || _a === void 0 ? void 0 : _a.isPaused()); if (historyActive) (_b = this._history) === null || _b === void 0 ? void 0 : _b.pause(); (_c = this._itemHandler.canvas) === null || _c === void 0 ? void 0 : _c.pauseRedraw(); const changed = this._applyLayoutImpl(fromCenter); (_d = this._itemHandler.canvas) === null || _d === void 0 ? void 0 : _d.continueRedraw(); if (historyActive) (_e = this._history) === null || _e === void 0 ? void 0 : _e.resume(!!changed, false, true); this._layoutEvent.notify(changed); } finally { this._ignoreLayoutApply = false; } } _applyLayoutImpl(fromCenter = false) { var _a; let sortedItemHandlers = this._getSortedItemHandlers(); if (sortedItemHandlers == null || sortedItemHandlers.length == 0) return; var originalRectangle = this._itemHandler.rectangle; var originalCenter = originalRectangle.center; const { alignItems, orientation, margin, anchorPoint } = this._item.layoutSettings; const { angle } = this._item.transform; let layoutRect = originalRectangle.toRectangleF(); let layoutAnchorPoint = this._getAnchorPoint(layoutRect, anchorPoint, orientation); let currentPoint = this._getOriginalPoint(layoutRect, alignItems, orientation); sortedItemHandlers = sortedItemHandlers.filter(itemHandler => this._filterHiddenItems(itemHandler)); let rects = []; sortedItemHandlers.forEach(element => { // Calculate the new position of the element inside the layout, // taking into account parent's alignItems and orientation const elementPositionRect = this._getItemHandlerPositionRect(element, originalRectangle, currentPoint, alignItems, orientation); // Calculates the offset of the current element along the main layout axis // relative to the layout start. // In other words, this defines how much the element should be shifted // along the layout direction to be placed after previous elements. const offset = this._getPositionRectOffset(orientation, layoutRect, elementPositionRect) + margin; // Shift the anchor point for the next element currentPoint = this._getOriginalPoint(layoutRect, alignItems, orientation, offset); rects.push(elementPositionRect); }); rects = this._applyAnchor(rects, layoutRect, anchorPoint, orientation); let anyChildChanged = false; this._isApplyingChildrenTransforms = true; try { sortedItemHandlers.forEach(element => { let elementPositionRect = rects.shift(); // Set the actual position of the element on the canvas const changed = this._setItemHandlerPosition(element, originalRectangle, elementPositionRect); anyChildChanged = anyChildChanged || changed; }); } finally { this._isApplyingChildrenTransforms = false; } const filteredItems = this._itemHandlers.cast().where(this._filterHiddenItems); let sourceRectChanged = false; // Recalculate the layout's own sourceRectangle so that it wraps its children if (filteredItems.any()) { let childItemsBounds = filteredItems .select(x => x.getTransformedRectangle(false, true)) .aggregate((a, b) => RotatedRectangleF.union(a, b)); const newSourceRectangle = this._getSourceRectangleByAnchorPoint(childItemsBounds, layoutAnchorPoint, orientation, anchorPoint); if (fromCenter) { var translationTransform = getTranslationTransform(newSourceRectangle.center, originalCenter); newSourceRectangle.setTransform(translationTransform); } this._itemHandler.setSourceRectangle(newSourceRectangle); sourceRectChanged = !originalRectangle.equals(newSourceRectangle, 0.01); } const changed = anyChildChanged || sourceRectChanged; // TODO: move outside if (changed) (_a = this._itemHandler.canvas) === null || _a === void 0 ? void 0 : _a.updateTexts(); return changed; } _getSourceRectangleByAnchorPoint(childBounds, layoutAnchorPoint, orientation, anchor) { const childRect = childBounds.toRectangleF(); if (orientation == AutoLayoutOrientation.horizontal) { if (anchor == AutoLayoutAnchorPoint.start) { childRect.left = layoutAnchorPoint.x; } else if (anchor == AutoLayoutAnchorPoint.center) { const dx = childRect.center.x - layoutAnchorPoint.x; childRect.left -= dx; } else { // end childRect.right = layoutAnchorPoint.x; } } else { // vertical if (anchor == AutoLayoutAnchorPoint.start) { childRect.top = layoutAnchorPoint.y; } else if (anchor == AutoLayoutAnchorPoint.center) { const dy = childRect.center.y - layoutAnchorPoint.y; childRect.top -= dy; } else { // end childRect.bottom = layoutAnchorPoint.y; } } return RotatedRectangleF.fromRectangleF(childRect, childBounds.angle); } _getAnchorPoint(layoutRect, anchorPoint, orientation) { if (orientation == AutoLayoutOrientation.horizontal) { const y = layoutRect.center.y; switch (anchorPoint) { case AutoLayoutAnchorPoint.start: return new PointF(layoutRect.left, y); case AutoLayoutAnchorPoint.center: return new PointF(layoutRect.center.x, y); case AutoLayoutAnchorPoint.end: return new PointF(layoutRect.right, y); } } else { const x = layoutRect.center.x; switch (anchorPoint) { case AutoLayoutAnchorPoint.start: return new PointF(x, layoutRect.top); case AutoLayoutAnchorPoint.center: return new PointF(x, layoutRect.center.y); case AutoLayoutAnchorPoint.end: return new PointF(x, layoutRect.bottom); } } } _applyAnchor(inputRects, layoutRect, anchorPoint, orientation) { if (inputRects.length == 0) return inputRects; if (anchorPoint == AutoLayoutAnchorPoint.start) return inputRects; let calculateDiff = (rects) => { let dx = 0; let dy = 0; var unionRect = rects.reduce((prev, current) => RectangleF.union(prev, current)); if (orientation == AutoLayoutOrientation.horizontal) { if (anchorPoint == AutoLayoutAnchorPoint.center) dx = unionRect.center.x - layoutRect.center.x; else if (anchorPoint == AutoLayoutAnchorPoint.end) dx = unionRect.right - layoutRect.right; else throw new Error(`Invalid _applyAnchor operation. Orientation: '${orientation}' AnchorPoint: '${anchorPoint}'`); } else { // vertical if (anchorPoint == AutoLayoutAnchorPoint.center) dy = unionRect.center.y - layoutRect.center.y; else if (anchorPoint == AutoLayoutAnchorPoint.end) dy = unionRect.bottom - layoutRect.bottom; else throw new Error(`Invalid _applyAnchor operation. Orientation: '${orientation}' AnchorPoint: '${anchorPoint}'`); } return new PointF(dx, dy); }; const diff = calculateDiff(inputRects); return inputRects.map(x => new RectangleF(x.left - diff.x, x.top - diff.y, x.width, x.height)); } _setItemHandlerPosition(itemHandler, originalRectangle, elementPositionRect) { const setRectangle = (itemHandler) => { const itemHandlerRect = itemHandler.getTransformedRectangle(false, true); const originalRect = itemHandlerRect.clone(); itemHandlerRect.rotateAt(-originalRectangle.angle, originalRectangle.center); const itemHandlerRectBounds = itemHandlerRect.bounds; // elementPositionRect - целевая позиция // itemHandlerRectBounds - физическая const dx = elementPositionRect.left - itemHandlerRectBounds.left; const dy = elementPositionRect.top - itemHandlerRectBounds.top; itemHandlerRect.translate(dx, dy); itemHandlerRect.rotateAt(originalRectangle.angle, originalRectangle.center); if (originalRect.equals(itemHandlerRect)) return false; this._setItemHandlerRectangle(itemHandler, itemHandlerRect, setRectangle); return true; }; return setRectangle(itemHandler); } _setItemHandlerRectangle(itemHandler, itemHandlerRect, setRectangleFunc) { const margin = itemHandler.getBorderMargin(); itemHandlerRect.width -= margin * 2; itemHandlerRect.height -= margin * 2; if (itemHandler instanceof LayoutItemHandler) { itemHandler.setRectangle(itemHandlerRect, true, true); } else { itemHandler.setRectangle(itemHandlerRect); if (itemHandler instanceof PlaceholderItemHandler) itemHandler.updateContentAndFrames(setRectangleFunc); if (itemHandler instanceof GroupItemHandler) { itemHandler.getNestedItemHandlers(true).forEach((item) => { setRectangleFunc(item); }); } } } _getPositionRectOffset(orientation, layoutRect, elementPositionRect) { if (orientation == AutoLayoutOrientation.vertical) { return elementPositionRect.bottom - layoutRect.top; } else { // horizontal return elementPositionRect.right - layoutRect.left; } } _getItemHandlerPositionRect(element, layoutRectangle, currentPoint, alignItems, orientation) { let itemHandlerRectangle = element.getTransformedRectangle(false, true); // Rotate the child so that it is in coordinates where parent's angle is 0 itemHandlerRectangle.angle -= layoutRectangle.angle; let itemHandlerBounds = itemHandlerRectangle.bounds; // Get the element's anchor point let itemHandlerPoint = this._getOriginalPoint(itemHandlerBounds, alignItems, orientation); // currentPoint is the layout's anchor point let dx = currentPoint.x - itemHandlerPoint.x; let dy = currentPoint.y - itemHandlerPoint.y; if (alignItems !== AutoLayoutAlignItems.none || orientation === AutoLayoutOrientation.horizontal) itemHandlerBounds.left += dx; if (alignItems !== AutoLayoutAlignItems.none || orientation === AutoLayoutOrientation.vertical) itemHandlerBounds.top += dy; return itemHandlerBounds; } _getOriginalPoint(rect, alignItems, orientation, offset = 0) { let result; if (orientation == AutoLayoutOrientation.vertical) { if (alignItems == AutoLayoutAlignItems.start || alignItems == AutoLayoutAlignItems.none) { result = new PointF(rect.left, rect.top); } else if (alignItems == AutoLayoutAlignItems.center) { result = new PointF(rect.center.x, rect.top); } else { result = new PointF(rect.right, rect.top); } result.y += offset; } else { // horizontal if (alignItems == AutoLayoutAlignItems.start || alignItems == AutoLayoutAlignItems.none) result = new PointF(rect.left, rect.top); else if (alignItems == AutoLayoutAlignItems.center) result = new PointF(rect.left, rect.center.y); else result = new PointF(rect.left, rect.bottom); result.x += offset; } return result; } } //# sourceMappingURL=AutoLayoutHandler.js.map