'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(['jquery', 'underscore', '../VisView', '../../../lib/@waca/core-client/js/core-client/utils/dom-utils', '../../../lib/@waca/dashboard-common/dist/utils/ScrollBarUtil', '../../../lib/@waca/core-client/js/core-client/i18n/FormatResources', '../../../util/DashboardFormatter', '../../../widgets/livewidget/nls/StringResources', '../../../lib/@waca/dashboard-common/dist/utils/EventChainLocal', '../VisEventHandler', './IndentedListEventTarget', '../../../util/EventUtils'], function ($, _, VisView, domUtils, ScrollBarUtil, FormatResources, Formatter, stringResources, EventChainLocal, VisEventHandler, IndentedListEventTarget, EventUtils) {
'use strict';
var IndentedListView = VisView.extend({
DEFAULT_MAX_WIDTH: 320,
DEFAULT_MAX_HEIGHT: 480,
GROUP_MAX_LENGTH: 100,
DELAYLOADING_THROTTLE: 2000,
ATTRIBUTES: {
COLUMN_CLASS: 'col',
ROW_CLASS: 'row'
},
// Maps related to changing text properties
// Map properties to css style strings
PROPERTY_TO_STYLE: {
'font-weight': 'bold',
'font-style': 'italic',
'text-decoration': 'underline'
},
PROPERTY_MAP: {
listValueColor: 'color',
listValueFontSize: 'font-size',
listValueFontFace: 'font-family',
listValueFontBold: 'font-weight',
listValueFontItalic: 'font-style',
listValueFontUnderline: 'text-decoration',
listValueFontAlign: 'text-align'
},
templateString: '
',
events: {
'mousedown .item': 'onDataItemMousedown',
'touchdown .item': 'onDataItemMousedown',
'keydown .item': 'onDataItemKeydown',
'hold .item': 'onDataItemHold',
'mousedown': 'onMouseDown'
},
initialRender: true,
_groupArrays: [],
_groupCounter: 0,
init: function init() {
IndentedListView.inherited('init', this, arguments);
this._cache = []; /*Holds all items after a render*/
this._groupArrays = []; /*Holds all items after a 3*/
this._groupCounter = 0;
this._focusedNode = null;
this.$el.addClass('dataview indentedlist-view indentedlist-view-initial-max-size');
this.initialRender = true;
var $inner = this.$el.find('.indentedlist-content').first();
this.scrollBarUtil = new ScrollBarUtil($inner, // inner
this.$el // outer
);
this.isMobilePannable = true;
this.content = this.content || this.dashboardApi.getCanvas().getContent(this.ownerWidget.getId());
this._scrollHandlerFn = this.scrollHandler.bind(this);
},
scrollHandler: function scrollHandler(evt) {
var $dom = $(evt.target);
if ($dom.scrollTop() + $dom.innerHeight() >= $dom[0].scrollHeight - 20) {
this._addMore();
}
},
onMouseDown: function onMouseDown(event) {
if (this._isOnScrollBar(event)) {
event.stopPropagation();
}
},
getRenderer: function getRenderer() {
return 'dashboard-analytics/visualizations/renderer/indentedlist/IndentedListRenderer';
},
/**
* Loads and creates the control. Returns a promise which is resolved when the control
* is created and ready to render
*/
whenVisControlReady: function whenVisControlReady() {
var _this = this;
var result = void 0;
if (this.visControl) {
result = Promise.resolve(this.visControl);
} else {
result = new Promise(function (resolve, reject) {
try {
require([_this.visModel.getDefinition().control], function (VisControl) {
_this.visControl = new VisControl({
domNode: _this.$el.find('.indentedlist-content')[0]
});
resolve(_this.visControl);
}, reject);
} catch (error) {
reject(error);
}
}).then(function () {
_this.eventHandler = new VisEventHandler({
target: new IndentedListEventTarget({
$el: _this.$el,
visControl: _this.visControl,
visAPI: _this.visModel,
view: _this,
logger: _this.logger
}),
transaction: _this.transactionApi,
logger: _this.logger,
ownerWidget: _this.ownerWidget,
visAPI: _this.visModel,
edgeSelection: true
});
return _this.visControl;
});
}
return result;
},
/*
* Set a list widget's size based on its content in order to set an initial size
*/
_clampDefaultDimensions: function _clampDefaultDimensions() {
//indentedlist-view-initial-max-size is applied when a list widget is first created to help determine a reasonable default size.
//It turns off line wrapping. This is to compensate for a browser behaviour which artificially reduces available space to a list widget when it
//is on a templated page outside the main area (see defect 49563).
//This function determines the best default size of a list widget and hard codes it in.
if (this.$el.hasClass('indentedlist-view-initial-max-size')) {
if (this.$el.width() > this.DEFAULT_MAX_WIDTH) {
//Clamp max default width
this.$el.width(this.DEFAULT_MAX_WIDTH);
} else {
//Set width (to compensate for browser artificially shortening width).
this.$el.width(this.$el.width());
}
//Turn line wrapping back on now that the width is set.
this.$el.removeClass('indentedlist-view-initial-max-size');
if (this.$el.height() > this.DEFAULT_MAX_HEIGHT) {
//Clamp max height
this.$el.height(this.DEFAULT_MAX_HEIGHT);
//Set width (to compensate for vertical scroll bar).
var newWidth = this.$el.width() + this._getScrollbarWidth();
this.$el.width(newWidth);
}
}
},
/*
* Clear the clamping performed in _clampDefaultDimensions.
*/
_clearDefaultDimensionClamp: function _clearDefaultDimensionClamp() {
this.$el.width('');
this.$el.height('');
},
_getScrollbarWidth: function _getScrollbarWidth() {
var $content = this.$el.find('.datawidget');
var scrollbarWidth = $content.width() - $content[0].scrollWidth;
return scrollbarWidth;
},
/**
* @param {Object} renderInfo - renderInfo passed to all render methods from the render sequence.
* @returns a Promise which is resolved when rendering is complete.
*/
render: function render(renderInfo) {
var _this2 = this;
this._cache = [];
this._groupArrays = [];
this._groupCounter = 0;
this._focusedNode = null;
if (!this.isMappingComplete() || this.hasMissingFilters() || this.hasUnavailableMetadataColumns()) {
this.$el.find('.indentedlist-content').empty();
this.resizeToWidget(renderInfo);
this.initialRender = true; // Reset flag to calculate width
this.renderIconView();
return Promise.resolve(this);
}
this.dataItems = renderInfo.useAPI ? renderInfo.data.getResult() : renderInfo.data.getDefaultQueryResult().getQueryResult();
this.onResultsReady(this.dataItems);
this.removeIconView();
this.resizeToWidget(renderInfo);
this.renderSelected();
if (this.initialRender) {
this.initialRender = false;
this.visModel.ownerWidget.whenContainerIsReady.promise.then(function () {
_this2._clampDefaultDimensions();
_this2._clearDefaultDimensionClamp();
});
}
return IndentedListView.inherited('render', this, arguments);
},
remove: function remove() {
this.scrollBarUtil = null;
if (this.eventHandler) {
this.eventHandler.remove();
this.eventHandler = null;
}
if (this._cache) {
this._cache = null;
}
IndentedListView.inherited('remove', this, arguments);
},
getDescription: function getDescription() {
// Append the F12 key instruction to the description
var description = IndentedListView.inherited('getDescription', this, arguments);
return stringResources.get('WidgetLabelWithDescripion', {
label: description,
description: stringResources.get('f12KeyDescription')
});
},
renderIconView: function renderIconView() {
// Remove the size restriction for an indented list's icon view
this.$el.removeClass('indentedlist-view-initial-max-size');
IndentedListView.inherited('renderIconView', this, arguments);
},
/**
* Divide the original array of view HTML markups Strings into multiple arrays
* each array has an maximum length of GROUP_MAX_LENGTH defined in the class
**/
_groupItems: function _groupItems(origArray) {
var lists = _.groupBy(origArray, function (element, index) {
return Math.floor(index / this.GROUP_MAX_LENGTH);
}.bind(this));
return _.toArray(lists); //Added this to convert the returned object to an array.
},
_isEqualLevel: function _isEqualLevel(curTuple, prevTuple) {
var notSame = _.find(curTuple, function (obj, k) {
return prevTuple[k] ? prevTuple[k].value !== obj.value : true;
});
return notSame ? false : true;
},
onResultsReady: function onResultsReady(results) {
var _this3 = this;
var code = [];
var prevRow = null;
var indentationLevel = 0;
var slots = this.content.getFeature('Visualization').getSlots();
var level1Slot = slots.getSlot('level1');
var resultRowSize = results.getRowCount();
var dataItemFormatMap = {};
_.each(level1Slot.getDataItemList(), function (dataItem) {
dataItemFormatMap[dataItem.getId()] = dataItem.getFormat();
});
var _loop = function _loop(rowIndex) {
var row = [];
_.each(level1Slot.getDataItemList(), function (dataItemAPI, colIndex) {
var v = [],
t = {};
/*By current design, indented list currently does not support stacked data items per data slot
*Therefore there will be only one tuple part for one dataItem
*/
var dataItem = results.getValue(rowIndex, colIndex)[0];
var dataItemLabel = dataItem.label;
if (dataItemAPI) {
v.push(Formatter.format(dataItemLabel, dataItemFormatMap[dataItemAPI.getId()]));
t[dataItemAPI.getColumnId()] = dataItem;
}
row.push({
tuple: t,
value: v.join(FormatResources.getListSeparatorSymbol() + ' '),
rowId: rowIndex,
colId: colIndex
});
});
var tuple = {};
var startLevel = 0;
if (prevRow) {
while (startLevel < row.length && _this3._isEqualLevel(row[startLevel].tuple, prevRow[startLevel].tuple)) {
_.extend(tuple, row[startLevel].tuple);
startLevel++;
}
}
if (startLevel < row.length) {
if (prevRow && startLevel !== prevRow.length - 1) {
levels = prevRow.length - startLevel - 1;
for (i = 0; i < levels; i++) {
indentationLevel--;
code.push('');
}
}
for (colIdx = startLevel; colIdx < row.length; colIdx++) {
if (colIdx > 0 && colIdx !== startLevel) {
indentationLevel++;
code.push('');
}
_.extend(tuple, row[colIdx].tuple);
//Store Element Metadata only
code.push({
// ensure the clone, so that the child tuple doesn't overwrite the parent tuple
'tuple': _.clone(tuple),
'colIdx': colIdx,
'rowValue': row[colIdx].value,
'iLevel': indentationLevel,
'rowId': row[colIdx].rowId,
'colId': row[colIdx].colId
});
}
}
prevRow = row;
};
for (var rowIndex = 0; rowIndex < resultRowSize; rowIndex++) {
var levels;
var i;
var colIdx;
_loop(rowIndex);
}
for (var k = 0; k < indentationLevel; k++) {
code.push('
');
}
if (this.el) {
var $listContent = this.$el.find('.indentedlist-content');
var $scrollInner = $listContent.first();
$scrollInner.off('scroll', this._scrollHandlerFn);
if (code.length <= this.DELAYLOADING_THROTTLE) {
this._convertListObjectsToHTML(code);
$listContent.html(code.join(''));
} else {
$scrollInner.on('scroll', this._scrollHandlerFn);
this._cache = code;
this._addMore(true);
}
}
},
/**
* Append more HTML markups to the existing view
* @param {boolean} isNewRender, true, add first group only
**/
_addMore: function _addMore(isNewRender) {
var currentArray = [];
this._groupArrays = this._groupItems(this._cache);
//Add only first group for a rerender
if (isNewRender) {
currentArray = this._groupArrays[0];
} else {
//Add one more group
if (this._groupArrays.length > this._groupCounter) {
var nEndPos = this._groupCounter * this.GROUP_MAX_LENGTH + this._groupArrays[this._groupCounter].length;
currentArray = this._cache.slice(0, nEndPos);
} else {
return;
}
}
this._convertListObjectsToHTML(currentArray);
var nLen = currentArray.length;
var sLastEle = currentArray[nLen - 1];
if (sLastEle !== '') {
var $LastEle = $(sLastEle);
var sDepth = $LastEle.attr('depth');
if (sDepth === '2') {
currentArray.push('');
} else if (sDepth === '1') {
currentArray.push('');
}
} else {
var s2ndLastEle = currentArray[nLen - 2];
if (s2ndLastEle !== '') {
var $2ndLastEle = $(s2ndLastEle);
if ($2ndLastEle.attr('depth') === '2') {
currentArray.push('');
}
}
}
this.$el.find('.indentedlist-content').empty();
this.$el.find('.indentedlist-content').html(currentArray.join(''));
this._groupCounter++;
this.renderSelected();
},
/*@param {Array} objects*/
_convertListObjectsToHTML: function _convertListObjectsToHTML(objects) {
var oCurElement;
var styleString = this._constructStyleString(this.visModel);
for (var i = 0; i < objects.length; i++) {
oCurElement = objects[i];
if (oCurElement && oCurElement.rowValue) {
oCurElement.style = styleString;
objects[i] = this.renderItem(oCurElement);
}
}
},
_constructStyleString: function _constructStyleString(visModel) {
var styleString = 'style="';
var propertyToStyle = this.PROPERTY_TO_STYLE;
_.each(this.PROPERTY_MAP, function (styleName, propertyName) {
var propertyValue = visModel.getPropertyValue(propertyName);
if (propertyValue) {
if (propertyToStyle[styleName]) {
propertyValue = propertyToStyle[styleName];
}
styleString = styleString + styleName + ':' + propertyValue + ';';
}
});
styleString += '"';
return styleString;
},
_getTextAlignmentStyle: function _getTextAlignmentStyle() {
var textAlignment = this.visModel.getPropertyValue('listValueFontAlign');
var textAlignmentStr = '';
if (textAlignment) {
textAlignmentStr = 'style="text-align:' + textAlignment + ';"';
}
return textAlignmentStr;
},
renderItem: function renderItem(item) {
var tuple, level, label, indentationLevel, colId, rowId, styleString;
if (item) {
tuple = item.tuple;
level = item.colIdx;
label = item.rowValue;
indentationLevel = item.iLevel;
colId = item.colId;
rowId = item.rowId;
styleString = item.style;
}
if (!label) {
return;
}
var arrowClass = level > 0 ? 'arrow wfg_shape_arrow_right' : 'no-arrow';
var textAlignment = this._getTextAlignmentStyle();
return ['', '',
// (edge case) Truncate labels to 500 characters. Limit of 500 already used in modelling/DataPreviewView.js
// Browsers can have issues displaying really huge strings (100,000+ characters). See bug 8461.
'', _.escape(label.substr(0, 500)), '', '
'].join('');
},
/**
* Process the rendered items in the list and highlight any selected ones.
* Each entry in the list has a data-tuple attribute.
*/
renderSelected: function renderSelected() /*renderInfo*/{
var selections = this.getController();
if (selections) {
var items = $(this.el).find('.item');
_.each(items, function (item) {
var tuple = null;
$(item).removeClass('selected');
try {
var decodedTuple = decodeURIComponent(item.getAttribute('data-tuple'));
tuple = JSON.parse(decodedTuple);
} catch (err) {
if (this.logger) {
this.logger.warn('Unable to parse the string into a JSON object in the indented list.', decodedTuple);
}
}
if (this.isSubsetOfTupleSelected(tuple, selections)) {
$(item).addClass('selected');
}
}.bind(this));
}
},
renderFocused: function renderFocused(bShow) {
//Clear first
$(this.el).find('.item.focused').removeClass('focused');
if (bShow && ($(this._focusedNode).hasClass('selected') || $(this._focusedNode).is(':focus'))) {
$(this._focusedNode).addClass('focused');
} else {
$(this._focusedNode).blur();
this._focusedNode = null;
}
},
/**
* @returns true if a subset of this tuple is selected.
* A tuple is a list of columnId/value pairs stored as the "data-tuple" attribute
* for row rendered in the list {"5":"Canada", "2":"Backpacks"}
*
* A subset is selected if:
* 1) the last entry in the tuple (in field order) is selected
* 2) preceding entries either have selections that match the tuple column/value
* OR have NO selections for their columns
*/
isSubsetOfTupleSelected: function isSubsetOfTupleSelected(tuple, selections) {
if (!tuple || !selections) {
return false;
}
var lastInTupleSelected = false;
var tupleKeys = _.keys(tuple);
for (var i = 0; i < tupleKeys.length; ++i) {
var columnId = tupleKeys[i];
if (tuple[columnId] !== undefined) {
if (selections.isItemSelected(columnId) && !selections.isValueSelected(columnId, EventUtils.toDeprecatedPayload(tuple[columnId]))) {
return false;
}
lastInTupleSelected = selections.isValueSelected(columnId, EventUtils.toDeprecatedPayload(tuple[columnId]));
}
}
return lastInTupleSelected;
},
/**
* Initiate a select from this widget....called from the click/touch handler.
* @param wasSelected The current node was previously selected
* @param tuple The set of column/value pairs representing this row
* eg: data-tuple="{ '5': {u: 'Canada'}, '3': {u: '2001'} }"
* @param clearFirst Prior to selecting the new value, clear old values.
* NOTE: the wasSelected switch takes precedence over this switch.
* If the user clicks a selected item it should simply remove that item from the selection
* not clear everything.
*/
doSelect: function doSelect(wasSelected, tuple, clearFirst) {
this.getController().select({
itemIds: _.keys(tuple),
tuple: _.values(tuple),
command: wasSelected ? 'remove' : 'update',
slotsToClear: clearFirst && !wasSelected ? this.visualization.getSlots().getMappedSlotList() : [],
edgeSelect: true //IndentedList selects tuples as edgeSelections not dataPoints.
});
},
/**
* Handle Click and Touch events
*/
onDataItemClick: function onDataItemClick(ev) {
// Ignore the click if it was handled by a hold event
if (this._isHandledByHold) {
this._isHandledByHold = false;
return;
}
// do selection - only clear the selection list if not multi-select (ctrl key on windows/linux, command key on Mac)
this._touchOrClickSelection(ev, !(ev.ctrlKey || ev.metaKey));
},
onDataItemHold: function onDataItemHold(ev) {
// do multi-select
this._touchOrClickSelection(ev, false);
this._isHandledByHold = true;
},
onDataItemMousedown: function onDataItemMousedown(ev) {
if (ev.ctrlKey || ev.metaKey) {
var eventChainLocal = new EventChainLocal(ev);
eventChainLocal.setProperty('preventWidgetDeselect', true);
}
},
/**
* Function to get the dom element from the event and perform the selection.
* @param ev Event object
* @param bClearList Boolean flag to clear the previous selection or not
* @return null
* @private
*/
_touchOrClickSelection: function _touchOrClickSelection(ev, bClearList) {
var el = domUtils.getAncestorOfClass(ev.target, 'item');
if (el) {
this._focusedNode = el;
var tuple = JSON.parse(_.unescape(el.getAttribute('data-tuple')));
this.doSelect($(el).hasClass('selected'), tuple, bClearList);
}
},
/**
* Handle the on container entered event
* Sets up keyboard navigation inside the list
*/
onEnterContainer: function onEnterContainer() {
// get list items and through and set tabindex
var oItems = $(this.el).find('.item');
// place focus on the first item
if (oItems.length > 0) {
if (this._focusedNode) {
this._focusedNode.focus();
} else {
oItems[0].focus();
}
}
},
/**
* Handle keyboard events
* @param event - keyboard event object
*/
onDataItemKeydown: function onDataItemKeydown(event) {
// process various keyboard events
switch (event.keyCode) {
case 37:
case 38:
this._moveFocusUp();
break;
case 39:
case 40:
this._moveFocusDown();
break;
case 32:
case 13:
// pass to the click handler for item selection
this.onDataItemClick(event);
// prevent widget move/resize/etc when clicked on item.
event.stopPropagation();
break;
default:
return;
}
event.preventDefault();
},
/**
* Returns the index of list item that has focus
* @return integer
* @private
*/
_getFocusedItemIndex: function _getFocusedItemIndex() {
var oItems = $(this.el).find('.item');
var iFocusIndex = 0;
for (var i = 0; i < oItems.length; i++) {
if ($(oItems[i]).is(':focus')) {
iFocusIndex = i;
break;
}
}
return iFocusIndex;
},
/**
* Sets the focus on the list item that is rendered above the currently focussed item (stops at top)
* @private
*/
_moveFocusUp: function _moveFocusUp() {
// get current index
var iFocusIndex = this._getFocusedItemIndex();
// set focus on item
this._setItemFocus(Math.max(iFocusIndex - 1, 0));
},
/**
* Sets the focus on the list item that is rendered below the currently focussed item (stops at bottom)
* @private
*/
_moveFocusDown: function _moveFocusDown() {
// get current index
var iFocusIndex = this._getFocusedItemIndex();
// set focus on item
this._setItemFocus(Math.min(iFocusIndex + 1, $(this.el).find('.item').length - 1));
},
/**
* Sets the focus on the list item at the given index
* @param iIndex: List item index to set focus
* @private
*/
_setItemFocus: function _setItemFocus(iIndex) {
$(this.el).find('.item')[iIndex].focus();
},
/**
* Indented List view overwrites the default animation in order to avoid it fading in/out when
* applying filters.
*/
//@Override
animate: function animate() {
this.visModel.renderCompleteBeforeAnimation(); // Rendering is complete, no animation yet
},
_isOnScrollBar: function _isOnScrollBar(event) {
return this.scrollBarUtil.isOnScrollBar(event);
}
});
return IndentedListView;
});
//# sourceMappingURL=IndentedListView.js.map