| /* |
| * Copyright (c) 2014-2018 BSI Business Systems Integration AG. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * BSI Business Systems Integration AG - initial API and implementation |
| */ |
| import {App, arrays, defaultValues, EventSupport, objects, PropertyChangeEventFilter, RemoteEvent, scout, strings, Widget, WidgetEventTypeFilter} from '../index'; |
| import * as $ from 'jquery'; |
| |
| /** |
| * A model adapter is the connector with the server, it takes the events sent from the server and calls the corresponding methods on the widget. |
| * It also sends events to the server whenever an action happens on the widget. |
| */ |
| export default class ModelAdapter { |
| constructor() { |
| this.initialized = false; |
| this.attached = false; |
| this.destroyed = false; |
| this.widget = null; |
| this._enabledBeforeOffline = true; |
| |
| /** |
| * Widget properties which should be sent to server on property change. |
| */ |
| this._remoteProperties = []; |
| this._widgetListener = null; |
| |
| this._propertyChangeEventFilter = new PropertyChangeEventFilter(); |
| this._widgetEventTypeFilter = new WidgetEventTypeFilter(); |
| this.events = new EventSupport(); |
| } |
| |
| init(model) { |
| this._init(model); |
| this.initialized = true; |
| } |
| |
| /** |
| * @param model expects a plain-object with properties: id, session |
| */ |
| _init(model) { |
| scout.assertParameter('id', model.id); |
| scout.assertParameter('session', model.session); |
| $.extend(this, model); |
| this.session.registerModelAdapter(this); |
| } |
| |
| destroy() { |
| this._detachWidget(); |
| this.widget.destroy(); |
| this.widget = null; |
| this.session.unregisterModelAdapter(this); |
| this.destroyed = true; |
| } |
| |
| createWidget(adapterData, parent) { |
| var model = this._initModel(adapterData, parent); |
| this.widget = this._createWidget(model); |
| this._attachWidget(); |
| this._postCreateWidget(); |
| return this.widget; |
| } |
| |
| /** |
| * Override this method to do something right after the widget has been created and has been |
| * attached to the remote adapter. The default impl. does nothing. |
| */ |
| _postCreateWidget() { |
| // NOP |
| } |
| |
| _initModel(model, parent) { |
| // Make a copy to prevent a modification of the given model |
| var deepCopy = this.session.adapterExportEnabled; |
| model = $.extend(deepCopy, {}, model); |
| |
| // Fill in the missing default values |
| defaultValues.applyTo(model); |
| |
| model.parent = parent; |
| model.owner = parent; // Set it explicitly because server sends owner in inspector mode -> ignore the owner sent by server. |
| model.modelAdapter = this; |
| |
| if (model.global) { |
| // Use the root adapter as owner if global is set to true |
| model.owner = this.session.getModelAdapter('1').widget; |
| } |
| |
| this._initProperties(model); |
| |
| return model; |
| } |
| |
| /** |
| * Override this method to call _sync* methods of the ModelAdapter _before_ the widget is created. |
| */ |
| _initProperties(model) { |
| // NOP |
| } |
| |
| /** |
| * @returns A new widget instance. The default impl. uses calls scout.create() with property objectType from given model. |
| */ |
| _createWidget(model) { |
| var widget = scout.create(model); |
| widget._addCloneProperties(['modelClass', 'classId']); |
| return widget; |
| } |
| |
| _attachWidget() { |
| if (this._widgetListener) { |
| return; |
| } |
| this._widgetListener = { |
| func: this._onWidgetEventInternal.bind(this) |
| }; |
| this.widget.addListener(this._widgetListener); |
| this.attached = true; |
| this.events.trigger('attach'); |
| } |
| |
| _detachWidget() { |
| if (!this._widgetListener) { |
| return; |
| } |
| this.widget.removeListener(this._widgetListener); |
| this._widgetListener = null; |
| this.attached = false; |
| this.events.trigger('detach'); |
| } |
| |
| goOffline() { |
| this.widget.visitChildren(function(child) { |
| if (child.modelAdapter) { |
| child.modelAdapter._goOffline(); |
| } |
| }); |
| } |
| |
| _goOffline() { |
| // NOP may be implemented by subclasses |
| } |
| |
| goOnline() { |
| this.widget.visitChildren(function(child) { |
| if (child.modelAdapter) { |
| child.modelAdapter._goOnline(); |
| } |
| }); |
| } |
| |
| _goOnline() { |
| // NOP may be implemented by subclasses |
| } |
| |
| isRemoteProperty(propertyName) { |
| return this._remoteProperties.indexOf(propertyName) > -1; |
| } |
| |
| _addRemoteProperties(properties) { |
| this._addProperties('_remoteProperties', properties); |
| } |
| |
| _removeRemoteProperties(properties) { |
| this._removeProperties('_remoteProperties', properties); |
| } |
| |
| _addProperties(propertyName, properties) { |
| if (Array.isArray(properties)) { |
| this[propertyName] = this[propertyName].concat(properties); |
| } else { |
| this[propertyName].push(properties); |
| } |
| } |
| |
| _removeProperties(propertyName, properties) { |
| properties = arrays.ensure(properties); |
| arrays.removeAll(this[propertyName], properties); |
| } |
| |
| /** |
| * @returns Creates a Event object from the current adapter instance and |
| * sends the event by using the Session#sendEvent() method. Local objects may |
| * set a different remoteHandler to call custom code instead of the Session#sendEvent() |
| * method. |
| * |
| * @param type of event |
| * @param data of event |
| * @param options (optional) options according to the following table: |
| * |
| * Option name Default value Description |
| * ----------------------------------------------------------------------------------------- |
| * delay 0 Delay in milliseconds before the event is sent. |
| * |
| * coalesce undefined Coalesce function added to event-object. |
| * |
| * showBusyIndicator undefined Whether sending the event should block the UI |
| * (true*) after a certain delay. |
| * * The default value 'undefined' means that the |
| * default value ('true') is determined in Session.js. |
| * We don't write it explicitly to the event here |
| * because that would break many Jasmine tests. |
| */ |
| _send(type, data, options) { |
| // Legacy fallback with all options as arguments |
| var opts = {}; |
| if (arguments.length > 2) { |
| if (options !== null && typeof options === 'object') { |
| opts = options; |
| } else { |
| opts.delay = arguments[2]; |
| opts.coalesce = arguments[3]; |
| opts.showBusyIndicator = arguments[4]; |
| } |
| } |
| options = opts; |
| // (End legacy fallback) |
| |
| var event = new RemoteEvent(this.id, type, data); |
| // The following properties will not be sent to the server, see Session._requestToJson(). |
| if (options.coalesce !== undefined) { |
| event.coalesce = options.coalesce; |
| } |
| if (options.showBusyIndicator !== undefined) { |
| event.showBusyIndicator = options.showBusyIndicator; |
| } |
| if (options.newRequest !== undefined) { |
| event.newRequest = options.newRequest; |
| } |
| this.session.sendEvent(event, options.delay); |
| } |
| |
| /** |
| * Sends the given value as property event to the server. |
| */ |
| _sendProperty(propertyName, value) { |
| var data = {}; |
| data[propertyName] = value; |
| this._send('property', data); |
| } |
| |
| /** |
| * Adds a custom filter for events. |
| */ |
| addFilterForWidgetEvent(filter) { |
| this._widgetEventTypeFilter.addFilter(filter); |
| } |
| |
| /** |
| * Adds a filter which only checks the type of the event. |
| */ |
| addFilterForWidgetEventType(eventType) { |
| this._widgetEventTypeFilter.addFilterForEventType(eventType); |
| } |
| |
| /** |
| * Adds a filter which checks the name and value of every property in the given properties array. |
| */ |
| addFilterForProperties(properties) { |
| this._propertyChangeEventFilter.addFilterForProperties(properties); |
| } |
| |
| /** |
| * Adds a filter which only checks the property name and ignores the value. |
| */ |
| addFilterForPropertyName(propertyName) { |
| this._propertyChangeEventFilter.addFilterForPropertyName(propertyName); |
| } |
| |
| _isPropertyChangeEventFiltered(propertyName, value) { |
| if (value instanceof Widget) { |
| // In case of a remote widget property use the id, otherwise it would always return false |
| value = value.id; |
| } |
| return this._propertyChangeEventFilter.filter(propertyName, value); |
| } |
| |
| _isWidgetEventFiltered(event) { |
| return this._widgetEventTypeFilter.filter(event); |
| } |
| |
| resetEventFilters() { |
| this._propertyChangeEventFilter.reset(); |
| this._widgetEventTypeFilter.reset(); |
| } |
| |
| _onWidgetPropertyChange(event) { |
| var propertyName = event.propertyName; |
| var value = event.newValue; |
| |
| // TODO [7.0] cgu This does not work if value will be converted into another object (e.g DateRange.ensure(selectionRange) in Planner.js) |
| // -> either do the check in this._send() or extract ensure into separate method and move the call of addFilterForProperties. |
| // The advantage of the first one would be simpler filter functions (e.g. this.widget._nodesToIds(this.widget.selectedNodes) in Tree.js) |
| if (this._isPropertyChangeEventFiltered(propertyName, value)) { |
| return; |
| } |
| |
| if (this.isRemoteProperty(propertyName)) { |
| value = this._prepareRemoteProperty(propertyName, value); |
| this._callSendProperty(propertyName, value); |
| } |
| } |
| |
| _prepareRemoteProperty(propertyName, value) { |
| if (!value || !this.widget.isWidgetProperty(propertyName)) { |
| return value; |
| } |
| |
| if (!Array.isArray(value)) { |
| return value.modelAdapter.id; |
| } |
| |
| return value.map(function(widget) { |
| return widget.modelAdapter.id; |
| }); |
| } |
| |
| _callSendProperty(propertyName, value) { |
| var sendFuncName = '_send' + strings.toUpperCaseFirstLetter(propertyName); |
| if (this[sendFuncName]) { |
| this[sendFuncName](value); |
| } else { |
| this._sendProperty(propertyName, value); |
| } |
| } |
| |
| _onWidgetDestroy() { |
| this.destroy(); |
| } |
| |
| /** |
| * Do not override this method. Widget event filtering is done here, before _onWidgetEvent is called. |
| */ |
| _onWidgetEventInternal(event) { |
| if (!this._isWidgetEventFiltered(event)) { |
| this._onWidgetEvent(event); |
| } |
| } |
| |
| _onWidgetEvent(event) { |
| if (event.type === 'destroy') { |
| this._onWidgetDestroy(event); |
| } else if (event.type === 'propertyChange') { |
| this._onWidgetPropertyChange(event); |
| } |
| } |
| |
| _syncPropertiesOnPropertyChange(newProperties) { |
| var orderedPropertyNames = this._orderPropertyNamesOnSync(newProperties); |
| orderedPropertyNames.forEach(function(propertyName) { |
| var value = newProperties[propertyName]; |
| var syncFuncName = '_sync' + strings.toUpperCaseFirstLetter(propertyName); |
| if (this[syncFuncName]) { |
| this[syncFuncName](value); |
| } else { |
| this.widget.callSetter(propertyName, value); |
| } |
| }, this); |
| } |
| |
| /** |
| * May be overridden to return a custom order of how the properties will be set. |
| */ |
| _orderPropertyNamesOnSync(newProperties) { |
| return Object.keys(newProperties); |
| } |
| |
| /** |
| * Called by Session.js for every event from the model |
| */ |
| onModelEvent(event) { |
| if (!event) { |
| return; |
| } |
| if (event.type === 'property') { // Special handling for 'property' type |
| this.onModelPropertyChange(event); |
| } else { |
| this.onModelAction(event); |
| } |
| } |
| |
| /** |
| * Processes the JSON event from the server and calls the corresponding setter of the widget for each property. |
| */ |
| onModelPropertyChange(event) { |
| this.addFilterForProperties(event.properties); |
| this._syncPropertiesOnPropertyChange(event.properties); |
| } |
| |
| /** |
| * The default impl. only logs a warning that the event is not supported. |
| */ |
| onModelAction(event) { |
| if (event.type === 'scrollToTop') { |
| this.widget.scrollToTop(); |
| } else { |
| $.log.warn('Model action "' + event.type + '" is not supported by model-adapter ' + this.objectType); |
| } |
| } |
| |
| toString() { |
| return 'ModelAdapter[objectType=' + this.objectType + ' id=' + this.id + ']'; |
| } |
| |
| /** |
| * This method is used to modify adapterData before the data is exported (as used for JSON export). |
| */ |
| exportAdapterData(adapterData) { |
| // use last part of class-name as ID (because that's better than having only a number as ID) |
| var modelClass = adapterData.modelClass; |
| if (modelClass) { |
| var pos = Math.max(0, |
| modelClass.lastIndexOf('$') + 1, |
| modelClass.lastIndexOf('.') + 1); |
| adapterData.id = modelClass.substring(pos); |
| } |
| delete adapterData.owner; |
| delete adapterData.classId; |
| delete adapterData.modelClass; |
| return adapterData; |
| } |
| |
| /** |
| * Static method to modify the prototype of Widget. |
| */ |
| static modifyWidgetPrototype() { |
| if (!App.get().remote) { |
| return; |
| } |
| |
| // _createChild |
| objects.replacePrototypeFunction(Widget, '_createChild', function(model) { |
| if (model instanceof Widget) { |
| return model; |
| } |
| |
| // Remote case |
| // If the widget has a model adapter use getOrCreateWidget of the session to resolve the child widget |
| // The model normally is a String containing the (remote) object ID. |
| // If it is not a string it may be a local model -> use default local case instead |
| if (this.modelAdapter && typeof model === 'string') { |
| return this.session.getOrCreateWidget(model, this); |
| } |
| |
| // Local case (default) |
| return this._createChildOrig(model); |
| }, true); // <-- true = keep original function |
| } |
| } |
| |
| App.addListener('bootstrap', ModelAdapter.modifyWidgetPrototype); |