'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2013, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['./LayoutBaseView', 'jquery', 'underscore', '../../../../lib/@waca/core-client/js/core-client/utils/Deferred', '../../LayoutHelper', '../../../util/PxPercentUtil', '../../../glass/util/InstrumentationUtil'], function (BaseLayout, $, _, Deferred, LayoutHelper, PxPercentUtil, InstrumentationUtil) { var PageLayout = null; var WIDGET_PADDING = 5; PageLayout = BaseLayout.extend({ init: function init(options) { PageLayout.inherited('init', this, arguments); if (this.layoutController.topLayoutModel) { this.layoutController.topLayoutModel.on('change:showGrid', this.renderGrid, this); this.layoutController.topLayoutModel.on('change:fitPage', this.renderGrid, this); this.layoutController.topLayoutModel.on('change:pageSize', this.onPageSize, this); } this.services = options.services; this._templateHelper = options.templateHelper; // Optional this.whenIsReadyDfd = new Deferred(); this.specializeConsumeView(['setPreferredLocation']); this._initialize(); }, createDropZone: function createDropZone() { var _this = this; this._dropTarget = this._dndManager.addDropTarget(this.domNode, { accepts: this.accepts.bind(this), onDrop: this.onDrop.bind(this), onDragLeave: this.onDragLeave.bind(this), onDragEnd: this.onDragEnd.bind(this), onDragMove: this.onDragMove.bind(this), priority: -100 /* Indicate that any other drop zone takes priority */ , info: function info(domNode, event) { return _this._getCoordsRelativeToTarget(domNode, event); }, type: 'dropOnCanvas' }); this._dropHandler = this._dndManager.registerDropHandler({ accepts: function accepts(dragPayload, dropPayload) { return dragPayload.type.match(/content\..*/) && dropPayload.type === 'dropOnCanvas'; }, onDrop: function onDrop(dragPayload, dropPayload) { _this.onContentDrop(dragPayload, dropPayload); } }); }, _getCoordsRelativeToTarget: function _getCoordsRelativeToTarget(domNode, event) { var offset = $(domNode).offset(); return { top: Math.round(event.pageY - offset.top + domNode.scrollTop), left: Math.round(event.pageX - offset.left + domNode.scrollLeft) }; }, onContentDrop: function onContentDrop(dragPayload, dropPayload) { var content = JSON.parse(JSON.stringify(dragPayload.data)); var specStyle = content.spec.style || {}; if (dropPayload && dropPayload.info) { var style = { width: specStyle.width, height: specStyle.height, top: dropPayload.info.top, left: dropPayload.info.left }; this._stylePxToInt(style); this._setStyleWithinBounds(style); LayoutHelper.styleIntToPx(style); specStyle.width = style.width; specStyle.height = style.height; specStyle.left = style.left; specStyle.top = style.top; content.spec.style = specStyle; var canvas = this.dashboardApi.getFeature('Canvas'); return canvas.addContent(content); } this.clearDragState(); }, onShow: function onShow() { PageLayout.inherited('onShow', this, arguments); this.renderGrid(); }, whenIsReady: function whenIsReady() { return this.whenIsReadyDfd.promise; }, destroy: function destroy() { if (this._dropTarget) { this._dropTarget.remove(); this._dropTarget = null; } if (this._dropHandler) { this._dropHandler.deregister(); this._dropHandler = null; } this._dndManager = null; if ($.glassdnd && $.glassdnd.cancelDroppable) { $.glassdnd.cancelDroppable(this.$el); } this.$el.find('.absoluteLayoutGrid').remove(); this.layoutController.topLayoutModel.off('change:showGrid', this.renderGrid, this); this.layoutController.topLayoutModel.off('change:pageSize', this.onPageSize, this); PageLayout.inherited('destroy', this, arguments); }, removeChild: function removeChild() { PageLayout.inherited('removeChild', this, arguments); }, _initialize: function _initialize() { this._dndManager = this.dashboardApi.getFeature('DashboardDnd.internal'); this.whenIsReadyDfd.resolve(); this.createDropZone(); }, _getDragPositionRelativeToTarget: function _getDragPositionRelativeToTarget(dragObject, target) { var offset = $(target).offset(); return { top: Math.round(dragObject.position.y - offset.top + target.scrollTop), left: Math.round(dragObject.position.x - offset.left + target.scrollLeft) }; }, _getDragBoxForNewDrop: function _getDragBoxForNewDrop(dragObject, target) { var size = this._getDragObjectDimensions(dragObject); var coords = this._getDragPositionRelativeToTarget(dragObject, target); var dragBox = { top: coords.top, left: coords.left, ids: [] }; dragBox.right = dragBox.left + parseInt(size.width, 10); dragBox.bottom = dragBox.top + parseInt(size.height, 10); dragBox.center_x = (dragBox.left + dragBox.right) / 2; dragBox.center_y = (dragBox.top + dragBox.bottom) / 2; return dragBox; }, /** * Get the drag object dimensions. We use the layout/style information if avaliable. If not, we use the drag avatar. */ _getDragObjectDimensions: function _getDragObjectDimensions(dragObject) { var style = dragObject.data.layoutProperties && dragObject.data.layoutProperties.style || {}; var $avatar = $(dragObject.avatar); if ($avatar.length === 0) { $avatar = $('.avatar'); } return { width: style.width || $avatar.width() || '10px', //If we can't find anything to give a size, default to a 10*10 box height: style.height || $avatar.height() || '10px' }; }, _getDragBox: function _getDragBox(nodeInfoList) { var dragBox = nodeInfoList.reduce(function (box, current) { var $n = $(current.node), top, left, bottom, right; if (current.dropPosition) { top = current.dropPosition.y; left = current.dropPosition.x; bottom = top + $n.outerHeight(); right = left + $n.outerWidth(); } else { //If no dropPosition is present, don't affect the dragBox. This happens for group members, for which we can't currently scroll the canvas without disconnecting the widget and mouse. top = Infinity; left = Infinity; bottom = 0; right = 0; } box.top = Math.min(box.top, top); box.left = Math.min(box.left, left); box.bottom = Math.max(box.bottom, bottom); box.right = Math.max(box.right, right); box.ids.push(current.node.id); return box; }, { top: Infinity, left: Infinity, bottom: 0, right: 0, ids: [] }); dragBox.center_x = (dragBox.left + dragBox.right) / 2; dragBox.center_y = (dragBox.top + dragBox.bottom) / 2; return dragBox; }, /** * Called by the DnD manager to check if this drop zone accept the dragged object * We only accept object with type widget, pin and groupContent * @returns {Boolean} */ accepts: function accepts(dragObject) { var canvasDnD = this.dashboardApi.getFeature('CanvasDnD'); return canvasDnD.accepts(dragObject, { fromCanvas: true }); }, /** * Called by the DnD manager when we move inside the drop zone (only if the drop zone accepts the object) * @param dragBox {object} A drag-position object with left,right,top,bottom and center_x, center_y. * */ onDragMove: function onDragMove(dragObject, target) { this.$el.addClass('layoutDragInProgress'); var nodeInfoList = dragObject.data && dragObject.data.nodeInfoList; if (nodeInfoList && nodeInfoList.length) { dragObject.dragBox = this._getDragBox(nodeInfoList); } else { dragObject.dragBox = this._getDragBoxForNewDrop(dragObject, target); } this.adjustScrollPosition(dragObject); }, _moveDrop: function _moveDrop(dragObject) { var positionUpdateArray = [], nodeInfo, positionInfo; var i = 0; for (var iLen = dragObject.data.nodeInfoList.length; i < iLen; i++) { nodeInfo = dragObject.data.nodeInfoList[i]; positionInfo = this.getWidgetPositionUpdate(nodeInfo); if (positionInfo) { positionUpdateArray.push(positionInfo); } } var transactionApi = this.dashboardApi.getFeature('Transaction'); var transactionToken = transactionApi.startTransaction(); var nodeIds = positionUpdateArray.map(function (update) { return update.id; }); var insertBeforeMap = {}; _.reduce(positionUpdateArray, function (oMap, item) { if (item.insertBefore) { oMap[item.id] = item.insertBefore; } return oMap; }, insertBeforeMap); this.dashboardApi.getCanvas().moveContent(this.model.id, nodeIds, transactionToken, insertBeforeMap); this.updateModel(positionUpdateArray, transactionToken); transactionApi.endTransaction(transactionToken); }, _newDrop: function _newDrop(dragObject, target, isTrackAction) { var _this2 = this; var layoutProperties = dragObject.data.layoutProperties || {}; var offset = $(target).offset(); if (target && offset) { return this._getModelToAddFromDragObject(dragObject).then(function (newModel) { if (newModel) { if (!layoutProperties.style) { layoutProperties.style = {}; } var coords = _this2._getDragPositionRelativeToTarget(dragObject, target); var style = { width: layoutProperties.style.width, height: layoutProperties.style.height, top: coords.top, left: coords.left }; _this2._stylePxToInt(style); _this2._setStyleWithinBounds(style); LayoutHelper.styleIntToPx(style); layoutProperties.style.width = style.width; layoutProperties.style.height = style.height; layoutProperties.style.left = style.left; layoutProperties.style.top = style.top; var widgetSpec = { parentId: _this2.id, model: newModel, layoutProperties: layoutProperties }; if (isTrackAction) { InstrumentationUtil.trackWidget('created', _this2.dashboardApi, widgetSpec.model); } _this2._addWidget(widgetSpec, dragObject.isTouch); } }); } else { return Promise.resolve(); } }, /** * A helper function handles pin dropping. * @param {object} dragObject - The dragObject to process */ _onPinDrop: function _onPinDrop(dragObject) { var pinSpec = dragObject.data.pinSpec; var isTouch = dragObject.isTouch; var x = dragObject.position.x; var y = dragObject.position.y; // pin drop absolute location var top = Math.round(y - this.$el.offset().top) + 'px'; var left = Math.round(x - this.$el.offset().left) + 'px'; // we only need to consider gemini widget if (pinSpec.contentType === 'boardFragment') { // set absolute drop position back into the layout for top-left corner. // height width retained from original widget, this could be absolute // or a percentage if it came from a template. %age retains original // dimensions still if (!pinSpec.content.layout.style) { pinSpec.content.layout.style = {}; } pinSpec.content.layout.style.top = top; pinSpec.content.layout.style.left = left; // If our pinSpec is too big for the content box, shrink and center it to fit. Only do this for pan-and-zoom. this._resizeToFitBoundaries(this.consumeView.getMaximumHeight(), this.consumeView.getMaximumWidth(), pinSpec.content.layout.style); } pinSpec.parentId = this.id; this._processWidgetSpecForPin(pinSpec, isTouch); }, /** Resize the top, left, width & height of style such that they fit within the specified maxHeight and maxWidth, * maintaining their original aspect ratio. * @param maxHeight - maximum height of the layout specified in style * @param maxWidth - maximum width of the layout specified in style * @param style - expected to have height and width properties to compare with maxHeight and maxWidth. Will be resized to fit. */ _resizeToFitBoundaries: function _resizeToFitBoundaries(maxHeight, maxWidth, style) { var width = parseInt(style.width, 10); var dWidth = maxWidth / width; var height = parseInt(style.height, 10); var dHeight = maxHeight / height; if (dWidth < 1 || dHeight < 1) { var dMultiple = Math.min(dWidth, dHeight); style.height = Math.round(height * dMultiple) + 'px'; style.width = Math.round(width * dMultiple) + 'px'; style.left = Math.round((maxWidth - Math.round(width * dMultiple)) / 2) + 'px'; style.top = Math.round((maxHeight - Math.round(height * dMultiple)) / 2) + 'px'; } }, /** * Called by the drag&drop manager when a drop happens * * @param dragObject * @param targetNode */ onDrop: function onDrop(dragObject, targetNode) { var _this3 = this; var promise = void 0; if (!dragObject.data && !targetNode) { promise = this._newDrop(dragObject.originalEvent, dragObject.targetNode); } else if (dragObject.data.operation === 'move') { promise = Promise.resolve(this._moveDrop(dragObject)); } else if (dragObject.type === 'pin' && dragObject.data.operation === 'new') { //calls _onPinDrop when the dragObject with type pin promise = Promise.resolve(this._onPinDrop(dragObject)); } else { promise = this._newDrop(dragObject, targetNode, /*isTrackAction*/true); } return promise.then(function () { return _this3.clearDragState(); }); }, onDragLeave: function onDragLeave() { this.clearDragState(true); }, onDragEnd: function onDragEnd() { this.clearDragState(); this.previousDragBox = null; }, clearDragState: function clearDragState() { this.$el.removeClass('layoutDragInProgress'); }, _changePercentModelToPixel: function _changePercentModelToPixel(style) { var $referenceView = this.$el; // this view could be hidden, so find a view that isn't hidden. if (!$referenceView.is(':visible')) { $referenceView = $(this.layoutController.getLastVisiblePage()); } var referenceViewSize = { width: $referenceView.width(), height: $referenceView.height() }; PxPercentUtil.changePercentPropertiesToPixel(style, referenceViewSize); }, /** * Set the preferred layout location in the layout properties * * @param options * options.layoutProperties * options.parentId */ setPreferredLocation: function setPreferredLocation(options) { var locationSet = false; if (this._templateHelper && !options.layoutProperties.style) { locationSet = this._templateHelper.setPreferredLocation(options); } if (!locationSet) { var properties = options.layoutProperties; if (!properties.style) { properties.style = {}; } // get the dimensions of the widget being added var size = { width: properties.style.width || '300', height: properties.style.height || '300' }; if (size.width.indexOf('%') !== -1 || size.height.indexOf('%') !== -1) { this._changePercentModelToPixel(size); } var width = parseInt(size.width, 10); var height = parseInt(size.height, 10); var location = !properties.style.top && !properties.style.left ? this.findAvailableSpace({ width: width, height: height }) : null; if (location) { properties.style.top = location.top + 'px'; properties.style.left = location.left + 'px'; properties.style.width = properties.style.width || (location.width || width) + 'px'; properties.style.height = properties.style.height || (location.height || height) + 'px'; } // update the parent id options.parentId = this.model.id; } }, getWidgetContainerNode: function getWidgetContainerNode() { return this.domNode; }, /** * Find the location of the available space that fits the given dimensions * * 1- Sort the list of existing rectangles from left to right and from top to bottom using the center of the box. * 2- for each rectangle in the list do the following: * - find if there is available space that is adjacent to the bottom border * * * @param dimensions - dimensions that we are trying to find space for * * dimensions.width * dimensions.height * * @returns location - object that contains the top and left positions * */ findAvailableSpace: function findAvailableSpace(dimensions) { // Get the scan options that will be used as input to scan for available space var usedSpaceRectArray = this.getChildrenRectList(); // sort the rectangle from left to right and from top to bottom ( using the bottom border as the reference point). // This is the order that we use to try to find an available position var sortedUsedSpace = usedSpaceRectArray.slice(0).sort(function (a, b) { return a.height - b.height === 0 ? a.top - b.top === 0 ? a.left - b.left : a.top - b.top : a.height - b.height; }); var i, iLen, rect; var coords = { top: 0, left: 0 }; for (i = 0, iLen = sortedUsedSpace.length; i < iLen; i++) { rect = sortedUsedSpace[i]; coords = this.findSpaceAdjacentToBottomBorder(rect, dimensions, usedSpaceRectArray); if (coords) { break; } } var style = { width: dimensions.width, height: dimensions.height, top: coords.top, left: coords.left }; this._setStyleWithinBounds(style); coords.top = style.top; coords.left = style.left; coords.width = style.width; coords.height = style.height; return coords; }, /** * Check that widget style does not fall outside max bounds. * Updates the style to fit within bounds * Style should be unitless * * @param style */ _setStyleWithinBounds: function _setStyleWithinBounds(style) { // will return Infinity if scrollDrop is supported var maxWidth = this.consumeView.getMaximumWidth(); var maxHeight = this.consumeView.getMaximumHeight(); if (isFinite(maxHeight) && isFinite(maxWidth)) { // widgets that are larger than layout will be fit to max size, maintaining ratio this._resizeToFitBoundaries(maxHeight, maxWidth, style); // convert to integer values for rest of operations this._stylePxToInt(style); // widgets with unknown width & height will occupy relative % of layout size if (!style.width && !style.height) { if (maxHeight > maxWidth) { style.width = Math.floor(maxWidth * 0.5); style.height = Math.floor(style.width * 3 / 4); } else { style.height = Math.floor(maxHeight * 0.5); style.width = Math.floor(style.height * 4 / 3); } } this.moveToFitBoundaries(maxHeight, maxWidth, style); } }, _stylePxToInt: function _stylePxToInt(style) { for (var key in style) { if (style.hasOwnProperty(key)) { var value = style[key]; if (value) { style[key] = parseInt(value, 10); } } } }, /** * Find a space that is adjacent to the bottom border of a given rectangle where the space is bigger or equal to the given dimensions * * * @param topRect * @param dimensions * @param rectList * @returns */ findSpaceAdjacentToBottomBorder: function findSpaceAdjacentToBottomBorder(topRect, dimensions, rectList) { // Find all the rectangles below the current rectangle that might intersect with the rectangle we are trying to add. var list = _.filter(rectList, function (rect) { return rect !== topRect && topRect.top + topRect.height < rect.top + rect.height && topRect.top + topRect.height + dimensions.height + WIDGET_PADDING > rect.top && rect.left <= topRect.left + topRect.width + dimensions.width + WIDGET_PADDING && rect.left + rect.width >= topRect.left - dimensions.width; }); var coords; if (list.length === 0) { coords = { top: topRect.top + topRect.height + WIDGET_PADDING, left: topRect.left }; } else { coords = this.findSpaceBetweenRectangles(topRect, list, dimensions); } return coords; }, /** * Find a space that fits the given dimension by scanning between the rectangles in the list * * @param topRect - top boundary rectangle * @param list - list if rectangle to scan * @param dimensions - dimensions of the new rectangle we are trying to find * @param padding - padding used between rectangles * @returns */ findSpaceBetweenRectangles: function findSpaceBetweenRectangles(topRect, list, dimensions) { var width = dimensions.width + WIDGET_PADDING; list.sort(function (a, b) { return a.left - b.left; }); // If there is space before the first rectangle, then we insert before var top = topRect.top + topRect.height + WIDGET_PADDING; var first = list[0]; if (first.left > topRect.left && first.left >= width && first.left - width > topRect.left - width) { return { top: top, left: Math.min(topRect.left, first.left - width) }; } // Scan if there is space after each rectangle. var maxWidth = $(this.getWidgetContainerNode()).width(); var rect, next; for (var i = 0; i < list.length; i++) { rect = list[i]; next = list[i + 1]; if (next && rect.left + rect.width > next.left + next.width) { // the next rectangle is contained within the current one. Throw it away and replace with the current rectangle. list[i + 1] = rect; } else { var rectLeft = next ? next.left : maxWidth; if (rect.left + rect.width + width <= rectLeft && topRect.left + topRect.width + width > rect.left + rect.width + width) { return { top: top, left: rect.left + rect.width + WIDGET_PADDING }; } } } return null; }, /** * Get the list of rectangle information for every child of this layout * * @returns {Array} */ getChildrenRectList: function getChildrenRectList() { var usedSpaceArray = []; // Virtual boxes at the top and left of the page to force adding widgets at a certain padding usedSpaceArray.push({ top: 0, left: 0, height: this.getMinimumTop() - WIDGET_PADDING, width: Math.round($(this.getWidgetContainerNode()).width()) }); usedSpaceArray.push({ top: 0, left: 0, height: Math.round($(this.getWidgetContainerNode()).height()), width: this.getMinimumLeft() - WIDGET_PADDING }); var children = $(this.getWidgetContainerNode()).children().not('.relativeLayoutGrid, .absoluteLayoutGrid, .pagetemplateIndicator, .pagetemplateDropZone, .sizeDashboardIndicatorContainer'); var i, iLen; for (i = 0, iLen = children.length; i < iLen; i++) { var $child = $(children[i]); usedSpaceArray.push({ top: $child[0].offsetTop, left: $child[0].offsetLeft, height: $child.outerHeight(), width: $child.outerWidth() }); } return usedSpaceArray; }, adjustScrollPosition: function adjustScrollPosition(dragObject) { var dragBox = dragObject.dragBox; var scrollInfo = this.getScrollInfo(dragObject); this.scrollToDragBox(dragBox, scrollInfo); }, getScrollInfo: function getScrollInfo(dragObject) { var dragBox = dragObject.dragBox; var $node = this.getScrollAreaNode(); var scrollInfo = { h: $node.height(), w: $node.width(), scrollTop: $node.scrollTop(), scrollLeft: $node.scrollLeft(), $node: $node, direction: [] }; if (this.previousDragBox) { if (dragBox.left !== this.previousDragBox.left) { scrollInfo.direction[dragBox.left - this.previousDragBox.left > 0 ? 'right' : 'left'] = true; } if (dragBox.top !== this.previousDragBox.top) { scrollInfo.direction[dragBox.top - this.previousDragBox.top > 0 ? 'bottom' : 'top'] = true; } } this.previousDragBox = dragBox; return scrollInfo; }, getScrollAreaNode: function getScrollAreaNode() { var $parents = this.$el.parents('.page'); var $el = this.$el; var index = 0; while ($el && $el.length && $el.css('overflow') !== 'auto') { $el = $parents.eq(index); index++; } if ($el.length === 0) { $el = this.$el; } return $el; }, scrollToDragBox: function scrollToDragBox(dragBox, scrollInfo) { var SCROLL_INCREMENT = 16; var DRAG_EDGE_BUFFER_SIZE = 2; //Size of a buffer to add to the top & left checks due to varying mouse position. This will only be called as long as the mouse is over the canvas, and when inserting a widget, the mouse cursor is just outside the top-left corner making checks for scrolling up and left problematic. if (dragBox.bottom > scrollInfo.scrollTop + scrollInfo.h && scrollInfo.direction.bottom) { scrollInfo.$node.scrollTop(scrollInfo.scrollTop + SCROLL_INCREMENT); } else if (scrollInfo.scrollTop > dragBox.top - DRAG_EDGE_BUFFER_SIZE && scrollInfo.direction.top) { scrollInfo.$node.scrollTop(scrollInfo.scrollTop - SCROLL_INCREMENT); } if (dragBox.right > scrollInfo.scrollLeft + scrollInfo.w && scrollInfo.direction.right) { scrollInfo.$node.scrollLeft(scrollInfo.scrollLeft + SCROLL_INCREMENT); } else if (scrollInfo.scrollLeft > dragBox.left - DRAG_EDGE_BUFFER_SIZE && scrollInfo.direction.left) { scrollInfo.$node.scrollLeft(scrollInfo.scrollLeft - SCROLL_INCREMENT); } }, renderGrid: function renderGrid() { var showGridProp = this.model.getValueFromSelfOrParent('showGrid'); var showGrid = showGridProp === undefined ? false : showGridProp; var layoutPositioning = this.model.getValueFromSelfOrParent('layoutPositioning'); if (showGrid && layoutPositioning === 'absolute') { if (this.$el.hasClass('gridCapable')) { this.$el.find('.absoluteLayoutGrid').remove(); this.$el.prepend('
'); } } else { this.$el.find('.absoluteLayoutGrid').remove(); } }, onResize: function onResize() { PageLayout.inherited('onResize', this, arguments); this.renderGrid(); }, onPageSize: function onPageSize() { this.renderGrid(); } }); return PageLayout; }); //# sourceMappingURL=Absolute.js.map