define([ 'jsclass/class', './WidgetService', 'app/common/layout/manager', 'app/common/layout/model', 'app/common/datatable/component.datatable', 'Shared/Base/ConfigureGrid', 'Shared/Filter/filter', 'Shared/Filter/PageFilterSortInfo', 'Shared/Base/LoadMask', 'Shared/Extensions/ScopedWorkspace', 'Shared/Util/Widget/GenericHelpers', './WidgetLoader', './Util', 'nunjucks', 'Shared/Extensions/WindowTitleForDate', 'async' ], function ( Class, WidgetService, Manager, Model, Datatable, ConfigureGrid, Filter, PageFilterSortInfo, LoadMask, ScopedWorkspace, GenericHelpers, WidgetLoader, Util, nunjucks, WindowTitleForDate, Async ) { 'use strict'; var DIR = 'Shared/WidgetFoundation/', $CacheMemory = {}; return new Class( { extend: { EXPORT_TYPE: { DETACH: 'DETACH' }, loader: function(settings) { return new WidgetLoader(this, settings); } }, settings: { // default: { features: {} } - workspace and subworkspace, all tabs, main and detach // // workspace: { default: { default: { features: {} } } } - workspace, all tabs, main and detach // workspace: { default: { main: { features: {} } } } - workspace, all tabs, main // workspace: { reports: { default: { features: {} } } } - workspace, reports tab, main and detach // workspace: { reports: { main: { features: {} } } } - workspace, reports tab, main // workspace: { reports: { detach: { features: {} } } } - workspace, reports tab, detach // // subworkspace: { default: { features: {} } } - subworkspace, main and detach // subworkspace: { main: { features: {} } } - subworkspace, main // subworkspace: { detach: { features: {} } } - subworkspace, detach default: { title: 'Abstract Widget Title', features: { export: true, exportType: ['DETACH'], maximize: true, mininize: true, close: true, resize: true, drag: true }, size: { width: 500, height: 400 }, listeners: { // format: eventName: function() {}, or eventName: [function() {}] // widgetInitialized: [], // widgetPassive: function() { console.info('just become passive'); } // available event types: // eventName - widget foundation event (see this.eventsMap) // widget:eventName (see this.frameworkEventsMap) // service:serviceName }, // Methods, which will be passed to this[methodKey] methods: {}, plugins: { // PluginPath: { parameters: 'here' } }, internal: { workspaceChangeWorkaroundTimeout: 50, paths: { service: 'Service', view: ':text:' + DIR + 'View/AbstractWidget.html' // :text:, :require:, :service: or direct url (without html) }, services: { getState: 'AUM/GetWidgetState', saveState: 'AUM/SetWidgetState', getStateNoId: 'User/GetWidgetStateWithoutId', saveStateNoId: 'User/SetWidgetStateWithoutId' } } } }, // Fetched settings actualSettings: {}, // State originalState: {}, state: {}, service: null, eventListeners: null, memory: null, plugins: null, // Widget initialized: false, id: null, // unique id widgetFrameworkPath: null, // framework internal, ie AssetsUnderManagement/AssetsUnderManagement:Assets Under Management widgetPath: null, // ie AssetsUnderManagement/AssetsUnderManagement widgetWorkspaceTab: null, widgetObject: null, widgetBodyContainerId: null, isWidgetDetached: null, widgetArgs: [], workspaceContext: null, spinner: null, // Mapping featuresMap: {}, // WidgetFoundation events eventsMap: { settingsFetched: 'settingsFetched', widgetInitialized: 'widgetInitialized', viewInitialized: 'viewInitialized', stateLoaded: 'stateLoaded', statePersisting: 'statePersisting', exportDataToDetached: 'exportDataToDetached', detachedReceiveData: 'detachedReceiveData' }, // Framework events frameworkEventsMap: { // Features onExportClicked: Model.WIDGET_EXPORT, onSettingsClicked: 'settings', onVisualizationClicked: Model.WIDGET_VISUALIZATION, onFilterClicked: 'filter', onRefreshClicked: 'refresh', onResized: Model.AFTER_RESIZE, onMaximized: Model.AFTER_MAXIMIZE, onCascaded: Model.AFTER_CASCADE, onDragged: Model.WIDGET_DRAG_COMPLETED, onClose: Model.WIDGET_CLOSE, // Detach onDetachedInitializing: Model.EXPORTED_VIEW_READY, onDetachedDataReceived: Model.EXPORTED_VIEW_DATA_EVENT, // Strange one-instance widget strategy events onWidgetPassivate: Model.VIEW_IN_PASSIVE_MODE, onWidgetActivate: Model.VIEW_IN_ACTIVE_MODE, onScopeChanged: ScopedWorkspace.SCOPE_CHANGE_EVENT, onUserPreferenceChanged: 'USER_PREFERENCES_SAVED' }, // Work like localization for export items exportTypesMap: { DETACH: 'Detach' }, workspacesIdMap: { "Home": 1, "Clients": 2, "Worklist": 3, "Markets": 4, "Resources": 5, "Trading": 6, "Services": 8, "Reports": 9, "Fixed Income": 10, "Back Office": 19, "Admin": 20, // "Home": 21, // "Home": 22, "Accounts": 23, "Documents": 24, // "Markets": 25, "Bill Pay": 26, "Credit Card Activity": 27 // "Trading": 28, }, initialize: function (settings, whom, isDetached, args, widgetModulePath) { var self = this; // Merge inherited values in the right way Util.forEach(['settings', 'featuresMap', 'eventsMap', 'exportTypesMap', 'frameworkEventsMap'], function(i, key) { var merged = {}; Util.forEach(self.__eigen__().lookup(key), function(i, value) { merged = Util.Object.merge(merged, value); }); self[key] = merged; }); // Merge settings this.settings = Util.Object.merge(this.settings, settings); this.widgetFrameworkPath = widgetModulePath; this.widgetPath = widgetModulePath.split(':')[0]; this.isWidgetDetached = !!isDetached; this.widgetArgs = args || []; (function (initSettings, nextStep) { // Define some settings manually if (!self.isWidgetDetached) { self.updateWorkspaceContext(); setTimeout(function() { // don't work for clients tab // self.widgetWorkspaceTab = Manager.getWorkspaces()[Manager.getWorkspace()].name; self.widgetWorkspaceTab = $('#workspaceTabsHolder').find('.workspace.active').text(); initSettings(nextStep); }, self.settings.default.internal.workspaceChangeWorkaroundTimeout); } // Receive settings from original widget else { var dummyWindow = new Model.WindowProperties(whom, self.widgetFrameworkPath); var dummyWidget = Manager.createWidget(dummyWindow); Manager.renderWidget(dummyWidget, function(event, postMessage) { if (self.frameworkEventsMap.onDetachedDataReceived == event) { if (postMessage.state) { self.originalState = Util.Object.clone(postMessage.state); self.state = Util.Object.clone(postMessage.state); } self.widgetWorkspaceTab = postMessage.widgetWorkspaceTab || 'Unknown'; self.workspaceContext = postMessage.workspaceContext || {}; (function(next) { if (!self.initialized) initSettings(next); else next(); })(function() { nextStep(function() { self.fireEvent(self.eventsMap.detachedReceiveData, { postMessage: postMessage }); if (postMessage.state) { self.fireEvent(self.eventsMap.stateLoaded, postMessage.state); } }); }); } }, ''); // Prepare html to make it wonderful $('#' + dummyWidget.getId()).hide(); $('body #centerpane').prepend($('
' + '' + 'Exporting...' + '' + '
') ); Manager.subscribeToEvent(self.frameworkEventsMap.onDetachedDataReceived, self.widgetFrameworkPath); Manager.sendToParentWindow(self.widgetFrameworkPath, self.frameworkEventsMap.onDetachedInitializing); } })(function(callback) { // Fetch settings from given hash self.actualSettings = self._fetchActualSettings(self.settings); // Store methods to use it as native Util.forEach(self.actualSettings.methods, function(methodKey, method) { if (self[methodKey]) throw new Error('Cannot override "' + methodKey + '"! Method must have unique name!'); self[methodKey] = method.bind(self); }); self.fireEvent(self.eventsMap.settingsFetched); // Initialize plugins if (!Util.Object.isEmpty(self.actualSettings.plugins)) { self.plugins = []; Async.each(Util.Object.getKeys(self.actualSettings.plugins), function(plugin, cb) { require([plugin], function(Plugin) { self.plugins[plugin] = new Plugin(self, self.actualSettings.plugins[plugin], cb); }); }, callback); } else callback(); }, function (listenersInitialized) { // Go next var windowProperties = new Model.WindowProperties(whom, self.widgetFrameworkPath); // Title if (self.isWorkspace()) windowProperties.setTitle(self.actualSettings.title); else windowProperties.setTitle(self.actualSettings.title + ' for ' + self.workspaceContext.display); // Features var features = []; for (var feature in self.featuresMap) { if (self.featuresMap.hasOwnProperty(feature)) { if (self.actualSettings.features[feature]) features.push(self.featuresMap[feature]); } } if (features.length) windowProperties.showDefaultMenuOptions(features); windowProperties.showDetachOption(self.actualSettings.features.export); // If not detached, then we customize some window properties if (!self.isWidgetDetached) { windowProperties.isMaximizable(self.actualSettings.features.maximize); windowProperties.isMinimizable(self.actualSettings.features.minimize); windowProperties.isClosable(self.actualSettings.features.close); windowProperties.isResizable(self.actualSettings.features.resize); windowProperties.isDraggable(self.actualSettings.features.drag); } // Size if (!self.isWidgetDetached) { windowProperties.setWidth(self.actualSettings.size.width); windowProperties.setHeight(self.actualSettings.size.height); windowProperties.setMinSize(self.actualSettings.size.width, self.actualSettings.size.height); } else { var viewPortDims = viewport(); windowProperties.setWidth(viewPortDims.width); windowProperties.setHeight(viewPortDims.height); } self.widgetObject = Manager.createWidget(windowProperties); // And here we go self.stackInitServices(function() { self.stackInitEventListeners(function() { listenersInitialized && listenersInitialized(); self.stackInitView(function() { if (self.isWidgetDetached) self.getNode().hide(); // Subworkspace title if (self.isSubworkspace()) { self.appendToTitle('for ' + self.workspaceContext.display); } self.showSpinner(); if (self.isWidgetDetached) { $('#loading').remove(); self.getNode().show(); } self.stackInitState(function() { self.hideSpinner(); self.initialized = true; self.fireEvent(self.eventsMap.widgetInitialized); }); }); }); }); }); }, /** * Fetch settings for current workspace/subworkspace from all settings (smart merge) * * @param allSettings * @returns {{}} * @private */ _fetchActualSettings: function(allSettings) { var settings = Util.Object.clone(allSettings), paths = ['default']; if (this.isWorkspace()) { // Defaults paths.push('workspace.default.default'); // With tab var currentTab = this.getCurrentTabName().toLowerCase(); paths.push('workspace.' + currentTab + '.default'); // With widget state (main or detach) paths.push('workspace.default.' + (!this.isDetached() ? 'main' : 'detach')); paths.push('workspace.' + currentTab + '.' + (!this.isDetached() ? 'main' : 'detach')); } else { // Defaults paths.push('subworkspace.default'); // With widget state (main or detach) paths.push('subworkspace.' + (!this.isDetached() ? 'main' : 'detach')); } var fetchedSettings = {}; for (var i = 0; i < paths.length; i++) { fetchedSettings = Util.Object.merge(fetchedSettings, Util.Object.getPath(settings, paths[i], {})); } return fetchedSettings; }, // --- Stack /** * Initialize widget services * @private */ stackInitServices: function(next) { var self = this; require([this._determineServicesPath()], function (Service) { self.service = (new Service).initializeServices(self.getWidgetPath()); // Listen services as events Util.forEach(self.service.servicesConfig, function(serviceName) { self.service.listenService(serviceName, function(data) { if (!self.active) return; self.fireEvent('service:' + serviceName, data); }); }); next(); }); }, /** * Subscribe to events * @private */ stackInitEventListeners: function(next) { var self = this; Util.forEach(this.actualSettings.listeners, function(eventName) { var events = self.actualSettings.listeners[eventName]; if ('function' == typeof events) events = [events]; for (var i = 0; i < events.length; i++) { self.listenEvent(eventName, events[i]); } }); next(); }, stackInitState: function(next) { if (!this.isWidgetDetached) this.loadState(next); else next(); }, // --- Generic getNodeId: function () { return this.widgetObject.getId(); }, getNode: function () { return $('#' + this.getNodeId()); }, getWidgetObject: function() { return this.widgetObject; }, getNodeBodyContainerId: function() { return this.widgetBodyContainerId; }, getNodeBodyContainer: function() { return $('#' + this.widgetBodyContainerId); }, getWidgetFrameworkPath: function() { return this.widgetFrameworkPath; }, getWidgetPath: function () { return this.widgetPath; }, /** * For require.js usage * @param {Boolean} [trailingSlash=true] * @returns {String} */ getWidgetDir: function(trailingSlash) { if (undefined === trailingSlash) trailingSlash = true; return this.widgetPath.replace(/\/[^/]+$/, '') + (trailingSlash ? '/' : ''); }, getSettings: function () { return this.actualSettings; }, getAllSettings: function() { return this.settings; }, isInitialized: function() { return this.initialized; }, // --- Environment isDetached: function() { return this.isWidgetDetached; }, getArgs: function() { return this.widgetArgs; }, getWorkspaceContext: function() { return this.workspaceContext; }, updateWorkspaceContext: function() { if (this.isWidgetDetached) throw new Error('Cannot update workspace context for detached widget'); this.workspaceContext = ScopedWorkspace.getScopeOfCurrentWorkspace(); return this; }, isWorkspace: function() { return this.workspaceContext === ScopedWorkspace.SCOPE_UNDEFINED }, isSubworkspace: function() { return !this.isWorkspace(); }, getCurrentTabName: function() { return this.widgetWorkspaceTab; }, // --- Events listenEvent: function (event, handler) { if (!this.eventListeners) this.eventListeners = {}; var self = this; var eventModificators = { once: function(event, handler) { var extraHandler = function() { // Remove event if (~self.eventListeners[event].indexOf(extraHandler)) { delete self.eventListeners[event][self.eventListeners[event].indexOf(extraHandler)]; } // Call original handler handler.apply(this, arguments); }; return extraHandler; } }; var extraEventMatch = event.match(/:([^:]+)$/); if (extraEventMatch && eventModificators[extraEventMatch[1]]) { // if (!eventModificators[extraEventMatch[1]]) throw new Error('Modificator ' + extraEventMatch[1] + ' is unknown'); // Remove modificator from event event = event.replace(/:([^:]+)$/, ''); handler = eventModificators[extraEventMatch[1]](event, handler); } if (!this.eventListeners[event]) this.eventListeners[event] = []; this.eventListeners[event].push(handler); return this; }, fireEvent: function(event, args) { if (!this.eventListeners) this.eventListeners = {}; // Currently used for widget final instance handlers (settings.listeners) if (this.eventListeners[event]) { for (var i = 0; i < this.eventListeners[event].length; i++) { this.eventListeners[event][i].call(this, args); } } // Internal event methods var methodEventHandler = event.replace(/^[^:]+:/, '') + 'EventHandler'; if (this[methodEventHandler] && 'function' == typeof this[methodEventHandler]) this[methodEventHandler].call(this, args); return this; }, // --- Event handlers onCloseEventHandler: function() { this.saveStateIfChanged(); }, onExportClickedEventHandler: function() { var self = this; require(["Shared/Extensions/Export"], function (_export) { var items = []; Util.forEach(self.actualSettings.features.exportType, function(i, type) { if (self.exportTypesMap[type]) { items.push({ name: self.exportTypesMap[type], callback: self.onExportingEventHandler.bind(self), id: Util.getUniqueId(), type: type }); } }); _export.createExportmenu(self.getNodeId(), self.getNodeBodyContainerId(), items); }); }, onExportingEventHandler: function(item) { var self = this; switch (item.type) { case 'DETACH': Manager.detachWindow(this.widgetFrameworkPath, function(event) { // Export general values if (self.frameworkEventsMap.onDetachedInitializing == event) { var postMessage = { state: self.state, widgetWorkspaceTab: self.widgetWorkspaceTab, workspaceContext: self.workspaceContext }; self.fireEvent(self.eventsMap.exportDataToDetached, { postMessage: postMessage, sendMethod: this.sendMessage.bind(this) }); this.sendMessage(postMessage); } // console.info('export layoutEventHandler: ', arguments, this); }); break; default: console.error('Unknown export type: ' + item.type); } }, onScopeChangedEventHandler: function() { this.updateWorkspaceContext().appendToTitle('for ' + this.workspaceContext.display); }, // --- Services getService: function() { return this.service; }, getServices: function() { return WidgetService; }, callOwnService: function(service, parameters, callback) { this.service.call(this.service.getRealServiceName(service), parameters, callback); return this; }, _determineServicesPath: function () { return this.getWidgetDir() + this.actualSettings.internal.paths.service; }, // --- State getFromState: function (path, def) { return Util.Object.getPath(this.state, path, def); }, setToState: function (path, value) { Util.Object.setPath(this.state, path, value); return this; }, loadState: function (callback) { var requestCallback = function (data) { var state = data || {}; this.state = state; this.originalState = Util.Object.clone(state); this.fireEvent(this.eventsMap.stateLoaded, state); callback(state); }.bind(this); if (this.actualSettings.stateId) { console.warn('stateId is deprecated, but used in "' + this.widgetPath + '" - ' + this.actualSettings.stateId); WidgetService.directRequest(this.actualSettings.internal.services.getState, { id: this.actualSettings.stateId }, 'GET', 'json', function(data) { requestCallback(data && data.Preference ? data.Preference : {}); }); } else { WidgetService.directRequest(this.actualSettings.internal.services.getStateNoId, { widgetPath: this.widgetFrameworkPath, workspaceId: Util.Object.getPath(this.workspacesIdMap, this.getCurrentTabName(), this.workspacesIdMap.Home), isSubworkspace: this.isSubworkspace() }, 'GET', 'json', function(data) { if (data && data.json) data = data.json; requestCallback(data); }, true); } return this; }, isStateChanged: function () { // skip whitespace from comparison return JSON.stringify(this.originalState).replace(/\s+/g, '') != JSON.stringify(this.state).replace(/\s+/g, ''); }, saveState: function (callback) { var self = this; this.fireEvent(this.eventsMap.statePersisting, this.state); var requestCallback = function () { self.originalState = Util.Object.clone(self.state); if (callback) callback(); }.bind(this); if (this.actualSettings.stateId) { console.warn('stateId is deprecated, but used in "' + this.widgetPath + '" - ' + this.actualSettings.stateId); WidgetService.directRequest(this.actualSettings.internal.services.saveState, { id: this.actualSettings.stateId, Preference: this.state }, 'POST', 'json', requestCallback); } else { WidgetService.directRequest(this.actualSettings.internal.services.saveStateNoId, { widgetPath: this.widgetFrameworkPath, workspaceId: Util.Object.getPath(this.workspacesIdMap, this.getCurrentTabName(), this.workspacesIdMap.Home), isSubworkspace: this.isSubworkspace(), json: this.state }, 'POST', 'json', function(data) { if (!data.Response) alert('Error: state wasn`t persisted! { Response: false }'); requestCallback(); }); } return this; }, saveStateIfChanged: function(callback) { if (!this.isStateChanged()) { if (callback) callback(); } else this.saveState(callback); return this; }, // --- View loadView: function (callback) { this.loadHTML(this.actualSettings.internal.paths.view, callback); return this; }, loadHTML: function(loadUrl, callback) { loadUrl = loadUrl.replace(/{WidgetFoundation}\/?/, DIR); // Service loading if (-1 != loadUrl.indexOf(':service:')) { WidgetService.call(loadUrl.replace(':service:'), callback); } // Require else if (-1 != loadUrl.indexOf(':require:')) { require([loadUrl.replace(':require:')], callback); } // Require-text else if (-1 != loadUrl.indexOf(':require-text:')) { require(['Lib/Vendor/RequireJS/text!' + loadUrl.replace(':require-text:', '')], callback); } // Text else if (-1 != loadUrl.indexOf(':text:')) { nunjucks.render(loadUrl.replace(':text:', '') // Relative to widget path .replace(new RegExp('^\./'), this.widgetPath.replace(/[^/]+$/, '') + '/'), function (err, html) { if (err) { console.error(err); } else setTimeout(function() { callback(html); }, 0); }); } // URL request else { WidgetService.directRequest(loadUrl, {}, 'GET', 'html', function (template) { callback(template); }.bind(this)); } return this; }, // --- Spinner getSpinner: function() { if (!this.spinner) { if (this.widgetBodyContainerId) { this.spinner = new LoadMask.LoadMask(this.getNodeBodyContainer().parent().prop('id')); } else { console.warn('Cannot construct spinner for ' + this.widgetPath + '! Node id is unknown yet!'); } } return this.spinner; }, showSpinner: function() { if (!this.spinnerCallCount) this.spinnerCallCount = 0; // Count how much it called to show this.spinnerCallCount++; if (1 == this.spinnerCallCount) { var spinner = this.getSpinner(); if (spinner) spinner.start(); } return this; }, hideSpinner: function() { if (!this.spinnerCallCount) this.spinnerCallCount = 0; // Prevent from going under zero number if (this.spinnerCallCount > 0) this.spinnerCallCount--; if (0 == this.spinnerCallCount) { var spinner = this.getSpinner(); if (spinner) spinner.stop(); } return this; }, // --- Memory cache getFromGlobalCacheOrExecute: function(handler, key, callback) { if ('function' == typeof key) { callback = key; key = undefined; } if (undefined === key) key = Util.md5(handler.toString()); (function(handle) { // Already cached if ($CacheMemory[key]) handle($CacheMemory[key]); // Need to be cached else handler(function(result) { callback($CacheMemory[key] = result); }); })(callback); }, getFromMemory: function(path, def) { if (!this.memory) this.memory = {}; return Util.Object.getPath(this.memory, path, def); }, setToMemory: function(path, value) { if (!this.memory) this.memory = {}; Util.Object.setPath(this.memory, path, value); return this; }, // --- Title & header /** * Set widget title * @param {String} title */ setTitle: function(title) { title = (title + '').trim(); if (title) { Manager.updateWidgetTitle(this.widgetFrameworkPath, title); this.lastWidgetTitle = title; } return this; }, getTitle: function() { return this.lastWidgetTitle; }, /** * Append something to widget title. If empty value passed (or no value), then we restore original title * @param {String} [text=''] * @param {Boolean} [spaceBefore=true] * @returns this */ appendToTitle: function(text, spaceBefore) { if (!spaceBefore) spaceBefore = true; this.setTitle(this.actualSettings.title + (!text ? '' : (!spaceBefore ? text : ' ' + text))); return this; }, setAsOfDate: function(asOfDate) { if (asOfDate) WindowTitleForDate.addDateToTitle(this.getNodeId(), asOfDate, true); else WindowTitleForDate.removeDateFromTitle(this.getNodeId()); this.lastAsOfDate = asOfDate; return this; }, getAsOfDate: function() { return this.lastAsOfDate || ''; } }); });