'use strict'; /* *+------------------------------------------------------------------------+ *| Licensed Materials - Property of IBM *| IBM Cognos Products: Dashboard *| (C) Copyright IBM Corp. 2014, 2020 *| *| US Government Users Restricted Rights - Use, duplication or disclosure *| restricted by GSA ADP Schedule Contract with IBM Corp. *+------------------------------------------------------------------------+ */ define(['../../../lib/@waca/dashboard-common/dist/ui/BaseListView', 'text!./DataSlotsView.html', 'jquery', 'underscore', './DataSlotsViewAuthoringToolbar', '../../../DynamicFileLoader', '../../../dataSources/utils/ShapingConstants', '../../../dataSources/utils/ShapingUIUtils', '../../../visualizations/renderer/filter/FilterLabel', '../../../widgets/livewidget/nls/StringResources', '../../../apiHelpers/SlotAPIHelper', '../../../lib/@waca/baglass/js/baglass/utils/Utils', '../../../lib/@waca/dashboard-common/dist/utils/ActionTypes', '../../../lib/@waca/core-client/js/core-client/utils/dom-utils'], function (BaseListView, template, $, _, DataSlotsViewAuthoringToolbar, DynamicFileLoader, ShapingConstants, ShapingUIUtils, FilterLabel, stringResources, SlotAPIHelper, Utils, ActionTypes, DomUtils) { //These threshold values are taken from Watson Analytics Production Drag and Drop Implementation var DND_X_THRESHOLD = 10; //10 pixels movement for drag and drop data item between slots var DND_Y_THRESHOLD = 10; //10 pixels movement for drag and drop data item between slots /** * Function to convert an object to a representational string * @param {string} key - the key or id associated with this object * @param {object} o - the object to stringize * @param {[string|object]} filter - an array indicating elements to include in stringized version: * each element should be an object with members `key`, representing the key value in the object of the element to include, * and `type`, representing the type and therefore how to stringize the member, if no type is specified, string is assumed. * array elements may also be strings, which are the same as a `key` value in an object, and implicitly of type string. */ function toStringKey(key, o, filter) { return [key, '_'].concat(_.map(filter, function (attr) { return o[attr]; })).join('_'); } function hasDimensions(models) { if (models.length > 0) { for (var index = 0; index < models.length; index++) { if ('attribute' === models[index].getType()) { return true; } } } return false; } /** * This class shows the data slots and context filters used in a data widget. It is used in the Data Widget Focus view. * It inherits BaseListView which handles a11y for lists. */ var DataSlotsView = BaseListView.extend([FilterLabel], { templateString: template, events: { 'click ul.fields div.layerTitle div.layerExpanderButton': 'expandCollapseLayers', 'click ul.fields .slot.columnItem .listitem .menuoverflow': 'popupSlotOptions', 'click ul.fields .localFilter.columnItem .listitem:not(.unavailable) .menuoverflow': 'popupFilterOptions', 'click ul.fields .localFilter.columnItem .listitem:not(.unavailable) .slotInfo': 'openContextFilter', 'primaryaction ul.fields .localFilter': 'setAddFocus', 'primaryaction div.infographicZone': 'setAddFocus', 'mousedown ul.fields .slot .listitem:not(.unavailable)': 'onMouseDown', //To capture the positioning of the mousedown to determine the intention of the user to drag and drop or to open the Authoring Toolbar 'mousemove ul.fields .slot .listitem:not(.unavailable)': 'onDragStart', 'mouseup ul.fields .slot .listitem:not(.unavailable)': 'onMouseUp', //To clear the drag and drop states 'touchstart ul.fields .slot .listitem:not(.unavailable)': 'onMouseDown', //To capture the positioning of the mousedown to determine the intention of the user to drag and drop or to open the Authoring Toolbar 'touchmove ul.fields .slot .listitem:not(.unavailable)': 'onDragStart', 'touchend ul.fields .slot .listitem:not(.unavailable)': 'onMouseUp', //To clear the drag and drop states 'dragup ul.fields .slot .listitem:not(.unavailable)': 'onDragStart', 'dragdown ul.fields .slot .listitem:not(.unavailable)': 'onDragStart', 'keyup ul.fields .slot .listitem:not(.unavailable)': 'onEnterSlot', 'keyup ul.fields .localFilter .listitem:not(.unavailable)': 'onEnterLocalFilters', 'keydown ul.fields .slot .listitem, .dropfirst, .dropafter': 'onKeyDown', 'keyup ul.fields div.layerTitle div.layerExpanderButton': 'onEnterLayer', 'deleteaction ul.fields .localFilter .listitem:not(.missingFilter)': 'onDeleteKeyContextFilter', 'wheel ul.list.fields.slots': 'onScrollSlotsPanel' }, init: function init(options) { // Note: Don't do this. visAPI = this.widget.visAPI. // Instead, use this.getModel function to get the visAPI. // The visAPI could be recreated for the widget hereby rendering // the object this.visAPI obsolete. see defect 245997 DataSlotsView.inherited('init', this, arguments); // @todo: consider removing this.dashboardApi = options.dashboardApi; this.visualization = options.visualizationApi; this.slotsApi = this.visualization.getSlots(); this.localFilters = this.visualization.getLocalFilters(); this._dndManager = options.dndManager; this.widget = options.widget; this.content = this.widget.content; this.interactivityController = this.content.getFeature('InteractivityController.deprecated'); this.missingFilters = options.missingFilters || []; this._changeEvents = { 'change:graphic': 1 }; this._registerEvents(); //region layer is open by default this._visibleLayers = ['data.region']; this.transactionApi = this.dashboardApi.getFeature('Transaction'); this.dataSources = this.dashboardApi.getFeature('DataSources'); this.iconsFeature = this.dashboardApi.getFeature('Icons'); this.visMapColumnsToSlot = this.content.getFeature('VisDnD.utils'); }, onKeyDown: function onKeyDown(event) { //Ctrl + V. Paste selected items in tree to slots if (event.keyCode === 86 && event.ctrlKey) { var moduleApi = this._getVisAPI().getModule(); var mockDragObject = ShapingUIUtils.getCopiedTreeItems(moduleApi); if (!mockDragObject) { if (this.copyMappedItem) { mockDragObject = this.copyMappedItem; } else { return false; } } var dropNode = this.getFocusedDropNode(); mockDragObject.droppable = !this._isDropNodeInvalid(mockDragObject, dropNode); if ($(dropNode).hasClass('localFilter') && this.acceptsContext(mockDragObject)) { this.onDropContext(mockDragObject, dropNode); } else if (this.accepts(mockDragObject)) { this.onDrop(mockDragObject, dropNode); } ShapingUIUtils.clearCopiedTreeItems(); this.copyMappedItem = undefined; this.dashboardApi.triggerDashboardEvent('dataSourceGrid:clearSourceSelected'); } //Ctrl + C, copy item for a swap operation. if (event.keyCode === 67 && event.ctrlKey) { ShapingUIUtils.clearCopiedTreeItems(); this.copyMappedItem = this.getFocusedDragObject(); } }, getFocusedDropNode: function getFocusedDropNode() { var $element = $(document.activeElement); if ($element.hasClass('dropfirst')) { $element = $element.parent().parent(); } return $element[0]; }, getFocusedDragObject: function getFocusedDragObject() { var $element = $(document.activeElement); if (!$element.hasClass('listitem')) { return false; } $element.addClass('addFocus'); var mockDragObject = { $el: $element, type: 'slot', data: { slotId: $element.attr('data-slot-id'), mappingId: $element.attr('data-mapping-id') } }; return mockDragObject; }, getGraphicItem: function getGraphicItem() { var graphic = []; var graphicContent = this.content.getPropertyValue('value.graphic.content'); if (graphicContent) { graphic.push({ content: graphicContent, fillColor: this.content.getPropertyValue('value.graphic.fillColor'), currentScaleOption: this.content.getPropertyValue('value.graphic.currentScaleOption') }); } return graphic; }, onDropShapeWidget: function onDropShapeWidget(dragObject) { this.widget.onAddShapeWidget(dragObject.widgetSpec.model); this.updateShapeSlot(); }, updateShapeSlot: function updateShapeSlot() { var graphic = this.getGraphicItem(); if (graphic.length) { var $infographicNode = this.$el.find('div.infographicZone'); $infographicNode.removeClass('draggedOn'); $infographicNode.find('div.graphic').html(graphic[0].content); } }, rebuildEvents: function rebuildEvents() { for (var e in this._changeEvents) { if (this._changeEvents.hasOwnProperty(e)) { this._changeEvents[e].remove(); } } this._registerEvents(); }, _registerEvents: function _registerEvents() { this.widget.model.on('change:localFilters', this._onChangeEvent, this); this.visualization.on('change:type', this._onChangeEvent, this); this.visualization.on('change:slots', this._onChangeEvent, this); this.content.on('change:property:value.graphic.content', this._onChangeEvent, this); var m = this._getVisAPI(); for (var e in this._changeEvents) { if (this._changeEvents.hasOwnProperty(e)) { this._changeEvents[e] = m.on(e, this._onChangeEvent, this); } } }, remove: function remove() { DataSlotsView.inherited('remove', this, arguments); this.widget.model.off('change:localFilters', this._onChangeEvent, this); //this.widget.model.off('change:data', this._onChangeEvent, this); //this.widget.model.off('change:slotmapping', this._onChangeEvent, this); this.visualization.off('change:slots', this._onChangeEvent, this); this.visualization.off('change:type', this._onChangeEvent, this); this.visualization.off('change:type', this._onChangeEvent, this); this.content.off('change:property:value.graphic.content', this._onChangeEvent, this); this.widget.model.off('change:localFilters', this._onChangeEvent, this); for (var e in this._changeEvents) { if (this._changeEvents.hasOwnProperty(e)) { this._changeEvents[e].remove(); this._changeEvents[e] = null; } } this._changeEvents = []; this._clearToolbar(); this._clearDropTargets(); this.isOutsideFocusMode = false; this.focusModeBoundingClientRect = null; //To destroy the Event.js states since views inherit from Events.js DataSlotsView.inherited('destroy', this, arguments); }, _clearDropTargets: function _clearDropTargets() { if (this._dndManager) { this.$('.listitem, ul.fields .localFilter').map(function (index, el) { this._dndManager.removeDropTarget(el); }.bind(this)); } }, _clearToolbar: function _clearToolbar() { this.launchedToolbarNodeId = null; this.isImplicitClosingTheBoolbar = false; if (this._actionMenuToolbar) { _.each(this._actionMenuToolbar.selectionNodes, function (node) { $(node).removeClass('selected'); }); //Passing true to request the 'toolbar:remove' to trigger this._actionMenuToolbar.remove(true); this._actionMenuToolbar = null; } var $node = this.$('.addFocus'); if ($node.length > 0) { $node.removeClass('addFocus'); } $node = this.$('.selected'); if ($node.length > 0) { $node.removeClass('selected'); } var $nodeList = this.$('.draggedOn'); _.each($nodeList, function (node) { $node = $(node); $node.removeClass('draggedOn'); }); }, /** * @param moduleRef (optional) - a moduleReference (id) which allows access to modules other than the default module. * @returns module information if the module has been loaded. */ getModule: function getModule(moduleRef) { return this._getVisAPI().getModule(moduleRef); }, /** * Builds up a list of slots to display on the dataslots view. If this chart has an archetype, this is * a merger of all the archetype slots in addition to the slots that are part of the visualisation */ getSlotsInfo: function getSlotsInfo(hasCategory, localFilters) { var _this = this; return _.map(this.slotsApi.getSlotList(), function (slot) { return _this._buildSlotInfo(slot, hasCategory, localFilters); }); }, _isMultiMeasureSeries: function _isMultiMeasureSeries(dataItem) { return dataItem.getColumnId() === '_multiMeasuresSeries'; }, _getSlotIcon: function _getSlotIcon(slotDef) { var slotIconId = slotDef.getIcon(); var icon = {}; if (slotIconId) { icon = this.iconsFeature.getIcon(slotIconId); return icon; } else { return slotDef.getType() === 'ordinal' ? this.iconsFeature.getIcon('values') : this.iconsFeature.getIcon('category'); } }, _buildSlotInfo: function _buildSlotInfo(slot, hasCategory, localFilters) { var _this2 = this; var slotDef = slot.getDefinition(); var icon = this._getSlotIcon(slotDef); var mappings = _.map(slot.getDataItemList(), function (dataItem) { var metadataColumn = dataItem.getMetadataColumn(); var isMultiMeasuresSeries = _this2._isMultiMeasureSeries(dataItem); var isUnavailable = metadataColumn ? metadataColumn.isHidden() || metadataColumn.isMissing() : !isMultiMeasuresSeries; var mappingInfo = { columnId: dataItem.getColumnId(), name: isUnavailable ? stringResources.get('missingColumn', { 'columnLabel': _.escape(dataItem.getLabel()) }) : _.escape(dataItem.getLabel()), mappingId: dataItem.getId(), configurable: !isMultiMeasuresSeries, unavailable: isUnavailable ? metadataColumn : null }; if (hasCategory && localFilters) { _this2._buildFilterMappingInfoForDataItem(dataItem, slot, localFilters, mappingInfo); } return mappingInfo; }); var isMapped = slot.getDataItemList().length > 0; var isShapeEnabled = !!this.content.getPropertyValue('value.graphic.content'); var slotInfo = { 'id': slotDef.getId(), // This is used for UI only not the acutal dataset 'dataset': this._isSharingSlots() ? 'data' : slotDef.getDatasetIdList()[0], 'group': slotDef.getGroupId(), 'caption': slotDef.getCaption(), 'icon': icon, 'hidden': slotDef ? slotDef.isHidden() : false, 'mappings': mappings, 'noMapping': isMapped ? '' : 'noMapping', 'deletable': isMapped, 'optional': slotDef.isOptional(), 'showRequiredMarker': slotDef.getShowRequiredMarker(), 'shapeDropEnabled': isShapeEnabled, 'shapable': slotDef ? slotDef.isShapable() : false }; return slotInfo; }, _buildFilterMappingInfoForDataItem: function _buildFilterMappingInfoForDataItem(dataItem, slot, localFilters, mappingInfo) { var _this3 = this; var columnFilter = _.find(localFilters.models, function (filter) { if (!_this3._getVisAPI().isFilterEditable(filter)) { return false; } if (filter.filterBins) { if (dataItem.getBinning()) { return filter.id === dataItem.getId(); } } else if (!filter.filterBins) { var columnId = dataItem.getColumnId(); if (filter.aggregationType) { return filter.columnId === columnId && dataItem.getAggregation() === filter.aggregationType; } else { // If the case is when there is an ordinal and attribute we dont want to // display the filter beneath it. return filter.columnId === columnId && !(slot.getDefinition().getType() === 'ordinal' && dataItem.getType() === 'attribute'); } } }); //Only include the 'first filter' against an item (it can be edited). //Extra filters for an item or tuple (usually created with additional keeps/excludes) are shown as part of localFilters (and cant be edited) if (columnFilter) { var md = dataItem.getMetadataColumn(); var columnMetadata = { getDataType: md.getDataType.bind(md), getType: md.getType.bind(md), getFormat: function getFormat() { return dataItem.getFormat(); } }; mappingInfo.filterString = this.getFilterLabel(columnFilter, columnMetadata); mappingInfo.numberOfFilters = this.getFilterNumber(columnFilter); } }, /** * if binning is already applied but the vis is not supporting the binning or slot type is ordinal, * remove binning and local filters, this is needed when we change vis. * * @param slot A slot */ _clearBinningDefnIfNeeded: function _clearBinningDefnIfNeeded() /*slot, transactionToken*/{ // TODO livewidget_cleanup -- how does this relate to the cleanup happening inside the API //slot.setBinning(null, transactionToken); }, _getVisAPI: function _getVisAPI() { return this.widget.visAPI; }, /* Gets the number of filters applied on a selection filter view * @params columnFilter the filter applied. * @params json if the filter is in a json formatAction * @return the number of filters applied. */ getFilterNumber: function getFilterNumber(columnFilter, json) { if (columnFilter.operator === 'in' || columnFilter.operator === 'notin' || columnFilter.operator === 'isnull') { var filterNum; if (json) { filterNum = columnFilter.values.length; } else { filterNum = columnFilter.values.models.length; } return filterNum; } return; }, getLocalFiltersList: function getLocalFiltersList() { return this._getVisAPI().getLocalFiltersList(); }, getContextFilters: function getContextFilters() { var filters = []; var dataSource = this.visualization.getDataSource(); _.each(this.getLocalFiltersList(), function (localFilter) { var columnId = localFilter.columnId; if (columnId) { var metadataColumn = dataSource.getMetadataColumn(columnId); if (metadataColumn) { filters.push({ id: localFilter.id, columnId: localFilter.columnId, name: metadataColumn.getLabel(), filterString: this.getFilterLabel(localFilter, metadataColumn), numberOfFilters: this.getFilterNumber(localFilter, true) }); } } }.bind(this)); return filters; }, isMappingComplete: function isMappingComplete() { return this.visualization.getSlots().isMappingComplete(); }, // if there is anything to preserve from the previous mapping // do so here _preserveData: function _preserveData() /*slot, mapping*/{ // overriden by children }, _convertTransactionTokenToLegacyOptions: function _convertTransactionTokenToLegacyOptions(transactionToken) { var oldOptions = {}; if (transactionToken) { oldOptions.payloadData = { transactionToken: transactionToken }; if (transactionToken.transactionId) { oldOptions.payloadData.undoRedoTransactionId = transactionToken.transactionId; } } return oldOptions; }, /** * Performs swap mapping of slot data item 1 and slot data item 2 */ _swapSlotMapping: function _swapSlotMapping(dragObject, dropNode, transactionToken) { var sourceDataItemId = dragObject.data.mappingId; var targetDataItemId = dropNode.getAttribute('data-mapping-id'); // If we are swapping onto same item, we are done if (sourceDataItemId === targetDataItemId) { return; } var options = {}; var sourceSlotId = dragObject.data.slotId; var targetSlotId = dropNode.getAttribute('data-slot-id'); if (targetSlotId === sourceSlotId) { // Handle swapping within the same slot options.afterItem = $(dropNode).is('.dropafter') || $(dropNode).is('.slot'); if (options.afterItem) { targetDataItemId = $(dropNode).prev().attr('data-mapping-id'); } this._swapItemsInSlot(targetSlotId, sourceDataItemId, targetDataItemId, options, transactionToken); } else if (!targetDataItemId) { // Handle moving an item var targetIndex = parseInt($(dropNode).attr('map-index'), 10); this._moveItemToSlot(targetSlotId, sourceDataItemId, targetIndex, transactionToken); } else { this._swapItemsBetweenSlots(sourceSlotId, targetSlotId, sourceDataItemId, targetDataItemId, transactionToken); } }, _swapItemsBetweenSlots: function _swapItemsBetweenSlots(sourceSlotId, targetSlotId, sourceDataItemId, targetDataItemId, transactionToken) { // Update target slot items var targetSlot = this.slotsApi.getSlot(targetSlotId); var targetDataItemRefs = _.map(targetSlot.getDataItemList(), function (dataItem) { return dataItem.getId(); }); var targetIndex = targetDataItemRefs.indexOf(targetDataItemId); targetDataItemRefs[targetIndex] = sourceDataItemId; // Update source slot items var sourceSlot = this.slotsApi.getSlot(sourceSlotId); var sourceDataItemRefs = _.map(sourceSlot.getDataItemList(), function (dataItem) { return dataItem.getId(); }); var sourceIndex = sourceDataItemRefs.indexOf(sourceDataItemId); sourceDataItemRefs[sourceIndex] = targetDataItemId; // swap this.slotsApi.setDataItems(targetDataItemRefs, targetSlotId, transactionToken); this.slotsApi.setDataItems(sourceDataItemRefs, sourceSlotId, transactionToken); // @todo needs to be revise to be done behind the api this._clearBinningDefnIfNeeded(this.visualization.getSlots().getSlot(targetSlotId), transactionToken); }, _swapItemsInSlot: function _swapItemsInSlot(slotId, sourceDataItemId, targetDataItemId, options, transactionToken) { var slot = this.slotsApi.getSlot(slotId); var dataItemRefs = _.map(slot.getDataItemList(), function (dataItem) { return dataItem.getId(); }); var sourceIndex = dataItemRefs.indexOf(sourceDataItemId); var targetIndex = dataItemRefs.indexOf(targetDataItemId); if (options.afterItem) { dataItemRefs.splice(targetIndex + 1, 0, sourceDataItemId); dataItemRefs.splice(sourceIndex > targetIndex ? sourceIndex + 1 : sourceIndex, 1); } else { dataItemRefs[sourceIndex] = targetDataItemId; dataItemRefs[targetIndex] = sourceDataItemId; } // swap this.slotsApi.setDataItems(dataItemRefs, slotId, transactionToken); }, _moveItemToSlot: function _moveItemToSlot(targetSlotId, sourceDataItemId, targetIndex, transactionToken) { // Update target slot var targetSlot = this.slotsApi.getSlot(targetSlotId); var targetDataItemRefs = targetSlot.getDataItemList().map(function (dataItem) { return dataItem.getId(); }); // Add item to the target slot if (targetIndex === -1) { // At the end targetDataItemRefs.push(sourceDataItemId); } else { // Somewhere inside targetDataItemRefs.splice(targetIndex, 0, sourceDataItemId); } // move this.slotsApi.setDataItems(targetDataItemRefs, targetSlotId, transactionToken); this._clearBinningDefnIfNeeded(this.visualization.getSlots().getSlot(targetSlotId), transactionToken); }, accepts: function accepts(dragObject, targetNode) { var acceptsOptions = { dropTarget: ShapingConstants.DROP_TARGET_OPTIONS.SLOT }; if (targetNode) { acceptsOptions.targetNode = targetNode; } return dragObject.type === 'slot' || this.widget.accepts(dragObject, acceptsOptions); }, _getMetadataPayloadColumns: function _getMetadataPayloadColumns(dragObject) { return dragObject && dragObject.data && dragObject.data.columns ? dragObject.data.columns : null; }, _getDropMetadataColumns: function _getDropMetadataColumns(dragObject) { return dragObject && dragObject.data && dragObject.data.columns; }, acceptsContext: function acceptsContext(dragObject) { var droppedColumns = this._getMetadataPayloadColumns(dragObject); if (droppedColumns) { var metadataColumns = _.map(droppedColumns, function (column) { return column.metadataColumn; }); if (metadataColumns.length > 0 && metadataColumns[0].getType() === 'fact') { var dataItemAPIs = []; _.each(this.visualization.getSlots().getMappedSlotList(), function (mappedDataSlot) { dataItemAPIs = dataItemAPIs.concat(mappedDataSlot.getDataItemList()); }); // When data slots are empty, do not allow a numeric data item to be dropped to local filter area // Since the minmax value without context doesn't provide any meaning after a data item is dropped to a slot. if (dataItemAPIs.length === 0 || !hasDimensions(dataItemAPIs)) { return false; } } } return this.widget.accepts(dragObject, { dropTarget: ShapingConstants.DROP_TARGET_OPTIONS.FILTER }); }, onMouseDown: function onMouseDown(event) { this.startDrag = false; this.initialDragAndDropPosition = DomUtils.getEventPos(event); }, onMouseUp: function onMouseUp(event) { this.startDrag = false; delete this.initialDragAndDropPosition; this.itemNotDragged(event); }, onDragStart: function onDragStart(event) { if (!this._beginDragAndDrop(event) || this.startDrag) { return true; } this.startDrag = true; //drag and drop occurs so destroy the current opened toolbar if there is one opened this._clearToolbar(); var $target = $(this.getTarget(event.target, 'listitem')); this.dropInfo = { operation: 'swap', initialTarget: $target, slotId: $target.attr('data-slot-id'), mappingId: $target.attr('data-mapping-id') }; var menuOverflowIcon = this.iconsFeature.getIcon('overflowMenuHorizontal32'); var disableIcon = this.iconsFeature.getIcon('common-nodrop'); var $slotInfo = $target.find('.slotInfo').clone(false, false); var $menu = $('
'); var $unvalidAction = $(''); var $avatar = $('', { 'class': 'listitem columnName avatarLive' }); $avatar.append($unvalidAction).append($slotInfo).append($menu); this._dndManager.startDrag({ event: event, type: 'slot', data: this.dropInfo, avatar: $avatar, callerCallbacks: { onDragDone: this.onDragStop.bind(this, $target), onMove: this.onMove.bind(this) }, moveXThreshold: 20, moveYThreshold: 20 }); $target.addClass('isDragged'); return true; }, onDragStop: function onDragStop($target, event, payload) { this.startDrag = false; delete this.initialDragAndDropPosition; payload = payload || {}; if (this.isOutsideFocusMode && payload.isDropped === false) { this.isOutsideFocusMode = false; var mapIndex = $target.attr('map-index'); var slotId = $target.attr('data-slot-id'); var slot = this.visualization.getSlots().getSlot(slotId); this.interactivityController.getActionHelper().getActionsForSlots([slot], mapIndex, $target[0]).then(function (actions) { for (var index = 0; index < actions.length; index++) { if (actions[index].spec && actions[index].spec.name == 'delete' && actions[index].spec.actions && _.isFunction(actions[index].spec.actions.apply)) { actions[index].spec.actions.apply(); break; } } }); return true; } else { return payload.isDropped; } }, /** * Callback to handle the move event * * @param {object} event - the event object * @param {object} payload - the payload to process the event object */ onMove: function onMove(event, payload) { payload = payload || {}; var dragObject = payload.dragObject || {}; this.isOutsideFocusMode = this._isOutsideFocusModeBoundary(dragObject); var $unvalid = $(dragObject.avatar).children('.unvalid'); if (this.isOutsideFocusMode) { $unvalid.addClass('hidden'); } else { var $target = $(payload.dropTargetNode); if ($target.hasClass('listitem') || $target.hasClass('dropafter') || $target.hasClass('slot') || $target.hasClass('localFilter')) { this.onDragEnter(dragObject, payload.dropTargetNode); } else { $unvalid.removeClass('hidden'); } } }, itemNotDragged: function itemNotDragged(event) { var $target = $(this.getTarget(event.target, 'listitem')); $target.removeClass('isDragged'); }, /** * Callback to handle the drag enter event * * @param {object} dragObject - the drag object context * @param {object} dropNode - the node potentially being dropped */ onDragEnter: function onDragEnter(dragObject, dropNode) { dragObject.droppable = !this._isDropNodeInvalid(dragObject, dropNode); if (dragObject.droppable) { $(dropNode).addClass('draggedOn'); if ($(dropNode).is('.columnItem')) { $(dropNode).find('.dropfirst').addClass('draggedOn'); } $(dragObject.avatar).children('.unvalid').addClass('hidden'); } else { $(dragObject.avatar).children('.unvalid').removeClass('hidden'); } }, /** * Handler when a data item is dragged on a data slot, to replace the previous slot. * @param dragObject - the drag and drop payload from metadata tree or data strip. * @param dropNode - the target of the drop. */ onDrop: function onDrop(dragObject, dropNode) { if (!dragObject.droppable) { return; //Do nothing because, the dnd case is prohibited. } var transactionToken = this.transactionApi.startTransaction(); $(dropNode).removeClass('draggedOn'); this._setDataSource(dragObject, transactionToken); // The viz might not support the datasource so it might not be set var isDataSourceSet = !!this.visualization.getDataSource(); if (isDataSourceSet) { if (dragObject.type === 'slot') { this._swapSlotMapping(dragObject, dropNode, transactionToken); } else { this._addSlotMapping(dragObject, dropNode, transactionToken); } } this.transactionApi.endTransaction(transactionToken); }, _setDataSource: function _setDataSource(dragObject, transactionToken) { var sourceId = dragObject.data.sourceId; if (sourceId) { this.visualization.setDataSource(sourceId, transactionToken); } }, _addSlotMapping: function _addSlotMapping(dragObject, dropNode, transactionToken) { var droppedColumns = this._getDropMetadataColumns(dragObject); var metadataColumns = _.map(droppedColumns, function (column) { return column.metadataColumn; }); this.widget.addModelFilters(metadataColumns, transactionToken); var slotId = dropNode.getAttribute('data-slot-id'); var options = { position: parseInt($(dropNode).attr('map-index'), 10), bReplace: $(dropNode).is('.listitem') }; this.visMapColumnsToSlot.mapColumns(slotId, droppedColumns, options, transactionToken); }, /*Data slot view that has unavailable columns*/ _hasDataUnavailable: function _hasDataUnavailable($dropNode) { return $dropNode.is('.noMapping .unavailable'); }, /** * Handler when a data item is dropped in the Context Filter section. It adds it as Context Filter to the Data widget. * The whole section is the drop zone, not each filter slot. If a data item is already a filter, it will bring up the UI to update it. */ onDropContext: function onDropContext(dragObject) { var transactionToken = this.transactionApi.startTransaction(); this._setDataSource(dragObject, transactionToken); // The viz might not support the datasource so it might not be set var isDataSourceSet = !!this.visualization.getDataSource(); if (isDataSourceSet) { var droppedColumns = this._getMetadataPayloadColumns(dragObject); if (droppedColumns) { var metadataColumns = _.map(droppedColumns, function (column) { return column.metadataColumn; }); if (metadataColumns) { metadataColumns = this.widget.addModelFilters(metadataColumns, transactionToken); if (metadataColumns.length > 0) { var filterColumnId = metadataColumns[0].getId(); // don't process filter on the dataItem for a ordinal only view (ie: summary widget) var dataSlot = this.slotsApi.getSlot(filterColumnId); if (!dataSlot || dataSlot.getDefinition().getType() !== 'ordinal' || this._definitionHasCategory()) { var columnsWithMembers = dragObject.data.utils.getColumnsWithMembers(true /* filterMemberColumns */); var columnIds = Object.keys(columnsWithMembers); if (columnIds.length) { this._addLocalFiltersWithMembers(dragObject, transactionToken); } else { this._setContextFilter(metadataColumns[0], this.$('ul.fields .localFilter')[0], false, transactionToken); } } } } } } this.transactionApi.endTransaction(transactionToken); }, _definitionHasCategory: function _definitionHasCategory() { var definition = this.visualization.getDefinition(); if (definition) { var dataSlots = definition.getSlotList(); for (var i = 0; i < dataSlots.length; i++) { if (dataSlots[i].getType() === 'category' || dataSlots[i].getType() === 'any') { return true; } } } return false; }, _addLocalFiltersWithMembers: function _addLocalFiltersWithMembers(dragObject, transactionToken) { var columns = this._getDropMetadataColumns(dragObject); this.content.getFeature('VisDnD.utils').addMembersAsLocalFilters({ visualizationAPI: this.visualization, columns: columns }, transactionToken); }, /** * Handler for the delete key on filter. In this case, we only want the Delete and Backspace key to work. */ onDeleteKeyContextFilter: function onDeleteKeyContextFilter(e) { this.onRemoveContextFilter(e); }, onEnterSlot: function onEnterSlot(e) { if (e.keyCode === 32 || e.keyCode === 13) { this.popupSlotOptions(e); } }, onEnterLocalFilters: function onEnterLocalFilters(e) { if (e.keyCode === 32 || e.keyCode === 13) { this.popupFilterOptions(e); } }, onEnterLayer: function onEnterLayer(e) { if (e.keyCode === 32 || e.keyCode === 13) { this.expandCollapseLayers(e); } }, /** * Finds the proper slot data item element, verifies if the proper keys were pressed to delete. */ _getTargetSlotDataItem: function _getTargetSlotDataItem(e) { return $(this.getTarget(e.target, 'listitem')); }, /** * Handler for the delete button in a Context Filter, visualization will be updated. */ onRemoveContextFilter: function onRemoveContextFilter(e) { e.preventDefault(); e.stopPropagation(); this._clearToolbar(); var $slotDataItem = this._getTargetSlotDataItem(e); if ($slotDataItem) { var filterId = $slotDataItem.attr('data-filter-id'); if (filterId) { var visAPI = this._getVisAPI(); // we do not need to update the DOM as the slots re-render after filters modification completes visAPI.localFilters.removeFilterEntry({ id: filterId }); visAPI.localFilters.allFilterModificationComplete(); } } }, /** * no-op. We don't use selection for the slots. Needs to implement it because we use BaseListView. */ onSelectItem: function onSelectItem() { return false; }, _onChangeEvent: function _onChangeEvent() { this.slotsViewContentModified = true; this.render(); }, _onToolBarRemove: function _onToolBarRemove() /*options*/{ //Handle after the toolbar is removed //This case occurs when the user click once to open the toolbar and click again to close the toolbar //Since reRender is begin called by 'toolbar:hide' and 'toolbar:remove' have an options parameter //Having this onToolBarRemove function avoid sending the wrong parameter value to the reRender calls this._reRender(); }, _reRender: function _reRender() { if (this.isImplicitClosingTheBoolbar) { this._clearToolbar(); } if (this.slotsViewContentModified) { this.render(); this.slotsViewContentModified = false; } }, /** * Renders the UI based on a Dot template. It creates two lists, one for data slots and one for context filters. */ render: function render() { var _this4 = this; // If dataslotsview is being torn down (destroy function will remove the dotTemplate). if (!this.dotTemplate) { return; } if (this._actionMenuToolbar) { //The actionMenu is showing. For simple actions like drill, it is simply closed //For actions that have parameters, the actionMenu is a 'subview' (eg sort, navigate, format etc.) //For subviews, the ui stays open and slots are only rendered when the subview is explicitly closed (or loses focus). if (this._actionMenuToolbar.itemMap && this._actionMenuToolbar.itemMap.subview) { return; } //No need to re-render the view since the tool bar gets delete here this.slotsViewContentModified = false; this._clearToolbar(); } var hasCategory = this._definitionHasCategory(); var localFilters = this._getVisAPI().localFilters || {}; //Gather the info for each slot in an array to be passed to the Dot Template.= var slots = this.getSlotsInfo(hasCategory, localFilters); var maxWidth = this.$el.width(); var filters = this.getContextFilters(); var graphic = this.getGraphicItem(); if (!this._needToReRender(slots, filters, graphic)) { return this; } // rendering starts here. // We will refresh the UI, remove the previous elements that were added as drop targets. this._clearDropTargets(); this._updateMissingFilters(); //maintain the scrollposition of the slots before rerendering var slotsViewScrollPosition = this.$el.find('.list.fields.slots').scrollTop(); // Generate the html from the template using the slots and filters info collected above. //Passing in underscore as an option. var definition = this._getVisAPI().getDefinition(); var chevronRightIcon = this.iconsFeature.getIcon('common-chevron_right'); var menuOverflowIcon = this.iconsFeature.getIcon('overflowMenuHorizontal32'); var errorIcon = this.iconsFeature.getIcon('common-warning'); var closeIcon = this.iconsFeature.getIcon('common-close_16'); var filterIcon = this.iconsFeature.getIcon('common-filter'); var informationIcon = this.iconsFeature.getIcon('getInformation'); var sHtml = this.dotTemplate({ headerLabel: definition ? definition.caption : stringResources.get('evColumns'), isFilterable: hasCategory, slots: slots, datasets: definition ? this._getDatasets(definition.datasets) : null, visibleLayers: this._getVisibleLayers(), //todo api to get the dataslots groupedSlotsByDataset: this._getGroupedSlotsByDataset(slots), maxWidth: maxWidth + 'px', filterStringMaxWidth: maxWidth * 0.7 + 'px', ContextFiltersLabel: stringResources.get('evLocalFilters'), filterLabel: stringResources.get('evFilterTooltip'), filters: filters, missingFilters: this.missingFilters || [], missingFiltersLabel: stringResources.get('missingFiltering'), isMappingComplete: this.isMappingComplete(), graphic: graphic, infographicShapeLabel: stringResources.get('toolbarActionToggleShapeDrop'), requiredFieldsDescription: stringResources.get('LIVE_slots_required_field_description', { asterisk: '*' }), dragAndDropDescription: stringResources.get('LIVE_slots_drag_and_drop_description'), expandLabel: stringResources.get('evExpand'), collapseLabel: stringResources.get('evCollapse'), underscore: _, chevronRightIcon: chevronRightIcon.id, menuOverflowIcon: menuOverflowIcon.id, errorIcon: errorIcon.id, closeIcon: closeIcon.id, filterIcon: filterIcon.id, informationIcon: informationIcon.id }); this._detachEvents(); this.$el.empty().append(sHtml); this.$el.find('.list.fields.slots').scrollTop(slotsViewScrollPosition); this._hideText(); this.setElement(this.$el); //Add the 4 drop targets this._addDropTargets(); var $widgetExpanded = this.$el.closest('.widgetExpanded'); if ($widgetExpanded.length > 0) { this.focusModeBoundingClientRect = $widgetExpanded[0].getBoundingClientRect(); } this.updateShapeSlot(); // attempt coach marks to all slots setTimeout(function () { _.each(_this4.slotsApi.getSlotList(), function (slot) { _this4._applySlotCoachMarks(slot); }); }, 500); return this; }, /** * Apply coach marks on the slot or data item * The slot coach mark consists with: * - showPopover: boolean flag indicating whether the popover show along with the coach mark. * - mappedOnly: boolean flag indicating whether the coach mark applies when the slot is mapped or * regardless of the mapped state. * - titleResource: resource name for the coach title. * - contentResource: resource name for the coach content. * @param slot {Object} - SlotAPI * @return {Promise} */ _applySlotCoachMarks: function _applySlotCoachMarks(slot) { var slotDef = slot.getDefinition(); var coachMark = slotDef.getCoachMark(); if (coachMark) { var isMapped = slot.getDataItemList().length > 0; if (!coachMark.mappedOnly || isMapped) { var slotId = slot.getId(); var selector = (isMapped ? '.listitem.columnName' : '.slot') + '[data-slot-id=' + slotId + ']'; // Make the coachmark unique for each slot of every visualization var options = { id: this.visualization.getDefinition().getId() + '#' + slotId, $el: this.$el.find(selector), title: stringResources.get(coachMark.titleResource), contents: stringResources.get(coachMark.contentResource), showPopover: coachMark.showPopover }; this.dashboardApi.prepareGlassOptions(options); return Utils.addCoachmark(options); } } return Promise.resolve(); }, _getGroupedSlotsByDataset: function _getGroupedSlotsByDataset(slots) { return _.groupBy(slots, 'dataset'); }, _getVisibleLayers: function _getVisibleLayers() { return this._visibleLayers; }, _expandLayer: function _expandLayer(layer) { this._visibleLayers.push(layer); }, _collapseLayer: function _collapseLayer(layer) { this._visibleLayers = _.without(this._visibleLayers, layer); }, _hideText: function _hideText() { var $mappings = $('.columnItem:not(.noMapping)'); $('.columnLabel').removeClass('hidden'); _.each($mappings, function (mapItem) { var $mapItem = $(mapItem); $mapItem.find('.columnLabel').addClass('hidden'); }); }, _changeTabindices: function _changeTabindices() /*a, b*/{ // DataSlotsView.inherited('_changeTabIndices', this, null); return false; }, /** * Verifies if the drop node is valid for dropping the item. */ _isDropNodeInvalid: function _isDropNodeInvalid(dragObject, dropNode) { var $dropNode = $(dropNode); if ($dropNode.hasClass('localFilter')) { return false; } // @todo need to be revisited to use the new API // problem identified so far is in SlotAPI.supportsColumns (aka. itemsNotSupported in old SlotAPI) // in order to check the support availability the metadata needs to be a loaded which is not always the case // in the new SlotAPI var slots = this.visualization.getSlots(); var destinationSlot = slots.getSlot(dropNode.getAttribute('data-slot-id')); var slotId = dragObject.data && dragObject.data.slotId; var dragObjectSlot = slotId ? slots.getSlot(slotId) : null; var dragMetadata = void 0; var dragMembers = void 0; if (dragObject.data.columns) { dragMetadata = _.map(dragObject.data.columns, function (column) { return column.metadataColumn; }); dragMembers = dragObject.data.utils.getColumnsWithMembers(); var source = this.dataSources.getDataSource(dragObject.data.sourceId); // TODO livewidget_cleanup .. the dragMetadataObject should retun new metadata objects. dragMetadata = dragMetadata.map(function (legacy) { return source.getMetadataColumn(legacy.getId()); }); } else { if (dragObjectSlot) { dragMetadata = dragObjectSlot.getDataItemList()[dragObject.data.initialTarget[0].getAttribute('map-index')].getMetadataColumn(); } } var itemsNotSupported = !destinationSlot.supportsColumns(dragMetadata || []); //Get the dragged object mapping index and slot index specified in the template //If it comes from the metadata tree, it will return NaN for both var dragIndex = { mapIndex: parseInt($(dragObject.data.initialTarget).attr('map-index'), 10), slotIndex: parseInt($(dragObject.data.initialTarget).parents('.slot').attr('slot-index'), 10) }; //Same thing but for the drop node var dropIndex = { mapIndex: parseInt($dropNode.attr('map-index'), 10), slotIndex: $dropNode.is('.slot') ? parseInt($dropNode.attr('slot-index'), 10) : parseInt($dropNode.parents('.slot').attr('slot-index'), 10), slotId: $dropNode.attr('data-slot-id') }; // In the html the the dropzone before an item has the same index as the following item // ---------- index 0 // [listitem] index 0 // ---------- index 1 // [listitem] index 1 // ---------- index 2 // [listitem] index 2 // ---------- index 3 // The there will always be a trailing dropzone with an (n+1) index value; n being the last item's index value //This is to block the case where you drag an item and try to put it before itself or after itself in a same slot. var dropInSamePosition = // you can't drop the dragged item before itself (dragIndex.mapIndex === dropIndex.mapIndex || // you can't drop the dragged item after itself dragIndex.mapIndex + 1 === dropIndex.mapIndex) && // if it's in the same slot and the drop node is not an item dragIndex.slotIndex === dropIndex.slotIndex && !$dropNode.is('.listitem'); var targetSlot = this.slotsApi.getSlot(dropIndex.slotId); var dropSlotDefinition = targetSlot.getDefinition(); //For now there is only one case where it's not valid var isMultiMeasureAndOrdinal = dropSlotDefinition.isMultiMeasureSupported(); //Drag member from a column already projected should not be double counted var itemLimit = dropSlotDefinition.getMaxItems(); itemLimit = itemLimit === -1 ? 0 : itemLimit; var dragMembersFromProjectedColumnsOnly = this._dragMembersFromProjectedColumnsOnly(targetSlot, dragMetadata, dragMembers); var slotNotStackable = !$dropNode.hasClass('listitem') && !isMultiMeasureAndOrdinal && !dropSlotDefinition.isStackItems() && !dragMembersFromProjectedColumnsOnly && parseInt($dropNode.attr('mappedItems'), 10) >= itemLimit; // fix defect 231765 (prevent multimeasure group from being dropped on (thus replaced) from the data tree), but swapping is allowed var dataItemAPI = destinationSlot.getDataItemList()[dropIndex.mapIndex]; var isDropOnMultiMeasureDataItem = SlotAPIHelper.isMultiMeasuresSeriesSlot(destinationSlot) && dataItemAPI ? dataItemAPI.getColumnId() === '_multiMeasuresSeries' && !$dropNode.hasClass('dropafter') && Number.isNaN(dragIndex.slotIndex) : false; return itemsNotSupported || dropInSamePosition || slotNotStackable || isDropOnMultiMeasureDataItem || !dragMembersFromProjectedColumnsOnly && !isMultiMeasureAndOrdinal && this._isExceedsItemsLimit(dropNode, dragObject, dragMembers) || this._isSwapValueSlotItemsWithMultiMeasures(dropNode, dragObject); }, /** * @return true if the new drop object only has members from projected columns */ _dragMembersFromProjectedColumnsOnly: function _dragMembersFromProjectedColumnsOnly(targetSlot, dragColumns, dragMembers) { // 1. Return false if dragMembers is empty if (!dragMembers || _.isEmpty(dragMembers)) { return false; } // 2. Check if there is a dragColumn that is NOT built from drag and drop members var hasNonProjectedColumn = _.some(dragColumns, function (column) { return !dragMembers[column.getId()] || !dragMembers[column.getId()].length; }); // 3. Check all dragMembers are from already projected columns of the target slot if the first condition passes if (!hasNonProjectedColumn) { var projectedColumns = []; _.each(targetSlot.getDataItemList(), function (dataItem) { var metadataColumn = dataItem.getMetadataColumn(); if (metadataColumn) { projectedColumns.push(metadataColumn.getId()); } }); hasNonProjectedColumn = _.some(Object.keys(dragMembers), function (columnId) { return projectedColumns.indexOf(columnId) === -1; }); } return !hasNonProjectedColumn; }, /** * A slot can set the maximum number of the items it contains. * Particularly, for slot that can accept stackItems, slot definition * has a maxStackItems attribute that specifies a maximum number of * stackItems (from VIPR). For Vizs such as Xtab, Grid, Summary that does * not support stackItems attribute, to avoid confusion, we use maxItems attribute * to set the limit. By default, VIPR viz does not support maxItems attribute. **/ _isExceedsItemsLimit: function _isExceedsItemsLimit(dropNode, dragObject, dragMembers) { var slotId = $(dropNode).is('.slot') ? $(dropNode).attr('data-slot-id') : $(dropNode).parents('.slot').attr('data-slot-id'); if (!slotId) { return false; } var targetSlot = this.slotsApi.getSlot(slotId); var slotDef = targetSlot.getDefinition(); var limit = slotDef.getMaxItems() || -1; if (limit < 0) { return false; } var nCountOfItemsToDrop = 1; if (dragObject.data.items) { if (!dragMembers || _.isEmpty(dragMembers)) { nCountOfItemsToDrop = dragObject.data.items.length; } else { // Do not double count member columns that already exist. nCountOfItemsToDrop = 0; var memberParentIds = Object.keys(dragMembers); var mappedItemIds = this._getMappedItemsOfTargetSlot(targetSlot); var diffs = memberParentIds.filter(function (id) { return mappedItemIds.indexOf(id) === -1; }); nCountOfItemsToDrop = diffs.length; } } var nMappedItems = targetSlot.getDataItemList().length; if ($(dropNode).hasClass('listitem') && nCountOfItemsToDrop === 1) { return false; } return nCountOfItemsToDrop + nMappedItems > limit; }, _getMappedItemsOfTargetSlot: function _getMappedItemsOfTargetSlot(slot) { var mappedItems = []; slot.getDataItemList().forEach(function (dataItem) { if (dataItem.getMetadataColumn) { mappedItems.push(dataItem.getMetadataColumn().getId()); } }); return mappedItems; }, _isSwapValueSlotItemsWithMultiMeasures: function _isSwapValueSlotItemsWithMultiMeasures(dropNode, dragObject) { var dragObjectMappingId = dragObject && dragObject.data && dragObject.data.mappingId ? dragObject.data.mappingId : null; var dropNodeSlotId = $(dropNode).is('.slot') ? $(dropNode).attr('data-slot-id') : $(dropNode).parents('.slot').attr('data-slot-id'); var isDropNodeMultiMeasuresSeries = $(dropNode).hasClass('listitem') && $(dropNode)[0].dataset && $(dropNode)[0].dataset.mappingId === '_multiMeasuresSeries'; var dragObjectSlotId = dragObject && dragObject.data ? dragObject.data.slotId : null; var dropSlotAPI = dropNodeSlotId && this.slotsApi.getSlot(dropNodeSlotId).getDefinition(); var dragSlotAPI = dragObjectSlotId && this.slotsApi.getSlot(dragObjectSlotId).getDefinition(); // can not drag a multimeasure to a measure slot or replace a multmeasure dataItem (swapping is allowed) return dragObjectMappingId === '_multiMeasuresSeries' && dropSlotAPI.getType() === 'ordinal' || (!dragSlotAPI || dragSlotAPI.getType() === 'ordinal') && isDropNodeMultiMeasuresSeries; }, _addDropTargets: function _addDropTargets() { var _this5 = this; // Add DnD handlers var fAccept = this.accepts.bind(this); var fAcceptContext = this.acceptsContext.bind(this); var fOnDrop = this.onDrop.bind(this); var fOnDropContext = this.onDropContext.bind(this); var fOnDragEnter = this.onDragEnter.bind(this); var dndManager = this._dndManager; var onDragLeave = function onDragLeave(dragObject, dropNode) { var $dropNode = $(dropNode); $dropNode.removeClass('draggedOn'); $dropNode.find('.dropfirst').removeClass('draggedOn'); }; //replace/swap this.$('ul.fields .listitem').map(function (index, el) { dndManager.addDropTarget(el, { accepts: fAccept, onDragEnter: fOnDragEnter, onDragLeave: onDragLeave, onDrop: fOnDrop, priority: function priority() { var $widget = _this5.$el.closest('.liveWidget'); return $widget.hasClass('widgetExpanded') ? 1 : 0; } }); }); //Add after this.$('ul.fields .dropafter').map(function (index, el) { dndManager.addDropTarget(el, { accepts: fAccept, onDragEnter: fOnDragEnter, onDragLeave: onDragLeave, onDrop: fOnDrop, priority: function priority() { var $widget = _this5.$el.closest('.liveWidget'); return $widget.hasClass('widgetExpanded') ? 1 : 0; } }); }); //add first this.$('ul.fields .slot').map(function (index, el) { dndManager.addDropTarget(el, { accepts: fAccept, onDragEnter: fOnDragEnter, onDragLeave: onDragLeave, onDrop: fOnDrop, priority: function priority() { var $widget = _this5.$el.closest('.liveWidget'); return $widget.hasClass('widgetExpanded') ? 1 : 0; } }); }); //Filter stuff this.$('ul.fields .localFilter').map(function (index, el) { dndManager.addDropTarget(el, { accepts: fAcceptContext, onDragEnter: fOnDragEnter, onDragLeave: onDragLeave, onDrop: fOnDropContext, priority: 2 //When filter container and slots overlaps (slots list has scrollbars), //filter container takes priority to accept the dropped in items }); }); }, _needToReRender: function _needToReRender(slots, filters, graphic) { // Optimization - Build a key based on what is shown as data slots. var sRenderingKey = this._buildRenderingKey(slots, filters, graphic); if (this._lastRender !== sRenderingKey) { return true; } this._lastRender = sRenderingKey; return false; }, /** * Returns a boolean to indicate whether the dragObject position is outside focus mode UI * * @param {object} dragObject - the drag object object context * * @return {boolean} true if the drag position is outside of focus mode UI else return false */ _isOutsideFocusModeBoundary: function _isOutsideFocusModeBoundary(dragObject) { dragObject = dragObject || {}; if (dragObject.position) { var position = dragObject.position; if (position.x < this.focusModeBoundingClientRect.left || position.x > this.focusModeBoundingClientRect.right || position.y < this.focusModeBoundingClientRect.top || position.y > this.focusModeBoundingClientRect.bottom) { return true; } } return false; }, _buildRenderingKey: function _buildRenderingKey(slots, filters, graphic) { var visId = this._getVisAPI().getDefinition().id; // We use that key to prevent rendering the same UI multiple times because Rave can fire many rendering events. var sRenderingKey = _.reduce(slots, function (key, s) { s._unavailable = s.unavailable ? 1 : 0; var slotsKey = toStringKey(key, s, ['id', 'icon', 'filterString', '_unavailable']); return _.reduce(s.mappings, function (key, m) { return toStringKey(key, m, ['columnId', 'mappingId']); }, slotsKey); }, visId); sRenderingKey = _.reduce(filters, function (key, f) { return toStringKey(key, f, ['id', 'filterString']); }, sRenderingKey); sRenderingKey = _.reduce(graphic, function (key, v) { return toStringKey(key, v, ['content', 'fillColor', 'borderColor', 'currentScaleOption']); }, sRenderingKey); return sRenderingKey; }, /** * Brings up the UI to create a new context-type filter (ie: filter on a non-visible item). * @param filterColumnId - the columnId of the filter to edit. * @param node - the node to guide placement of the dialog. * @param forceOpen - true if force open the filter dialog. */ _setContextFilter: function _setContextFilter(metadataColumn, node, forceOpen, transactionToken) { DynamicFileLoader.load(['dashboard-analytics/visualizations/interactions/FilterDropAction']).then(function (modules) { var FilterDropAction = modules[0]; var fAction = this.interactivityController.getActionHelper().getNewFilterActionForContextColumn(FilterDropAction, metadataColumn); if (!forceOpen && metadataColumn.isNamedSet()) { var options = this._convertTransactionTokenToLegacyOptions(transactionToken); this.localFilters.addFilter(fAction.itemContext, { command: 'replace', exclude: false, valueDataItem: metadataColumn.getId(), type: metadataColumn.isProperty() ? 'display' : undefined }); this.localFilters.allFilterModificationComplete(options); } else { var sDialogModule = fAction.getEditorModuleName(); var viewOptions = fAction.getViewOptions(); DynamicFileLoader.load([sDialogModule]).then(function (modules) { var view = new modules[0](viewOptions); var preloadDone = view.preload ? view.preload() : Promise.resolve(); preloadDone.then(this._createContextFilter.bind(this, view, node)).then(function () { view.renderCallBack(this._actionMenuToolbar); }.bind(this)); }.bind(this)); } }.bind(this)); return; }, _createContextFilter: function _createContextFilter(view, element) { var actions = [{ responsive: false, editable: false, changedAction: null, subView: view, type: 'SubView' }]; //If it's from a dropzone open it at the label level var node = $(element).find('.dropfirst'); if (node.length === 0) { //If it's a click on a existing dataItem node = $(element).find('.mappingLabel'); } var toolbarOptions = { textOnly: true, container: $('body'), notCentered: true, popoverClass: 'popover actionToolbarPopover text' }; this._setToolbarForSlot(actions, node, stringResources.get('toolbarActionFilter'), toolbarOptions); }, _setToolbarForSlot: function _setToolbarForSlot(aActions, node, sLabel, toolbarOptions) { var authToolbarOptions = _.extend({}, toolbarOptions, { dataSlotsView: this }); var toolbar = new DataSlotsViewAuthoringToolbar(authToolbarOptions); toolbar.setName(sLabel); toolbar.addItems(aActions); toolbar.setSelectionContext([node]); toolbar.show(); node.addClass('selected'); var target = node[0]; toolbar.on('toolbar:remove', this._onToolBarRemove.bind(this)); //Call set focus here to avoid flashing the selected node toolbar.on('toolbar:show:before', this.setAddFocus.bind(this, { target: target })); this._actionMenuToolbar = toolbar; // Update the actions with the toolbar in case they need to exit during certain steps. aActions.forEach(function (action) { if (action && action.setToolbar) { action.setToolbar(toolbar); } }); }, /** * Brings up the UI to create a new filter for a visible slot. * @param slot - the data slot to filter (ie: 'category' or 'xAxis') * @param node - the node to guide placement of the dialog. */ _setSlotMenuActions: function _setSlotMenuActions(slot, node) { // WACA: TODO filters // _setSlotFilter: function(slot, node) { if (!slot) { return; } var mapIndex = parseInt($(node).attr('map-index')); return this.interactivityController.getActionHelper().getActionsForSlots([slot], mapIndex, node).then(function (actions) { var aToolbarActions = []; _.each(actions, function (action) { aToolbarActions = aToolbarActions.concat(action.getAvailableActions()); }); aToolbarActions.forEach(function (action) { if (action.order === undefined) { action.order = ActionTypes[action.name]; } }); aToolbarActions.sort(function (a, b) { return a.order - b.order; }); node = $(node).find('.menuoverflow'); var toolbarOptions = { textOnly: true, container: $('body'), placement: 'right', notCentered: true, popoverClass: 'popover actionToolbarPopover text', modal: true }; this._setToolbarForSlot(aToolbarActions, node, null, toolbarOptions); }.bind(this)); }, /** * Handler when user clicks on a filter icon to edit a filter for either a visible slot or context entry. * If this button represents a visible slot, a filter will be added on the slot. * If not, a context filter will be added on the column. * Normally, these two are the same but can differ when the user has used an attribute column * in an ordinal slot (where the filter to be added would be a count type filter). */ popupSlotOptions: function popupSlotOptions(e) { if (this._canLaunchAuthoringToolbar(e)) { var $slot = $(this.getTarget(e.target, 'listitem')); var slotId = $slot.attr('data-slot-id'); this.launchedToolbarNodeId = $slot.attr('data-mapping-id'); return this._setSlotMenuActions(this.visualization.getSlots().getSlot(slotId), $slot[0]); } else { this._clearToolbar(); } }, expandCollapseLayers: function expandCollapseLayers(e) { var $layer = $('div.completeLayer.' + e.target.id.replace('.', '\\.')); var $buttonDiv = $layer.find('.layerExpanderButton'); var $svg = $buttonDiv.find('svg'); var label; if ($layer.hasClass('expanded')) { $layer.removeClass('expanded'); $layer.addClass('collapsed'); $layer.attr('aria-expanded', 'false'); this._collapseLayer(e.target.id); label = stringResources.get('evExpand'); } else if ($layer.hasClass('collapsed')) { $layer.removeClass('collapsed'); $layer.addClass('expanded'); $layer.attr('aria-expanded', 'true'); this._expandLayer(e.target.id); label = stringResources.get('evCollapse'); } $buttonDiv.attr('title', label); $svg.attr('title', label); $svg.attr('aria-label', label); }, openContextFilter: function openContextFilter(e) { var $filter = $(this.getTarget(e.target, 'listitem')); var uniqueId = $filter.attr('data-mapping-id'); var columnId = $filter.attr('data-column-id'); var filterId = $filter.attr('data-filter-id'); //Note: data-filter-id is the uniqueId of the FilterEntry. var model = this._getVisAPI(); if (filterId) { var filterEntry = model.localFilters.getFilterEntry({ id: filterId }); if (filterEntry && !model.isFilterEditable(filterEntry)) { //Don't allow click-edit to edit an 'extra filter' (ie: an additional filter created by keep/exclude) return; } } var metadataColumn = model.getMetadataColumn(columnId); metadataColumn.uniqueId = uniqueId; this._setContextFilter(metadataColumn, $filter, true); }, popupFilterOptions: function popupFilterOptions(e) { if (!this._canLaunchAuthoringToolbar(e)) { this._clearToolbar(); return; } var $filter = $(this.getTarget(e.target, 'listitem')); var columnId = $filter.attr('data-column-id'); var model = this._getVisAPI(); var metadataColumn = model.getMetadataColumn(columnId); this.launchedToolbarNodeId = $filter.attr('data-filter-id'); //Note: data-filter-id is the uniqueId of the FilterEntry. if (metadataColumn) { this.interactivityController.getActionHelper().getActionsForLocalFilter(metadataColumn, this.launchedToolbarNodeId).then(function (actions) { var aToolbarActions = []; _.each(actions, function (action) { aToolbarActions = aToolbarActions.concat(action.getAvailableActions()); }); aToolbarActions.sort(function (a, b) { return a.order - b.order; }); var node = $filter.find('.menuoverflow'); var toolbarOptions = { textOnly: true, container: $('body'), placement: 'bottom', notCentered: true, popoverClass: 'popover actionToolbarPopover text' }; this._setToolbarForSlot(aToolbarActions, node, null, toolbarOptions); }.bind(this)); return; } }, /** * Handler when user clicks on local filter box to set focus. */ setAddFocus: function setAddFocus(e) { var focusedItem = this.$('.addFocus'); focusedItem.removeClass('addFocus'); var $target = $(e.target); $target.addClass('addFocus'); }, /** * Returns true if data slot has the addFocus class * addFocus class indicates slot where user wants model data inserted */ isSlotSelected: function isSlotSelected() { var resp = false; var addFocusSlot = this.$el.find('.addFocus'); if (addFocusSlot.length > 0) { resp = true; } return resp; }, /** * Returns true if data slot that has the addFocus class also has class filters * this combination indicated the slot is the Local Filters slot */ isSelectedSlotLocalFilters: function isSelectedSlotLocalFilters() { var resp = false; var addFocusSlot = this.$el.find('.addFocus'); if (addFocusSlot.length > 0 && addFocusSlot.hasClass('filters')) { resp = true; } return resp; }, /** * on scrolling the panel, close the Action menu if it is open, thus preventing it from dangling * */ onScrollSlotsPanel: function onScrollSlotsPanel() { this._clearToolbar(); }, _canLaunchAuthoringToolbar: function _canLaunchAuthoringToolbar(event) { var $listItem = $(this.getTarget(event.target, 'listitem.columnName')); var id = $listItem.attr('data-mapping-id') || $listItem.attr('data-filter-id'); var hasToolBar = this.launchedToolbarNodeId === id; return hasToolBar ? false : true; }, /** * Return a boolean indicating whether is dragging or not * * @param {event} - JQuery event object * * @return {boolen} true if is dragging else return false */ _beginDragAndDrop: function _beginDragAndDrop(event) { if (!this.initialDragAndDropPosition) { return false; } var currentPosition = DomUtils.getEventPos(event); return Math.abs(this.initialDragAndDropPosition.pageX - currentPosition.pageX) >= DND_X_THRESHOLD || Math.abs(this.initialDragAndDropPosition.pageY - currentPosition.pageY) >= DND_Y_THRESHOLD; }, _setIsImplicitClosingTheToolbar: function _setIsImplicitClosingTheToolbar(value) { this.isImplicitClosingTheBoolbar = value; }, _updateMissingFilters: function _updateMissingFilters() { var _this6 = this; this.widget.getUnavailableLocalFilter(); if (this.widget.visModelManager.filterSupport.missingFilters.length === 0) { this.missingFilters.length = 0; } else { this.widget.visModelManager.filterSupport.missingFilters.forEach(function (filterFromWidget) { //prevent duplicate var isMissingExist = _.find(_this6.missingFilters, function (filterColumnId) { return filterFromWidget.columnId === filterColumnId; }); if (!isMissingExist) { _this6.missingFilters.push(filterFromWidget.columnId); } }); } }, _isSharingSlots: function _isSharingSlots() { var slotList = this.slotsApi.getSlotList(); if (slotList && slotList.length > 0) { var sharingSlots = this.slotsApi.getSlotList().filter(function (slotApi) { return slotApi.getDefinition().getDatasetIdList().length > 1; }); return sharingSlots.length > 0; } return false; }, _getDatasets: function _getDatasets(datasets) { // This is used for UI only not the acutal dataset return !this._isSharingSlots() && datasets.length > 0 ? datasets : [{ name: 'data' }]; } }); return DataSlotsView; }); //# sourceMappingURL=DataSlotsView.js.map