'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