'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2021 * 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/utils/PerfUtils', '../../lib/@waca/core-client/js/core-client/utils/Deferred', 'underscore', 'jquery', '../widgets/PropertiesUtil', '../widgets/error/ErrorView', '../EventChannelRouterHelper', '../layout/LayoutHelper'], function (PerfUtils, Deferred, _, $, PropertiesUtil, ErrorView, ChannelRouterHelper, LayoutHelper) { var ERRORCODE = { 'LOAD': 20, 'UNLOAD': 21, 'WIDGET_ALREADY_LOADED': 22, 'RENDER': 23, 'UNLOADING_A_LOADING_WIDGET': 24 }; var _trackingId = 0; var WidgetLoader = function () { function WidgetLoader(params) { _classCallCheck(this, WidgetLoader); this.ERROR = ERRORCODE; /** * Hash to keep track of the widgets that are in the process of being loaded * * @type {Object} */ this.loadingWidgets = {}; this.failedWidgets = {}; this.eventGroups = null; this.boardModel = null; this._initializeParams(params); //TODO use a namespace prefix/suffix for dom nodes for widgets ?? this.registerEvents(); } WidgetLoader.prototype._initializeParams = function _initializeParams(params) { // Used to keep track of the widget API response promise (i.e. getWidget(id)) this.widgetReadyPromises = {}; // Used to tack request for the widget API before the widget is loaded this.pendingWidgetResolveRequests = {}; this.services = params.services; this.appSettings = params.appSettings; this.contentFeatureLoader = params.contentFeatureLoader; this.dashboardApi = params.dashboardApi; this.canvas = params.canvas; this.eventRouter = params.eventRouter; this.logger = this.dashboardApi.getGlassCoreSvc('.Logger'); this.colorsService = this.dashboardApi.getFeature('Colors'); this.widgetRegistry = params.widgetRegistry; this.loadedWidgets = params.loadedWidgets; if (params.boardModel) { this.eventGroups = params.boardModel.eventGroups; this.boardModel = params.boardModel; this.topLayoutModel = params.boardModel.layout; } this.channelRouterHelper = new ChannelRouterHelper({ eventRouter: this.eventRouter }); }; WidgetLoader.prototype.registerEvents = function registerEvents() { this.eventGroups.on('change:id', this.onEventGroupIdChange, this); this.eventGroups.on('change:widgetIds', this.onWidgetIdsChange, this); if (this.boardModel) { this.boardModel.on('change:eventGroups', this.onEventGroupsChange, this); } }; WidgetLoader.prototype.onEventGroupIdChange = function onEventGroupIdChange(event) { var options = { sender: event.sender, payloadData: { undoRedoTransactionId: event.data.undoRedoTransactionId, transactionToken: event.data.transactionToken } }; var eventRouter = this.channelRouterHelper.getChannelRouter(event.model.id); _.each(event.model.widgetIds, function (widgetId) { if (this.loadedWidgets[widgetId]) { this.loadedWidgets[widgetId].setEventRouter(eventRouter, options); } }.bind(this)); }; WidgetLoader.prototype.onWidgetIdsChange = function onWidgetIdsChange(event) { var options = { sender: event.sender, payloadData: { undoRedoTransactionId: event.data.undoRedoTransactionId, transactionToken: event.data.transactionToken } }; // process the widgets that are added or removed first var changedWidgets, newEventRouter; if (event.prevValue.length > event.value.length) { // widgets are removed from group changedWidgets = _.difference(event.prevValue, event.value); newEventRouter = null; } else { // widgets are added to group changedWidgets = _.difference(event.value, event.prevValue); newEventRouter = this.channelRouterHelper.getChannelRouter(event.model.id); } _.each(changedWidgets, function (widgetId) { if (this.loadedWidgets[widgetId]) { this.loadedWidgets[widgetId].setEventRouter(newEventRouter, options); } }.bind(this)); // process the existing widgets var remainingWidgets = this.dashboardApi.getFeature('EventGroups').getContentIdList(event.model.id); var eventRouter = this.channelRouterHelper.getChannelRouter(event.model.id); _.each(remainingWidgets, function (widgetId) { //check here to see if the widget(s) in the event groups are not deleted if (this.loadedWidgets[widgetId] && this.canvas.getContent(widgetId)) { //TODO: not sure why we have to set event router here. Need to revisit this. this.loadedWidgets[widgetId].setEventRouter(eventRouter, options); } }.bind(this)); }; WidgetLoader.prototype.onEventGroupsChange = function onEventGroupsChange() { var _this = this; var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (event.name === 'add') { var value = event.value || {}; if (value.widgetIds instanceof Array) { var eventRouter = this.channelRouterHelper.getChannelRouter(event.value.id); value.widgetIds.forEach(function (id) { if (_this.loadedWidgets[id]) { _this.loadedWidgets[id].setEventRouter(eventRouter, { silent: true }); } }); } } }; WidgetLoader.prototype.registerWidgetToEventGroup = function registerWidgetToEventGroup(widgetId, transactionId) { var widget = this.getWidget(widgetId); if (widget) { widget.registerEventGroup(transactionId); } }; WidgetLoader.prototype.unregisterWidgetFromEventGroup = function unregisterWidgetFromEventGroup(widgetId, transactionId, options) { this.eventGroups.removeWidgetsFromGroup([widgetId], { payloadData: { undoRedoTransactionId: transactionId, skipUndoRedo: options.data && options.data.skipUndoRedo }, sender: options.sender }); }; WidgetLoader.prototype.loadWidget = function loadWidget(widgetId, widgetSpec, domNode, cbOnLoad, cbOnError, transactionId) { var trackingId = _trackingId; _trackingId++; this.logger.debug('Widget load start (' + trackingId + ')', widgetId, widgetSpec, domNode); return this._invokeLifeCycleHandlers('pre:widget.load', { modelId: widgetId, widgetSpec: widgetSpec }).then(function () { var result; if (!this.loadedWidgets[widgetId] && this.loadingWidgets[widgetId]) { //If loadWidget is called while already loading a widget, //save callbacks for execution when the widget is loaded (and exit). this.loadingWidgets[widgetId].cbOnLoad.push(cbOnLoad); this.loadingWidgets[widgetId].cbOnError.push(cbOnError); } else if (widgetId && widgetSpec) { var widget = this.loadedWidgets[widgetId]; if (!widget && !this.loadingWidgets[widgetId]) { try { result = this._loadWidget(widgetId, widgetSpec, domNode, cbOnLoad, cbOnError, trackingId, transactionId); } catch (e) { this._onError(cbOnError, ERRORCODE.LOAD, widgetId, e, trackingId); result = Promise.reject(e); } } else { this._onError(cbOnError, ERRORCODE.WIDGET_ALREADY_LOADED, widgetId, null, trackingId); result = Promise.reject(new Error('Widget already loaded')); } } return result; }.bind(this)).catch(this.logger.error.bind(this.logger)); }; WidgetLoader.prototype.unLoadWidget = function unLoadWidget(widgetId, cbOnError) { var payload = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (widgetId) { var transactionId = payload.data ? payload.data.undoRedoTransactionId : undefined; this.logger.debug('Unloading widget', widgetId); if (this.loadingWidgets[widgetId]) { // Shouldn't unload a widget until it's loaded fully this._onError(cbOnError, ERRORCODE.UNLOADING_A_LOADING_WIDGET, widgetId); return; } var widget = this.loadedWidgets[widgetId]; this.unregisterWidgetFromEventGroup(widgetId, transactionId, payload); if (widget && widget.destroy) { try { widget.destroy(transactionId); } catch (e) { this._onError(cbOnError, ERRORCODE.UNLOAD, widgetId, e); } finally { this.loadedWidgets[widgetId] = null; this.widgetReadyPromises[widgetId] = null; } } var el = document.getElementById(widgetId); if (el && el.getElementsByClassName('widgetContentWrapper').length) { el.getElementsByClassName('widgetContentWrapper')[0].innerHTML = ''; } } }; /** * Set the state of a widget to 'loading' by adding an entry into the loadingWidgets map by widgetId. * This entry is also responsible for ensuring that, when multiple calls to loadWidget on the same id * occur, callbacks are not lost...they will be executed postLoad. * @param widgetId The id of the widget to set into loading state. */ WidgetLoader.prototype._setLoading = function _setLoading(widgetId) { this.loadingWidgets[widgetId] = { cbOnLoad: [], cbOnError: [] }; }; WidgetLoader.prototype._loadWidget = function _loadWidget(widgetId, widgetSpec, domNode, cbOnLoad, cbOnError, trackingId, transactionId) { cbOnLoad = cbOnLoad || function () {}; if (!this.loadingWidgets[widgetId]) { this._setLoading(widgetId); } var widgetModule = this.widgetRegistry[widgetSpec.type] && this.widgetRegistry[widgetSpec.type].widget; if (!widgetModule) { try { cbOnLoad({ 'id': widgetId, 'widget': null }); } catch (error) { cbOnError(error); } return; } var dfd = new Deferred(); var onLoadWidget = _.partial(this._onLoadWidget.bind(this), widgetId, widgetSpec, widgetModule, domNode, cbOnLoad, cbOnError, trackingId, transactionId); require([widgetModule], function (Widget) { return onLoadWidget(Widget).then(dfd.resolve.bind(dfd), dfd.reject.bind(dfd)); }, function (e) { // If the require fails, errback should be called // TODO: This doesn't get called by DOJO, but it will once we switch to require.js dfd.reject(e); delete this.loadingWidgets[widgetId]; this._onError(cbOnError, ERRORCODE.LOAD, widgetId, e, trackingId); }.bind(this)); return dfd.promise; }; WidgetLoader.prototype._onLoadWidget = function _onLoadWidget(widgetId, widgetSpec, widgetModule, domNode, cbOnLoad, cbOnError, trackingId, transactionId, Widget) { var _this2 = this; var nParent = void 0; var nWidgetContainer = void 0; var nParentWidgetContainer = void 0; var result = void 0; try { if (!this.loadingWidgets[widgetId]) { // This widget isn't in a loading state, we shouldn't continue loading it. // This can happen if the WidgetLoader is destroyed and there are pending loads return Promise.reject(new Error('Widget \'' + widgetId + '\' is not in a loading state. Unable to load.')); } PerfUtils.createPerformanceMark({ 'component': 'dashboard', 'name': 'loadWidget', 'state': 'start' }); this.logger.debug('Widget load finish (' + trackingId + ')'); result = this._invokeLifeCycleHandlers('post:widget.load', { modelId: widgetId, widgetSpec: widgetSpec }).then(function () { return _this2.contentFeatureLoader.whenContentReady(widgetId); }).then(function () { var _this3 = this; // @todo investigate nParentWidgetContainer = nParent = document.getElementById(widgetId) || domNode; if (nParent && nParent.getElementsByClassName('widgetContentWrapper').length) { nParent = nParent.getElementsByClassName('widgetContentWrapper')[0]; } if (!nParent) { this.logger.error('Could not find layout node for widget with id: "' + widgetId + '"'); return; } else { var createContentNode = true; var widgetRegistry = this.widgetRegistry[widgetSpec.type]; if (widgetRegistry) { createContentNode = this.widgetRegistry[widgetSpec.type].createContentNode; } if (createContentNode) { nWidgetContainer = document.createElement('div'); // Apply widget type specific css nWidgetContainer.className = widgetSpec.type + 'WidgetContent widgetContent'; nWidgetContainer.id = widgetId + '_'; $(nParent).append(nWidgetContainer); } else { nWidgetContainer = nParent; } //TODO use namespace for widget dom node? var options = { // TODO - to be removed after refactoring services: this.services, appSettings: this.appSettings, // TODO END - to be removed after refactoring dashboardApi: this.dashboardApi, id: widgetId, canvas: this.canvas, content: this.canvas && this.canvas.getContent(widgetId), initialConfigJSON: widgetSpec, el: nWidgetContainer, widgetContainer: nParentWidgetContainer, registry: widgetRegistry, eventRouter: this.eventRouter, errorView: new ErrorView(), propertiesUtil: PropertiesUtil, contentFeatureLoader: this.contentFeatureLoader }; options = this._addScopedEventInfo(options, widgetSpec, transactionId); options = this._addConfigInfo(options); //The Endor LiveWidget expects the widget model in its constructor rather than waiting for onContainerReady. var widgetModelInstance = this.boardModel && this.boardModel.widgetInstances && this.boardModel.widgetInstances[widgetId]; if (widgetModelInstance) { options.widgetModel = widgetModelInstance; } else { options.widgetSpec = widgetSpec; } return this._createWidget(options, cbOnLoad, cbOnError, Widget, trackingId, transactionId).then(function () { PerfUtils.createPerformanceMark({ 'component': 'dashboard', 'name': 'loadWidget', 'state': 'end' }); }, function (error) { //prevent widgets which failed to load to be added to an event group _this3.failedWidgets[widgetId].setEventRouter(null); throw error; }); } }.bind(this)).catch(this.logger.error.bind(this.logger)); } catch (e) { // Make sure the widget is no longer in loading state even if we get an error delete this.loadingWidgets[widgetId]; if (nParent) { nParent.removeChild(nWidgetContainer); } this._onError(cbOnError, ERRORCODE.LOAD, widgetId, e, trackingId); result = Promise.reject(e); } return result; }; WidgetLoader.prototype._createWidget = function _createWidget(options, cbOnLoad, cbOnError, Widget, trackingId, transactionId) { var _this4 = this; this.logger.debug('Widget create start (' + trackingId + ')'); var widgetId = options.id; var widget = new Widget(options); var result = void 0; var renderWidget = function renderWidget() { _this4.loadedWidgets[widgetId] = widget; return _this4._renderWidget(widget, widgetId, cbOnLoad, cbOnError, trackingId, transactionId); }; var initialize = function initialize() { var stateApi = options.content.getFeature('state.internal'); stateApi.setStatus(stateApi.STATUS.INITIALIZED); }; if (widget.initialize) { var initializeOptions = {}; if (transactionId) { initializeOptions.transactionId = transactionId; } result = widget.initialize(initializeOptions).then(function () { initialize(); return renderWidget(); }, function (error) { //widget failed to load and is added to failedWidgets object, so that it does not get assigned an event group _this4.failedWidgets[widgetId] = widget; if (error && error.uiErrorMessage && widget.showError) { widget.showError(error.uiErrorMessage); } throw error; }); } else { initialize(); result = renderWidget(); } return result; }; WidgetLoader.prototype._renderWidget = function _renderWidget(widget, widgetId, cbOnLoad, cbOnError, trackingId, transactionId) { var _this5 = this; this.logger.debug('Widget render start (' + trackingId + ')'); var result = void 0; // If we have a pending request for the widget, we resolve now. if (this.pendingWidgetResolveRequests[widgetId]) { this.pendingWidgetResolveRequests[widgetId](widget); delete this.pendingWidgetResolveRequests[widgetId]; } //If any callbacks were processed during load, we need to call them now.... var cbOnLoadCallbacks = this.loadingWidgets[widgetId].cbOnLoad; var cbOnErrorCallbacks = this.loadingWidgets[widgetId].cbOnError; delete this.loadingWidgets[widgetId]; try { if (!widget.content || widget.content.getType() !== 'widget.filter') { this.registerWidgetToEventGroup(widgetId, transactionId); } if (widget.render) { if (widget.createVisualizationCompleteDeferred) { result = widget.createVisualizationCompleteDeferred.then(function () { return Promise.resolve() // protection against $ deferred. TODO: please remove after all widgets return a proper promise .then(widget.render.bind(widget)).catch(function (error) { // render may reject when parent is not visible. ignore, but log the error. _this5.logger.error('[WidgetLoader] widget render error', error); }); }); } else { result = Promise.resolve(widget.render()).catch(function (error) { // render may reject when parent is not visible. ignore but log the error. _this5.logger.error('[WidgetLoader] widget render error', error); _.each(cbOnErrorCallbacks, function (cb) { _this5._onError(cb, ERRORCODE.RENDER, widgetId, error, trackingId); }); _this5._onError(cbOnError, ERRORCODE.RENDER, widgetId, error, trackingId); throw error; }); } } else { result = Promise.resolve(); } _.each(cbOnLoadCallbacks, function (cb) { cb({ 'id': widgetId, 'widget': widget }); }); cbOnLoad({ 'id': widgetId, 'widget': widget }); this.logger.debug('Widget render finish (' + trackingId + ')'); } catch (e) { _.each(cbOnErrorCallbacks, function (cb) { _this5._onError(cb, ERRORCODE.RENDER, widgetId, e, trackingId); }); this._onError(cbOnError, ERRORCODE.RENDER, widgetId, e, trackingId); result = Promise.reject(new Error('')); } return result; }; WidgetLoader.prototype._getChannelId = function _getChannelId(containerPageId, widgetId, transactionId) { var eventGroup; if (this.eventGroups) { eventGroup = this.eventGroups.findGroup(widgetId); if (!eventGroup) { eventGroup = this.eventGroups.getDefaultGroup(containerPageId, { payloadData: { undoRedoTransactionId: transactionId } }); } } // use the pageId if eventGroups are not defined return eventGroup ? eventGroup.id : containerPageId; }; /** * If channelRouterHelper is available, add containerPageId and update eventRouter * * @returns updated options */ WidgetLoader.prototype._addScopedEventInfo = function _addScopedEventInfo(options, widgetSpec, transactionId) { if (this.channelRouterHelper) { // Custom widget may declare filter support from it's definition var filterSupport = this.widgetRegistry[widgetSpec.type].filter; // Loading board case, containerPageId is passed in widgetSpec. // If not, it is onDrop case. Use current container page id. options.containerPageId = LayoutHelper.getContainerPageId(this.topLayoutModel, widgetSpec.id); var supported = ['data', 'live']; if (supported.indexOf(widgetSpec.type) !== -1 || filterSupport) { var channelId = this._getChannelId(options.containerPageId, widgetSpec.id, transactionId); options.eventRouter = this.channelRouterHelper.getChannelRouter(channelId); } } return options; }; /** * Updates the options for the widget with any app/view level constraints * @param {object} options */ WidgetLoader.prototype._addConfigInfo = function _addConfigInfo() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; options.focusModeDisabled = !!this.dashboardApi.getConfiguration('focusModeDisabled'); options.interactivitySettings = this.dashboardApi.getConfiguration('interactions'); return options; }; WidgetLoader.prototype.cleanup = function cleanup() { var map = this.loadedWidgets; for (var widgetId in map) { if (map.hasOwnProperty(widgetId)) { var widget = map[widgetId]; if (widget && widget.destroy) { widget.destroy(); this.loadedWidgets[widgetId] = null; this.widgetReadyPromises[widgetId] = null; } } } this.loadingWidgets = {}; if (this.eventGroups) { this.eventGroups.off('change:id', this.onEventGroupIdChange, this); this.eventGroups.off('change:widgetIds', this.onWidgetIdsChange, this); } if (this.boardModel) { this.boardModel.off('change:eventGroups', this.onEventGroupsChange, this); } if (this.channelRouterHelper) { this.channelRouterHelper.destroy(); delete this.topLayoutModel; } }; WidgetLoader.prototype.isLoaded = function isLoaded(id) { return id ? this.loadedWidgets[id] !== null && typeof this.loadedWidgets[id] !== 'undefined' : false; }; WidgetLoader.prototype.isLoading = function isLoading(id) { return this.loadingWidgets[id]; }; WidgetLoader.prototype.getWidget = function getWidget(id) { return id ? this.loadedWidgets[id] : undefined; }; WidgetLoader.prototype.getWidgetAsync = function getWidgetAsync(id) { var _this6 = this; if (!this.widgetReadyPromises[id]) { this.widgetReadyPromises[id] = new Promise(function (resolve) { var widget = void 0; if (_this6.isLoaded(id)) { widget = _this6.getWidget(id); } if (!widget) { _this6.pendingWidgetResolveRequests[id] = resolve; } else { resolve(widget); } }); } return this.widgetReadyPromises[id]; }; /** * Call the named API on all loaded widgets that implement it. * Optionally, return the result of those calls as an array. * @returns An array of results (of arbitrary form) that the caller can examine. * If the api does not return anything, this array will be empty. */ WidgetLoader.prototype.runAPIOnAllWidgets = function runAPIOnAllWidgets(api, pageId) { var resultSet = []; _.each(_.keys(this.loadedWidgets), function (widgetKey) { var loadedWidget = this.loadedWidgets[widgetKey]; if (loadedWidget && loadedWidget[api] && (!pageId || loadedWidget.getContainerPageId() === pageId)) { var result = loadedWidget[api](); if (result) { resultSet.push(result); } } }.bind(this)); return resultSet; }; WidgetLoader.prototype._onError = function _onError(fOnError, errCode, oWidgetInfo, e, trackingId) { var error = { 'errorCode': errCode, 'widget': oWidgetInfo, 'exception': e }; if (fOnError) { setTimeout(function () { fOnError(error); }, 1); } this.logger.error('Error loading widget', trackingId, error); }; WidgetLoader.prototype._invokeLifeCycleHandlers = function _invokeLifeCycleHandlers(name, payload) { return this.dashboardApi.getFeature('.LifeCycleManager').invokeLifeCycleHandlers(name, payload); }; return WidgetLoader; }(); return WidgetLoader; }); //# sourceMappingURL=WidgetLoader.js.map