'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/core-client/js/core-client/ui/core/Events', '../lib/@waca/core-client/js/core-client/utils/Deferred', '../api/Error', 'underscore', 'jquery', '../utils/HtmlXSSUtils', '../DynamicFileLoader', '../api/impl/Widget', '../utils/ContentUtil', './ExpandedController', './FocusView'], function (Events, Deferred, ApiError, _, $, HtmlCleanser, DynamicFileLoader, WidgetAPIImpl, ContentUtil, ExpandedController, FocusView) { /** * A widget will normally size itself, but when it has an error, default dimensions are specified so the error message is readable */ var ERROR_MESSAGE_MIN_DIMENSIONS = { width: 300, height: 300 }; /** * Base Widget * * Base class for Gemini widgets. This class encapsulates the interactions with the dashboard infrastructure. * * Widgets would have these life cycle functions * render : render the initial view * onContainerReady : called when container is ready. container provides a Widget Model object in this call. Widgets * can react to model changes by adding listeners as well as update the model so canvas container gets notified * destroy : cleanup on destruction * */ var WidgetBase = Events.extend({ expanded: false, // constructor init: function init(options) { WidgetBase.inherited('init', this, arguments); this.dashboardApi = options.dashboardApi; var usePreferredSizeConfig = [undefined, true, 'true'].indexOf(this.dashboardApi.getAppConfig('usePreferredSize')) !== -1; var usePreferredSizeModel = options.initialConfigJSON && options.initialConfigJSON.usePreferredSize; this.usePreferredSize = usePreferredSizeModel === false ? false : usePreferredSizeConfig; this.whenContainerIsReady = new Deferred(); this.id = options.id; this.canvas = options.canvas; this.content = options.content; this._stateAPI = this.content.getFeature('state.internal'); this._stateAPI.onChangeError(this.onStateChangeError.bind(this)); this.initialConfigJSON = options.initialConfigJSON; this.el = options.el; this.$el = $(this.el); this.$widgetContainer = $(options.widgetContainer); this.eventRouter = options.eventRouter; this.services = options.services; if (options.registry) { this.properties = options.registry.properties; } var dashboardInternalFeature = this.dashboardApi.getFeature('internal'); if (dashboardInternalFeature) { this.eventGroups = dashboardInternalFeature.getBoardModel().eventGroups; } this.colorsService = this.dashboardApi.getFeature('Colors'); // Can we register the events after render? this.registerEvents(options.eventRouter); this.contributionSpec = options.registry; this.logger = this.dashboardApi.getGlassCoreSvc('.Logger'); this.errorView = options.errorView; this.propertiesUtil = options.propertiesUtil; // Register the widget api as a deprecated content feature // Don't pass the widget iteself otherwise it will be destroyed twice (by the fetureLoader and the widget loader) options.contentFeatureLoader.registerDeprecatedFeature(this.id, 'WidgetAPI.deprecated', { getAPI: this.getAPI.bind(this) }); this._expandModeContainerSelector = '.boardPageView:visible > .pageViewContent > .dashboardFrame > .dashboardFrameCentre'; }, getDashboardApi: function getDashboardApi() { return this.dashboardApi; }, /** * Subclasses can call this method to extend the widget API with additional methods * @deprecated Visualization API should exposed in the getFeature response */ _extendAPI: function _extendAPI(extensions) { var widgetAPI = this.getAPI(); for (var name in extensions) { if (typeof extensions[name] === 'function') { widgetAPI[name] = extensions[name]; } } }, /** * For now, only live widgets support features */ getFeature: function getFeature() /*name*/{ return null; }, /** * For now, only live widgets support features */ setFeatureEnabled: function setFeatureEnabled() /*name, isEnabled*/{}, getId: function getId() { return this.id; }, /* * Return the widget API object */ getAPI: function getAPI() { if (!this.widgetAPI) { this.widgetAPI = new WidgetAPIImpl(this).getAPI(); } return this.widgetAPI; }, /** * Helper to register board events handlers for infrastructure events * Derived Widget classes would override this function to add their handlers * */ registerEvents: function registerEvents() { this.dashboardApi.on('widget:stopMove', this.onStopMove, this); this.dashboardApi.on('widget:startMove', this.onStartMove, this); this.dashboardApi.on('widget:onDetailErrors', this.addErrorDetailsHandler, this); if (this.colorsService) { this.colorsService.on('colorSet:changed', this.onDashboardColorSetChanged, this); } }, unregisterEvents: function unregisterEvents() { this.dashboardApi.off('widget:stopMove', this.onStopMove, this); this.dashboardApi.off('widget:startMove', this.onStartMove, this); this.dashboardApi.off('widget:onDetailErrors', this.addErrorDetailsHandler, this); if (this.colorsService) { this.colorsService.off('colorSet:changed', this.onDashboardColorSetChanged, this); } }, registerEventGroup: function registerEventGroup(transactionId) { transactionId = transactionId || _.uniqueId('_addToDefaultGroup_'); // check if the widget is already assigned to an eventGroup if (this.eventGroups) { var group = this.eventGroups.findGroup(this.id); if (!group || group.getPageId() !== this.getContainerPageId()) { // obtain the default event group of the current page group = this.eventGroups.getDefaultGroup(this.getContainerPageId(), { payloadData: { undoRedoTransactionId: transactionId } }); // assign to the default event group this.eventGroups.addToGroup(group.id, [this.id], { payloadData: { undoRedoTransactionId: transactionId } }); return true; } } return false; }, /** * Helper to register Widget Chrome event handlers * Derived Widget classes would override this function to add their handlers * * @param eventRouter - event router shared between 'widget chrome' and widget */ registerWidgetChromeEvents: function registerWidgetChromeEvents(eventRouter) { //TODO: derived classes override this to setup handlers on view events or to fire requests to view if (eventRouter) { eventRouter.on('widget:onResize', this.resize, this); eventRouter.on('widget:onShow', this.onShow, this); eventRouter.on('widget:onHide', this.onHide, this); eventRouter.on('widget:onMaximize', this.onMaximize, this); eventRouter.on('widget:onRestore', this.onRestore, this); eventRouter.on('widget:onTitleChange', this.onTitleChange, this); eventRouter.on('widgetchrome:selected', this.onChromeSelected, this); eventRouter.on('widgetchrome:deselected', this.onChromeDeselected, this); eventRouter.on('widget:onAuthoringMode', this.onAuthoringMode, this); eventRouter.on('widget:onConsumeMode', this.onConsumeMode, this); eventRouter.on('widget:onEventGroupMode', this.onEventGroupMode, this); eventRouter.on('widget:onEnterContainer', this.onEnterContainer, this); eventRouter.on('widget:onExitContainer', this.onExitContainer, this); eventRouter.on('layout:fillColorChange', this.onPagefillColorChange, this); } }, /** * Helper to unregister Widget Chrome event handlers * Derived Widget classes would override this function to add their handlers * * @param eventRouter - event router shared between 'widget chrome' and widget */ unregisterWidgetChromeEvents: function unregisterWidgetChromeEvents(eventRouter) { //TODO: derived classes override this to setup handlers on view events or to fire requests to view if (eventRouter) { eventRouter.off('widget:onResize', this.resize, this); eventRouter.off('widget:onShow', this.onShow, this); eventRouter.off('widget:onHide', this.onHide, this); eventRouter.off('widget:onMaximize', this.onMaximize, this); eventRouter.off('widget:onRestore', this.onRestore, this); eventRouter.off('widget:onTitleChange', this.onTitleChange, this); eventRouter.off('widgetchrome:selected', this.onChromeSelected, this); eventRouter.off('widgetchrome:deselected', this.onChromeDeselected, this); eventRouter.off('widget:onAuthoringMode', this.onAuthoringMode, this); eventRouter.off('widget:onConsumeMode', this.onConsumeMode, this); eventRouter.off('widget:onEventGroupMode', this.onEventGroupMode, this); eventRouter.off('widget:onEnterContainer', this.onEnterContainer, this); eventRouter.off('widget:onExitContainer', this.onExitContainer, this); eventRouter.off('layout:fillColorChange', this.onPagefillColorChange, this); } }, _registerModelEvents: function _registerModelEvents() { this.model.on('change', this._onModelChange, this); this.model.on('change:fillColor', this.applyFillColor, this); this.model.on('change:borderColor', this.applyBorderColor, this); this._modelEventsRegistered = true; }, _unregisterModelEvents: function _unregisterModelEvents() { if (this._modelEventsRegistered) { this.model.off('change', this._onModelChange, this); this.model.off('change:fillColor', this.applyFillColor, this); this.model.off('change:borderColor', this.applyBorderColor, this); this._modelEventsRegistered = false; } }, onStartMove: function onStartMove() {}, onStopMove: function onStopMove() {}, onPagefillColorChange: function onPagefillColorChange() /*payload*/{ // To be overriden by sub classes }, onDashboardColorSetChanged: function onDashboardColorSetChanged() /*payload*/{ this.colorsService.makeSureColorIsValidInModel({ model: this.model, propertyName: 'fillColor' }); this.colorsService.makeSureColorIsValidInModel({ model: this.model, propertyName: 'borderColor' }); }, getDefaultValue: function getDefaultValue(property) { var propSelected = _.find(this.properties, function (prop) { return prop.id === property; }); if (propSelected) { return propSelected.defaultValue; } return; }, /** * Widget Life cycle handler - gets called when container is ready * Canvas provides WidgetModel obj to widget, so that changes to it can be tracked at the Widget as well as canvas side */ onContainerReady: function onContainerReady(containerContext) { var model = containerContext.model, widgetChromeEventRouter = containerContext.widgetChromeEventRouter, isAuthoringMode = containerContext.isAuthoringMode, _containerContext$add = containerContext.additionalWidgetData, additionalWidgetData = _containerContext$add === undefined ? {} : _containerContext$add, layoutAPI = containerContext.layoutAPI; this.model = model; this.widgetChromeEventRouter = widgetChromeEventRouter; this.isAuthoringMode = isAuthoringMode; this.addPayloadData = additionalWidgetData.addPayloadData; this.layoutAPI = layoutAPI; if (this.model) { this._registerModelEvents(); this.addWhiteListAttrs('fillColor', 'borderColor', 'animationEntrance', 'animationExit'); this.addColorProperties(['fillColor', 'borderColor']); if (this.model.localizedProps && this.model.localizedProps.length) { this.dashboardApi.getFeature('TranslationService').registerView({ view: this, model: this.model }); } } this.registerWidgetChromeEvents(this.widgetChromeEventRouter); this.whenContainerIsReady.resolve(); }, /** * Get layout API * * @return {LayoutAPI} layout API */ getLayoutAPI: function getLayoutAPI() { return this.layoutAPI; }, onAuthoringMode: function onAuthoringMode() { this.isAuthoringMode = true; this.isEventGroupMode = false; }, onConsumeMode: function onConsumeMode() { this.isAuthoringMode = false; this.isEventGroupMode = false; }, onEventGroupMode: function onEventGroupMode() { this.isAuthoringMode = true; this.isEventGroupMode = true; this._setEventGroupOverlayContent(this.eventGroups.findGroup(this.id)); }, getEventGroupId: function getEventGroupId() { return this.eventRouter ? this.eventRouter.channelId : undefined; }, setEventRouter: function setEventRouter(newEventRouter, options) { if (!newEventRouter && this.eventRouter) { // unregister from the current event router this.unregisterEvents(this.eventRouter); this.onRemoveCurrentEventRouter(options); this.unregistered = true; } else if (this.unregistered || newEventRouter.channelId !== this.eventRouter.channelId) { // unregister first and then... if (!this.unregistered) { this.unregisterEvents(this.eventRouter); this.onRemoveCurrentEventRouter(options); } // register with the new event router this.eventRouter = newEventRouter; if (this.eventRouter) { this.registerEvents(this.eventRouter); this.onNewEventRouter(options); this.unregistered = false; } } this._setEventGroupOverlayContent(); }, onRemoveCurrentEventRouter: function onRemoveCurrentEventRouter() /*options*/{ // To be implemented by concrete classes }, onNewEventRouter: function onNewEventRouter() /*options*/{ // To be implemented by concrete classes }, _setEventGroupOverlayContent: function _setEventGroupOverlayContent() { var group = this.eventGroups.findGroup(this.id); var node = this.getWidgetStyleNode(); var overlayContent = node.find('.eventGroupOverlayContent'); if (group && overlayContent) { overlayContent.text(group.getGroupIndex()); } }, /** * Event handler to be overridden by extended classes. * Intended to be used to handle navigation within a widget */ onEnterContainer: function onEnterContainer() { /* meant to be overridden */ }, /** * Event handler to be overridden by extended classes. * Intended to be used to exit the navigation within a widget */ onExitContainer: function onExitContainer() { /* meant to be overridden */ }, /** * Widget Life cycle handler - gets called when board spec gets changed e.g. when undo/redo happens * Derived classes override this function * * @param options The updated model */ _onModelChange: function _onModelChange(options) { // call register properties to refresh the properties UI on undo/redo // We re-register only if we have previously registered. if (options && options.sender === 'UndoRedoController') { this.refreshPropertiesPane(); } }, /** * Widget Life cycle handler - gets called when widget is destroyed * Derived classes override this function */ destroy: function destroy() { this._unregisterModelEvents(); if (this._expanded) { this._expanded.remove(); } this.unregisterEvents(this.eventRouter); this.unregisterWidgetChromeEvents(this.widgetChromeEventRouter); if (this.model) { if (this.model.localizedProps && this.model.localizedProps.length) { this.dashboardApi.getFeature('TranslationService').deregisterView(this.model.id); } this.model.contentReferences = []; } this._expanded = null; this.eventRouter = null; }, /** * Returns property from model * * @param The property name to get */ get: function get(propertyName) { return this.model ? this.model[propertyName] : undefined; }, /** * Sets a hash of attributes on this widget's model and notifies the canvas of the change. * * @param The properties to set on the model * @param Model set options, see Model.js for details */ set: function set(attributes, options) { if (this.model && attributes) { options = options || {}; options = _.defaults(options, { sender: this.id }); this.model.set(attributes, options); } }, /** * Trigger an event through the canvas infrastructure eventing mechanism * * @param eventName The name of the event to trigger * @param event The event */ triggerExternalEvent: function triggerExternalEvent(eventName, event) { if (this.eventRouter) { this.eventRouter.trigger(eventName, event); } }, /** * @param options */ onTitleChange: function onTitleChange() /*options*/{ //handle title update }, /* * Called when properties are changed on the properties pane for this widget. Overridden by derived classes */ onPropertyUpdate: function onPropertyUpdate(propChange) { var prop = {}; prop[propChange.category] = propChange.item; var data = null; if (propChange.transactionId) { data = { undoRedoTransactionId: propChange.transactionId, transactionToken: propChange.transactionToken }; } if (propChange.category === 'fillColor' || propChange.category === 'borderColor') { this.colorsService.prepareForColorModelChange(prop, propChange.category); } this.set(prop, { silent: false, payloadData: data }); }, /** * Notify the layout to update the widget label */ updateDescription: function updateDescription(label) { if (this.widgetChromeEventRouter) { this.widgetChromeEventRouter.trigger('widget:updateDescription', { value: label }); } }, /** * Notify the layout to update the widget aria-label */ updateWidgetArialabel: function updateWidgetArialabel(label) { if (this.widgetChromeEventRouter) { this.widgetChromeEventRouter.trigger('widget:updateWidgetArialabel', { value: label }); } }, /** * Notify the layout to clear the widget aria-label */ clearWidgetArialabel: function clearWidgetArialabel() { if (this.widgetChromeEventRouter) { this.widgetChromeEventRouter.trigger('widget:clearWidgetArialabel'); } }, setLayoutProperties: function setLayoutProperties(properties) { if (this.widgetChromeEventRouter) { this.widgetChromeEventRouter.trigger('widget:setLayoutProperties', properties); } }, /** * @param icon - object - A jquery object referencing an element to add * @param name - string - A string representing the name of the icon * @param location - string|undefined - A string value of 'footer' to add an icon to the widget footer. undefined to keep backwards * compatability for adding icon to header */ addIcon: function addIcon(icon, name) { if (this.widgetChromeEventRouter) { var location = void 0; var $header = this._isMaximized && this._expanded && this._expanded.containerElement && this._expanded.containerElement.find('.widgetHeader'); if ($header && $header.length) { location = $header.find('.widgetIcons'); if (location.length === 0) { location = $header; } } this.widgetChromeEventRouter.trigger('widget:addIcon', { widgetIcon: icon, name: name, location: location }); } }, /** * Update the widget container to reflect the common style properties in the model */ applyCommonProperties: function applyCommonProperties() { if (this.model) { this.applyFillColor({ value: this.model.fillColor }); this.applyBorderColor({ value: this.model.borderColor }); var showTitle = this.model.showTitle; if (!showTitle) { showTitle = this.getDefaultValue('showTitle'); } this.showTitle({ value: showTitle }); } }, showTitle: function showTitle(data) { if (!data.value) { var $parent = this.$el.parent(); if ($parent.hasClass('widget')) { $parent.find('.textArea').addClass('hidden'); } } }, changeTitleType: function changeTitleType(data) { if (data.value === 'smart') { var widgetSpec = this.model.toJSON(); widgetSpec.name = ''; var value = this.dashboardApi.getDashboardCoreSvc('.SmartNamingSvc').getWidgetName(widgetSpec); this.$el.parent().find('.textArea').text(value); } }, /** * Apply the fill color value to the view node * @param data */ applyFillColor: function applyFillColor(data) { this.applyColor(data, 'fill'); }, /** * Apply the border color value to the view node * @param data */ applyBorderColor: function applyBorderColor(data) { this.applyColor(data, 'border'); }, /** * Apply color (border/fill) value to the view node * @param data */ applyColor: function applyColor(data, type) { var $node = this.getWidgetStyleNode(); $node.each(function (index, node) { // We set the classname this way so that it works for svg elements. JQuery add/removeClass does not work with svg elements. // clear previous fill color var re = new RegExp('\\s*\\b' + type + '-[^\\s]*\\b', 'g'); var className = node.getAttribute('class') || ''; className = className.replace(re, ''); if (data.value) { className += ' ' + this.getThemeColorClassName(data.value, type); } node.setAttribute('class', className); }.bind(this)); }, getWidgetStyleNode: function getWidgetStyleNode() { return this.$el.closest('.widget'); }, getThemeColorClassName: function getThemeColorClassName(color, type) { return type + '-' + color; }, resize: function resize() { // handle resize update }, onShow: function onShow() {}, onHide: function onHide() {}, /** * To be overridden by extended classes. * Intended to be used to re-render a widget when it is revealed */ reveal: function reveal() {}, onChromeSelected: function onChromeSelected() { //update state info this.chromeSelected = true; this.triggerExternalEvent('widget:selected', { sender: this.model.id, payloadData: this }); }, onChromeDeselected: function onChromeDeselected() { //update state info this.chromeSelected = false; this.triggerExternalEvent('widget:deselected', { sender: this.model.id }); this.triggerExternalEvent('properties:deregister', { sender: this.model.id }); //TODO do we need sender ? }, showWarning: function showWarning(msg, params) { this.showError(msg, params, 'warning'); }, _updateErrorContainer: function _updateErrorContainer() /*msg, errorContainer*/{}, _updateErrorType: function _updateErrorType(msg, errorType) { return errorType ? errorType : 'error'; }, /** * Check whether the widget is displaying an error * * @return {boolean} TRUE is widget has error and FALSE if not */ hasError: function hasError() { //TODO: Add a property on the widget model to expose the error state instead of //using the DOM. return this.$el.has('.errorContainer').length > 0; }, /** * Check whether the widget has warning * * @return {boolean} TRUE is widget has warning and FALSE if not */ hasWarning: function hasWarning() { //TODO: Add a property on the widget model to expose the warning state instead of //using the DOM. return this.$el.has('.warningContainer').length > 0; }, getError: function getError() { return this.errorMessage; }, onStateChangeError: function onStateChangeError(error) { if (error) { this.renderError(error.getMessage(), error.getParams(), error.getType()); } else { this._clearError(); } }, renderError: function renderError(msg, params, type) { // Always keep track of the last query response that caused an error if (params && params.errorInfo) { this._lastErrorInfo = params.errorInfo; } type = this._updateErrorType(msg, type); var hasError = this.hasError(); if (this.el) { var $errorContainer = this.errorView.renderContainer({ id: this.id + 'Title', type: type, msg: { str: msg, params: params } }); this._updateErrorContainer(msg, $errorContainer); this.$el.find('.errorContainer').remove(); // Only cause the widget to resize on the first error we show or it'll keep shrinking on every subsequent error if (this.widgetChromeEventRouter && !hasError) { var errorContainerStyle = { 'min-height': Math.max(ERROR_MESSAGE_MIN_DIMENSIONS.height, this.$el.innerHeight()), 'min-width': ERROR_MESSAGE_MIN_DIMENSIONS.width }; this.widgetChromeEventRouter.trigger('widget:showError', errorContainerStyle, this); } // Replace the text in helper node for Jaws reading. var msgText = this.errorView.makeNlsMessage({ str: msg, params: params }); this.errorMessage = msgText; this.updateWidgetArialabel(msgText); this.$el.append($errorContainer); this.addErrorDetailsHandler(); if (this.renderComplete) { //If a renderComplete function is available, call it as we have rendered what we can and anything waiting on this widget to render shouldn't be blocked. this.renderComplete(); } } }, showError: function showError(msg, params, type) { var error = new ApiError({ 'msg': msg, 'params': params }, { 'type': type }); this._stateAPI.setError(error); }, /** * Add onClick handler to the error div if details are enabled and we have information about an http request that failed */ addErrorDetailsHandler: function addErrorDetailsHandler() { if (window.dashboardErrorDetailsEnabled === true && this._lastErrorInfo && this.$el.has('.errorContainer').length > 0) { this.$el.find('.dashboardMessageBox').onClick(this._showErrorDetails.bind(this)); } }, _showErrorDetails: function _showErrorDetails() { if (this._lastErrorInfo) { return DynamicFileLoader.load(['ui/dialogs/MessageBox']).then(function (Modules) { var MessageBox = Modules[0]; var detailsObj = { httpResponse: { status: this._lastErrorInfo.status, details: this._lastErrorInfo.responseJSON ? this._lastErrorInfo.responseJSON : {} }, httpRequest: { querySpec: this._lastErrorInfo.querySpec ? this._lastErrorInfo.querySpec : {} }, widgetSpec: this.model }; var msgBox = new MessageBox('selectableInfo', 'Error details', JSON.stringify(detailsObj, null, 4)); msgBox.open(); }.bind(this)); } return Promise.resolve(); }, clearError: function clearError() { this._stateAPI.clearError(); }, /** * Clears inline error message */ _clearError: function _clearError() { this._lastQueryErrorInfo = null; this.errorMessage = null; if (this.hasError()) { if (this.widgetChromeEventRouter) { var payload = {}; this.widgetChromeEventRouter.trigger('widget:clearError', payload); } this.$el.find('.errorContainer').remove(); } }, clearMoreDataIndicator: function clearMoreDataIndicator() { if (this.widgetChromeEventRouter) { this.widgetChromeEventRouter.trigger('widget:clearMoreDataIndicator'); } }, addWhiteListAttrs: function addWhiteListAttrs() { var args = Array.prototype.slice.call(arguments, 0); if (this.model && args.length > 0) { if (this.model.whitelistAttrs) { this.model.whitelistAttrs = _.uniq(this.model.whitelistAttrs.concat(args)); } else { this.model.whitelistAttrs = args; } } }, addColorProperties: function addColorProperties(colorProperties) { if (this.model) { if (this.model.colorProperties) { this.model.colorProperties = this.model.colorProperties.concat(colorProperties); } else { this.model.colorProperties = colorProperties; } } }, addContentReferences: function addContentReferences(references) { if (this.model) { this.model.contentReferences = this.model.contentReferences.concat(references); } }, removeWhiteListAttrs: function removeWhiteListAttrs() { if (this.model && this.model.whitelistAttrs) { var args = Array.prototype.slice.call(arguments, 0); this.model.whitelistAttrs = _.difference(this.model.whitelistAttrs, args); } }, //when we 'expand' a widget, we still want to allow access to data strip and properties panels //so, a blocker is placed behind the widget over the canvas container //this selector identifies the container - should probably use a better/more decoupled way getExpandStartingPosition: function getExpandStartingPosition() { //when we 'expand' a widget, there is animation to zoom it to the bigger size (and back) //coordinates for the animation use screen coordinates, while the widget being zoomed uses relative position within the //chrome and also within the preview div //if we use the offset coordinates of this.$el, the starting position will get offset a bit //hence this adjustment to make it line up properly var screenVsRelativeOffset = $(this._expandModeContainerSelector).offset().top; var bounds = this.$el.offset(); bounds.top -= screenVsRelativeOffset; bounds.width = this.$el.width(); bounds.height = this.$el.height(); screenVsRelativeOffset = $(this._expandModeContainerSelector).offset().left; bounds.left -= screenVsRelativeOffset; return bounds; }, getExpandViewContent: function getExpandViewContent() { return $(''); }, isWidgetMaximized: function isWidgetMaximized() { return this._isMaximized === true; }, // TODO: Focus mode needs to be a feature onMaximize: function onMaximize() { if (this._isMaximized || !this.isMaximizeSupported) { return; } this._isMaximized = true; this.triggerExternalEvent('widget:maximize', { id: this.id }); if (this._expanded) { this._expanded.remove(); } //save this before getExpandStartingPosition() gets called this._restoreToParent = this.$el.parent(); this._expanded = new FocusView({ owner: this, content: this.content, data: this.el, containerElement: $(this._expandModeContainerSelector), launchPoint: this.getWidgetStyleNode()[0], dashboardState: this.dashboardApi.getFeature('DashboardState') }); return this._expanded.render(); }, toggleExpanded: function toggleExpanded(toggle) { if (toggle === undefined) { toggle = !this.expanded; } if (toggle !== this.expanded) { this.expanded = toggle; return this._getExpandedController().toggle(toggle); } return Promise.resolve(); }, _getExpandedController: function _getExpandedController() { if (!this._expandedController) { this._expandedController = new ExpandedController({ widget: this }); } return this._expandedController; }, whenRenderComplete: function whenRenderComplete() { this._stateAPI.setStatus(this._stateAPI.STATUS.RENDERED); // The base implementation does not have a render. Once constructed, we assume that it is rendered. return Promise.resolve(); }, /** * This method is called when restoring a widget from maximized back to its parent container. * It returns a promise when its parent exist and undefined otherwise * @return Promise or undefined */ onRestore: function onRestore() { this._isMaximized = false; this.triggerExternalEvent('widget:restore', { id: this.id }); if (this._restoreToParent) { return this.getVisBounds().then(function (bounds) { //set dimensions so this.resize() works (when expanded, this.$el would have been resized to a bigger size) this.$el.css({ width: bounds.width, height: bounds.height }); this._restoreToParent.append(this.$el); this._restoreToParent = null; this.applyCommonProperties(); this.resize(); }.bind(this)); } }, /* * Returns an array of toolbar items to be displayed. Overridden by derived classes */ getContextToolbarItems: function getContextToolbarItems() { return []; }, /** * returns containerPageId that the widget belongs to */ getContainerPageId: function getContainerPageId() { var type = this.dashboardApi.getAppConfig('pageContainerType'); var pageContent = ContentUtil.getPageContent(this.content, type); if (pageContent) { return pageContent.getId(); } return null; }, /** * Reset the widget size to the preferred size. * */ resizeToPreferredSize: function resizeToPreferredSize() { // to be implemented by concrete widgets }, /** * Trigger the setPreferred size event to notify listeners (e.g the widget layout view) * of the preferred size. * * @param preferredSize This is the visualization's preferred size gotten from its definition * @param options Object that contains other information for the layout view such as transaction id */ setPreferredSize: function setPreferredSize(preferredSize, options) { if (!this.usePreferredSize) { return; } var payload = { preferredSize: preferredSize, options: options }; if (this.widgetChromeEventRouter) { this.widgetChromeEventRouter.trigger('widget:setPreferredSize', payload, this); } }, /** * Get the appropriate visualization size from the layout view. This is an asynchronous function. * @returns {Promise} A Deferred's promise object that gets resolved with the vis bounds/size */ getVisBounds: function getVisBounds() { var dfd = new Deferred(); // The vis bounds depends on whether data widget is been rendered in focus mode, // intent view or otherwise. if (!this.widgetChromeEventRouter || this._isMaximized && this._expanded) { dfd.resolve({ top: 0, left: 0, width: this.$el.innerWidth(), height: this.$el.innerHeight() }); } else if (this._isIntentViewMode()) { // Widget is being rendered in intent results dialog var nPreviewContainer = this.$el.parents('.intent-results-preview'); dfd.resolve({ top: 0, left: 0, width: nPreviewContainer.innerWidth(), height: nPreviewContainer.innerHeight() }); } else { // TODO: never pass a deferred/promise through an event! //trigger event to layoutview and attach oDeffered object as payload this.widgetChromeEventRouter.trigger('widget:getSize', { deferred: dfd }, this); } return dfd.promise; }, /** * Determine if the widget is been rendered in an intent view dialog */ _isIntentViewMode: function _isIntentViewMode() { //TODO (STORY 53043) This is a hacky approach. Needs rework. return this.$el.parents('.intent-results-preview').length > 0; }, /** * Get the type of of the widget * * @return {string} type of the widget */ getType: function getType() { return this.model.type; }, _filterActiveAutoBinProperty: function _filterActiveAutoBinProperty(properties) { var _this = this; return _.filter(properties, function (property) { if (['autoBin.toggle', 'autoBin.count'].includes(property.id)) { return _this.visModelManager.getDefinition().binningConfig && _this.visModelManager.getDefinition().binningConfig.auto === true; } return true; }); }, onPropertyChange: function onPropertyChange(propertyName, value) { this.onPropertyUpdate(this.propertiesUtil.buildPropChangePayload(propertyName, value)); }, refreshPropertiesPane: function refreshPropertiesPane(options) { if (this.model) { this.triggerExternalEvent('properties:refreshPane', _.extend({ 'sender': this.model.id }, options)); } }, isValidHtmlContent: function isValidHtmlContent(widgetContent) { return HtmlCleanser.isValidHtmlContent(widgetContent); }, /** * check if the value of a property matched expected * @param {String} propertyName * @param {object} expectedValue * @return {boolean} true if it matches */ doesVisPropertyMatchExpected: function doesVisPropertyMatchExpected(propertyName, expectedValue) { if (this._currVis && this._currVis.doesVisPropertyMatchExpected) { return this._currVis.doesVisPropertyMatchExpected(propertyName, expectedValue); } }, /** * check if contextual grid is supported for this widget * to be overriden by children class * @return {Promise} promise resolves with true if enabled */ isContextualGridEnabled: function isContextualGridEnabled() { return Promise.resolve(false); }, _invokeLifeCycleHandlers: function _invokeLifeCycleHandlers(name, payload) { var _this2 = this; return this.dashboardApi.getDashboardSvc('.LifeCycleManager').then(function (LifeCycleManager) { return LifeCycleManager.invokeLifeCycleHandlers(name, payload); }).catch(function (e) { _this2.logger.error(e); }); }, /** * Render widget * * @return {Promise} promise resolved with current $el */ render: function render() { this._stateAPI.setStatus(this._stateAPI.STATUS.RENDERING); return Promise.resolve(this.$el); } }); return WidgetBase; }); //# sourceMappingURL=WidgetBase.js.map