| /* |
| * 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 { |
| arrays, |
| DeferredGlassPaneTarget, |
| Desktop, |
| Device, |
| Event, |
| EventDelegator, |
| EventSupport, |
| filters, |
| focusUtils, |
| Form, |
| graphics, |
| icons, |
| inspector, |
| KeyStrokeContext, |
| LoadingSupport, |
| LogicalGrid, |
| objects, |
| scout, |
| scrollbars, |
| strings, |
| texts, |
| TreeVisitResult, |
| widgets |
| } from '../index'; |
| import * as $ from 'jquery'; |
| |
| export default class Widget { |
| constructor() { |
| this.session = null; |
| |
| /** |
| * The owner is responsible that its children are destroyed when the owner is being destroyed. |
| */ |
| this.owner = null; |
| /** |
| * The parent is typically the same as the owner. |
| * But the widget may be used by another widget (like a popup), in that case the parent will be changed to the popup but the owner stays the same. |
| * This means the popup is now the temporary parent, when the popup is destroyed its widgets are not because the popup is not the owner. |
| * Example: ViewMenuPopup uses the ViewButtons as menu items. These view buttons are owned by the desktop and must therefore not be destroyed |
| * when the popup closes, otherwise they could not be reused the second time the popup opens. |
| */ |
| this.parent = null; |
| this.children = []; |
| this.initialized = false; |
| |
| /** |
| * The 'rendering' flag is set the true while the _inital_ rendering is performed. |
| * It is used to to something different in a _render* method when the method is |
| * called for the first time. |
| */ |
| this.rendering = false; |
| this.removing = false; |
| this.removalPending = false; |
| |
| /** |
| * The 'rendered' flag is set the true when initial rendering of the widget is completed. |
| */ |
| this.rendered = false; |
| this.attached = false; |
| this.destroyed = false; |
| this.destroying = false; |
| |
| this.enabled = true; |
| /** |
| * The computed enabled state. The difference to the 'enabled' property is that this member |
| * also considers the enabled-states of the parent widgets. |
| */ |
| this.enabledComputed = true; |
| this.inheritAccessibility = true; |
| this.disabledStyle = Widget.DisabledStyle.DEFAULT; |
| this.visible = true; |
| this.focused = false; |
| this.loading = false; |
| this.cssClass = null; |
| this.scrollTop = null; |
| this.scrollLeft = null; |
| |
| this.$container; |
| |
| // If set to true, remove won't remove the element immediately but after the animation has been finished |
| // This expects a css animation which may be triggered by the class 'animate-remove' |
| // If browser does not support css animation, remove will be executed immediately |
| this.animateRemoval = false; |
| this.animateRemovalClass = 'animate-remove'; |
| |
| this._widgetProperties = []; |
| this._cloneProperties = ['visible', 'enabled', 'inheritAccessibility', 'cssClass']; |
| this.eventDelegators = []; |
| this._preserveOnPropertyChangeProperties = []; |
| this._postRenderActions = []; |
| this._focusInListener = this._onFocusIn.bind(this); |
| this._parentDestroyHandler = this._onParentDestroy.bind(this); |
| this._parentRemovingWhileAnimatingHandler = this._onParentRemovingWhileAnimating.bind(this); |
| this._scrollHandler = this._onScroll.bind(this); |
| this.events = this._createEventSupport(); |
| this.events.registerSubTypePredicate('propertyChange', function(event, propertyName) { |
| return event.propertyName === propertyName; |
| }); |
| this.loadingSupport = this._createLoadingSupport(); |
| this.keyStrokeContext = this._createKeyStrokeContext(); |
| // Widgets using LogicalGridLayout may have a grid to calculate the grid data of the children |
| this.logicalGrid; |
| |
| // focus tracking |
| this.trackFocus = false; |
| this._$lastFocusedElement = null; |
| this._storedFocusedWidget = null; |
| |
| this._glassPaneContributions = []; |
| } |
| |
| /** |
| * Enum used to define different styles used when the field is disabled. |
| */ |
| static DisabledStyle = { |
| DEFAULT: 0, |
| READ_ONLY: 1 |
| }; |
| |
| init(model) { |
| var staticModel = this._jsonModel(); |
| if (staticModel) { |
| model = $.extend({}, staticModel, model); |
| } |
| model = model || {}; |
| model = this._prepareModel(model); |
| this._init(model); |
| this._initKeyStrokeContext(); |
| this.recomputeEnabled(); |
| this.initialized = true; |
| this.trigger('init'); |
| } |
| |
| /** |
| * Default implementation simply returns the unmodified model. A Subclass |
| * may override this method to alter the JSON model before the widgets |
| * are created out of the widgetProperties in the model. |
| */ |
| _prepareModel(model) { |
| return model; |
| } |
| |
| /** |
| * @param options |
| * - parent (required): The parent widget |
| * - session (optional): If not specified the session of the parent is used |
| */ |
| _init(model) { |
| if (!model.parent) { |
| throw new Error('Parent expected: ' + this); |
| } |
| this.setOwner(model.owner || model.parent); |
| this.setParent(model.parent); |
| |
| this.session = model.session || this.parent.session; |
| if (!this.session) { |
| throw new Error('Session expected: ' + this); |
| } |
| |
| this._eachProperty(model, function(propertyName, value, isWidgetProperty) { |
| if (value === undefined) { |
| // Don't set the value if it is undefined, compared to null which is allowed explicitly ($.extend works in the same way) |
| return; |
| } |
| if (isWidgetProperty) { |
| value = this._prepareWidgetProperty(propertyName, value); |
| } |
| this._initProperty(propertyName, value); |
| }.bind(this)); |
| |
| this._setCssClass(this.cssClass); |
| this._setLogicalGrid(this.logicalGrid); |
| this._setEnabled(this.enabled); |
| } |
| |
| /** |
| * This function sets the property value. Override this function when you need special init behavior for certain properties. |
| * For instance you could not simply set the property value, but extend an already existing value. |
| */ |
| _initProperty(propertyName, value) { |
| this[propertyName] = value; |
| } |
| |
| /** |
| * Default implementation simply returns undefined. A Subclass |
| * may override this method to load or extend a JSON model with models.getModel or models.extend. |
| */ |
| _jsonModel() { |
| } |
| |
| /** |
| * Creates the widgets using the given models, or returns the widgets if the given models already are widgets. |
| * @returns {Widget[]|Widget}an array of created widgets if models was an array. Or the created widget if models is not an array. |
| */ |
| _createChildren(models) { |
| if (!models) { |
| return null; |
| } |
| |
| if (!Array.isArray(models)) { |
| return this._createChild(models); |
| } |
| |
| var widgets = []; |
| models.forEach(function(model, i) { |
| widgets[i] = this._createChild(model); |
| }, this); |
| return widgets; |
| } |
| |
| /** |
| * Calls {@link scout.create} for the given model, or if model is already a Widget simply returns the widget. |
| * |
| * @param model {Object|Widget} |
| * @returns {Widget} |
| */ |
| _createChild(model) { |
| if (model instanceof Widget) { |
| return model; |
| } |
| if (typeof model === 'string') { |
| // Special case: If only an ID is supplied, try to (locally) resolve the corresponding widget |
| var existingWidget = this.widget(model); |
| if (!existingWidget) { |
| throw new Error('Referenced widget not found: ' + model); |
| } |
| return existingWidget; |
| } |
| model.parent = this; |
| return scout.create(model); |
| } |
| |
| _initKeyStrokeContext() { |
| if (!this.keyStrokeContext) { |
| return; |
| } |
| this.keyStrokeContext.$scopeTarget = function() { |
| return this.$container; |
| }.bind(this); |
| this.keyStrokeContext.$bindTarget = function() { |
| return this.$container; |
| }.bind(this); |
| } |
| |
| destroy() { |
| if (this.destroyed) { |
| // Already destroyed, do nothing |
| return; |
| } |
| this.destroying = true; |
| if (this.rendered && (this.animateRemoval || this._isRemovalPrevented())) { |
| // Do not destroy yet if the removal happens animated |
| // Also don't destroy if the removal is pending to keep the parent / child link until removal finishes |
| this.one('remove', function() { |
| this.destroy(); |
| }.bind(this)); |
| this.remove(); |
| return; |
| } |
| |
| // Destroy children in reverse order |
| this._destroyChildren(this.children.slice().reverse()); |
| this.remove(); |
| this._destroy(); |
| |
| // Disconnect from owner and parent |
| this.owner._removeChild(this); |
| this.owner = null; |
| this.parent._removeChild(this); |
| this.parent.off('destroy', this._parentDestroyHandler); |
| this.parent = null; |
| |
| this.destroying = false; |
| this.destroyed = true; |
| this.trigger('destroy'); |
| } |
| |
| /** |
| * Override this function to do clean-up (like removing listeners) when the widget is destroyed. |
| * The default impl. does nothing. |
| */ |
| _destroy() { |
| // NOP |
| } |
| |
| /** |
| * @param widgets may be an object or array of objects |
| */ |
| _destroyChildren(widgets) { |
| if (!widgets) { |
| return; |
| } |
| |
| widgets = arrays.ensure(widgets); |
| widgets.forEach(function(widget, i) { |
| this._destroyChild(widget); |
| }, this); |
| } |
| |
| _destroyChild(child) { |
| if (child.owner !== this) { |
| return; |
| } |
| child.destroy(); |
| } |
| |
| /** |
| * @param [$parent] The jQuery element which is used as $parent when rendering this widget. |
| * It will be put onto the widget and is therefore accessible as this.$parent in the _render method. |
| * If not specified, the $container of the parent is used. |
| */ |
| render($parent) { |
| $.log.isTraceEnabled() && $.log.trace('Rendering widget: ' + this); |
| if (!this.initialized) { |
| throw new Error('Not initialized: ' + this); |
| } |
| if (this.rendered) { |
| throw new Error('Already rendered: ' + this); |
| } |
| if (this.destroyed) { |
| throw new Error('Widget is destroyed: ' + this); |
| } |
| this.rendering = true; |
| this.$parent = $parent || this.parent.$container; |
| this._render(); |
| this._renderProperties(); |
| this._renderInspectorInfo(); |
| this._linkWithDOM(); |
| this.session.keyStrokeManager.installKeyStrokeContext(this.keyStrokeContext); |
| this.rendering = false; |
| this.rendered = true; |
| this.attached = true; |
| this.trigger('render'); |
| this.restoreFocus(); |
| this._postRender(); |
| } |
| |
| /** |
| * This method creates the UI through DOM manipulation. At this point we should not apply model |
| * properties on the UI, since sub-classes may need to contribute to the DOM first. You must not |
| * apply model values to the UI here, since this is done in the _renderProperties method later. |
| * The default impl. does nothing. |
| */ |
| _render() { |
| // NOP |
| } |
| |
| /** |
| * This method calls the UI setter methods after the _render method has been executed. |
| * Here values of the model are applied to the DOM / UI. |
| */ |
| _renderProperties() { |
| this._renderTrackFocus(); |
| this._renderEnabled(); |
| this._renderVisible(); |
| this._renderFocused(); |
| this._renderCssClass(); |
| this._renderLoading(); |
| this._renderScrollTop(); |
| this._renderScrollLeft(); |
| } |
| |
| /** |
| * Method invoked once rendering completed and 'rendered' flag is set to 'true'.<p> |
| * By default executes every action of this._postRenderActions |
| */ |
| _postRender() { |
| var actions = this._postRenderActions; |
| this._postRenderActions = []; |
| actions.forEach(function(action) { |
| action(); |
| }); |
| } |
| |
| remove() { |
| if (!this.rendered || this._isRemovalPrevented()) { |
| return; |
| } |
| if (this.animateRemoval) { |
| this._removeAnimated(); |
| } else { |
| this._removeInternal(); |
| } |
| } |
| |
| /** |
| * Will be called by {@link #remove()}. If true is returned, the widget won't be removed.<p> |
| * By default it just delegates to {@link #_isRemovalPending}. May be overridden to customize it. |
| */ |
| _isRemovalPrevented() { |
| return this._isRemovalPending(); |
| } |
| |
| /** |
| * Returns true if the removal of this or an ancestor widget is pending. Checking the ancestor is omitted if the parent is being removed. |
| * This may be used to prevent a removal if an ancestor will be removed (e.g by an animation) |
| */ |
| _isRemovalPending() { |
| if (this.removalPending) { |
| return true; |
| } |
| var parent = this.parent; |
| if (!parent || parent.removing || parent.rendering) { |
| // If parent is being removed or rendered, no need to check the ancestors because removing / rendering is already in progress |
| return false; |
| } |
| while (parent) { |
| if (parent.removalPending) { |
| return true; |
| } |
| parent = parent.parent; |
| } |
| return false; |
| } |
| |
| _removeInternal() { |
| if (!this.rendered) { |
| return; |
| } |
| |
| $.log.isTraceEnabled() && $.log.trace('Removing widget: ' + this); |
| this.removing = true; |
| this.removalPending = false; |
| this.trigger('removing'); |
| // transform last focused element into a scout widget |
| if (this.$container) { |
| this.$container.off('focusin', this._focusInListener); |
| } |
| if (this._$lastFocusedElement) { |
| this._storedFocusedWidget = scout.widget(this._$lastFocusedElement); |
| this._$lastFocusedElement = null; |
| } |
| // remove children in reverse order. |
| this.children.slice().reverse() |
| .forEach(function(child) { |
| // Only remove the child if this widget is the current parent (if that is not the case this widget is the owner) |
| if (child.parent === this) { |
| child.remove(); |
| } |
| }, this); |
| |
| if (!this.rendered) { |
| // The widget may have been removed already by one of the above remove() calls (e.g. by a remove listener) |
| // -> don't try to do it again, it might fail |
| return; |
| } |
| this._cleanup(); |
| this._remove(); |
| this.$parent = null; |
| this.rendered = false; |
| this.attached = false; |
| this.removing = false; |
| this.trigger('remove'); |
| } |
| |
| /** |
| * Adds class 'animate-remove' to container which can be used to trigger the animation. |
| * After the animation is executed, the element gets removed using this._removeInternal. |
| */ |
| _removeAnimated() { |
| if (!Device.get().supportsCssAnimation() || !this.$container || this.$container.isDisplayNone()) { |
| // Cannot remove animated, remove regularly |
| this._removeInternal(); |
| return; |
| } |
| |
| // Destroy open popups first, they are not animated |
| this.session.desktop.destroyPopupsFor(this); |
| |
| this.removalPending = true; |
| // Don't execute immediately to make sure nothing interferes with the animation (e.g. layouting) which could make it laggy |
| setTimeout(function() { |
| // check if the container has been removed in the meantime |
| if (!this.rendered) { |
| return; |
| } |
| if (!this.animateRemovalClass) { |
| throw new Error('Missing animate removal class. Cannot remove animated.'); |
| } |
| if (!this.$container.isVisible() || !this.$container.isEveryParentVisible()) { |
| // If element is not visible, animationEnd would never fire -> remove it immediately |
| this._removeInternal(); |
| return; |
| } |
| this.$container.addClass(this.animateRemovalClass); |
| this.$container.oneAnimationEnd(function() { |
| this._removeInternal(); |
| }.bind(this)); |
| }.bind(this)); |
| |
| // If the parent is being removed while the animation is running, the animationEnd event will never fire |
| // -> Make sure remove is called nevertheless. Important: remove it before the parent is removed to maintain the regular remove order |
| this.parent.one('removing', this._parentRemovingWhileAnimatingHandler); |
| } |
| |
| _onParentRemovingWhileAnimating() { |
| this._removeInternal(); |
| } |
| |
| _renderInspectorInfo() { |
| if (!this.session.inspector) { |
| return; |
| } |
| inspector.applyInfo(this); |
| } |
| |
| /** |
| * Links $container with the widget. |
| */ |
| _linkWithDOM() { |
| if (this.$container) { |
| this.$container.data('widget', this); |
| } |
| } |
| |
| /** |
| * Called right before _remove is called. |
| * Default calls LayoutValidator.cleanupInvalidComponents to make sure that child components are removed from the invalid components list. |
| * Also uninstalls key stroke context, loading support and scrollbars. |
| */ |
| _cleanup() { |
| this.parent.off('removing', this._parentRemovingWhileAnimatingHandler); |
| this.session.keyStrokeManager.uninstallKeyStrokeContext(this.keyStrokeContext); |
| if (this.loadingSupport) { |
| this.loadingSupport.remove(); |
| } |
| this._uninstallScrollbars(); |
| if (this.$container) { |
| this.session.layoutValidator.cleanupInvalidComponents(this.$container); |
| } |
| } |
| |
| _remove() { |
| if (this.$container) { |
| this.$container.remove(); |
| this.$container = null; |
| } |
| } |
| |
| setOwner(owner) { |
| scout.assertParameter('owner', owner); |
| if (owner === this.owner) { |
| return; |
| } |
| |
| if (this.owner) { |
| // Remove from old owner |
| this.owner._removeChild(this); |
| } |
| this.owner = owner; |
| this.owner._addChild(this); |
| } |
| |
| setParent(parent) { |
| scout.assertParameter('parent', parent); |
| if (parent === this.parent) { |
| return; |
| } |
| if (this.rendered && !parent.rendered) { |
| $.log.isInfoEnabled() && $.log.info('rendered child ' + this + ' is added to not rendered parent ' + parent + '. Removing child.', new Error('origin')); |
| this.remove(); |
| } |
| |
| if (this.parent) { |
| // Don't link to new parent yet if removal is still pending. |
| // After the animation the parent will remove its children. |
| // If they are already linked to a new parent, removing the children is not possible anymore. |
| // This may lead to an "Already rendered" exception if the new parent wants to render its children. |
| if (this.parent._isRemovalPending()) { |
| this.parent.one('remove', function() { |
| this.setParent(parent); |
| }.bind(this)); |
| return; |
| } |
| |
| this.parent.off('destroy', this._parentDestroyHandler); |
| this.parent.off('removing', this._parentRemovingWhileAnimatingHandler); |
| |
| if (this.parent !== this.owner) { |
| // Remove from old parent if getting relinked |
| // If the old parent is still the owner, don't remove it because owner stays responsible for destroying it |
| this.parent._removeChild(this); |
| } |
| } |
| var oldParent = this.parent; |
| this.parent = parent; |
| this.parent._addChild(this); |
| this.trigger('hierarchyChange', { |
| oldParent: oldParent, |
| parent: parent |
| }); |
| if (this.initialized) { |
| this.recomputeEnabled(this.parent.enabledComputed); |
| } |
| this.parent.one('destroy', this._parentDestroyHandler); |
| } |
| |
| _addChild(child) { |
| $.log.isTraceEnabled() && $.log.trace('addChild(' + child + ') to ' + this); |
| arrays.pushSet(this.children, child); |
| } |
| |
| _removeChild(child) { |
| $.log.isTraceEnabled() && $.log.trace('removeChild(' + child + ') from ' + this); |
| arrays.remove(this.children, child); |
| } |
| |
| /** |
| * @returns {Widget[]} a list of all ancestors |
| */ |
| ancestors() { |
| var ancestors = []; |
| var parent = this.parent; |
| while (parent) { |
| ancestors.push(parent); |
| parent = parent.parent; |
| } |
| return ancestors; |
| } |
| |
| /** |
| * @returns {boolean} true if the given widget is the same as this or a descendant |
| */ |
| isOrHas(widget) { |
| if (widget === this) { |
| return true; |
| } |
| return this.has(widget); |
| } |
| |
| /** |
| * @returns {boolean} true if the given widget is a descendant |
| */ |
| has(widget) { |
| while (widget) { |
| if (widget.parent === this) { |
| return true; |
| } |
| widget = widget.parent; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * @returns {Form} the form the widget belongs to (returns the first parent which is a {@link Form}. |
| */ |
| getForm() { |
| return Form.findForm(this); |
| } |
| |
| /** |
| * @returns {Form} the first form which is not an inner form of a wrapped form field |
| */ |
| findNonWrappedForm() { |
| return Form.findNonWrappedForm(this); |
| } |
| |
| /** |
| * @returns {Desktop} the desktop linked to the current session. |
| * If desktop is still initializing it might not be available yet, in that case it searches the parent hierarchy for it. |
| */ |
| findDesktop() { |
| if (this.session.desktop) { |
| return this.session.desktop; |
| } |
| return this.findParent(function(parent) { |
| return parent instanceof Desktop; |
| }); |
| } |
| |
| /** |
| * Changes the enabled property of this form field to the given value. |
| * |
| * @param {boolean} enabled |
| * Required. The new enabled value |
| * @param {boolean} [updateParents] |
| * (optional) If true, the enabled property of all parent form fields are |
| * updated to same value as well. Default is false. |
| * @param {boolean} [updateChildren] |
| * (optional) If true the enabled property of all child form fields (recursive) |
| * are updated to same value as well. Default is false. |
| */ |
| setEnabled(enabled, updateParents, updateChildren) { |
| this.setProperty('enabled', enabled); |
| |
| if (enabled && updateParents && this.parent) { |
| this.parent.setEnabled(true, true, false); |
| } |
| |
| if (updateChildren) { |
| this.visitChildren(function(field) { |
| field.setEnabled(enabled); |
| }); |
| } |
| } |
| |
| _setEnabled(enabled) { |
| this._setProperty('enabled', enabled); |
| if (this.initialized) { |
| this.recomputeEnabled(); |
| } |
| } |
| |
| recomputeEnabled(parentEnabled) { |
| if (parentEnabled === undefined) { |
| parentEnabled = true; |
| if (this.parent && this.parent.initialized && this.parent.enabledComputed !== undefined) { |
| parentEnabled = this.parent.enabledComputed; |
| } |
| } |
| |
| var enabledComputed = this._computeEnabled(this.inheritAccessibility, parentEnabled); |
| this._updateEnabledComputed(enabledComputed); |
| } |
| |
| _updateEnabledComputed(enabledComputed, enabledComputedForChildren) { |
| if (this.enabledComputed === enabledComputed && enabledComputedForChildren === undefined) { |
| // no change for this instance. there is no need to propagate to children |
| // exception: the enabledComputed for the children differs from the one for me. In this case the propagation is necessary. |
| return; |
| } |
| |
| this.setProperty('enabledComputed', enabledComputed); |
| |
| // Manually call _renderEnabled(), because _renderEnabledComputed() does not exist |
| if (this.rendered) { |
| this._renderEnabled(); |
| } |
| |
| var computedStateForChildren = scout.nvl(enabledComputedForChildren, enabledComputed); |
| this.children.forEach(function(child) { |
| if (child.inheritAccessibility) { |
| child.recomputeEnabled(computedStateForChildren); |
| } |
| }); |
| } |
| |
| _computeEnabled(inheritAccessibility, parentEnabled) { |
| return this.enabled && (inheritAccessibility ? parentEnabled : true); |
| } |
| |
| _renderEnabled() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.setEnabled(this.enabledComputed); |
| this._renderDisabledStyle(); |
| } |
| |
| setInheritAccessibility(inheritAccessibility) { |
| this.setProperty('inheritAccessibility', inheritAccessibility); |
| } |
| |
| _setInheritAccessibility(inheritAccessibility) { |
| this._setProperty('inheritAccessibility', inheritAccessibility); |
| if (this.initialized) { |
| this.recomputeEnabled(); |
| } |
| } |
| |
| setDisabledStyle(disabledStyle) { |
| this.setProperty('disabledStyle', disabledStyle); |
| |
| this.children.forEach(function(child) { |
| child.setDisabledStyle(disabledStyle); |
| }); |
| } |
| |
| _renderDisabledStyle() { |
| this._renderDisabledStyleInternal(this.$container); |
| } |
| |
| /** |
| * This function is used by subclasses to render the read-only class for a given $field. |
| * Some fields like DateField have two input fields and thus cannot use the this.$field property. |
| */ |
| _renderDisabledStyleInternal($element) { |
| if (!$element) { |
| return; |
| } |
| if (this.enabledComputed) { |
| $element.removeClass('read-only'); |
| } else { |
| $element.toggleClass('read-only', this.disabledStyle === Widget.DisabledStyle.READ_ONLY); |
| } |
| } |
| |
| /** |
| * @param {boolean} visible true, to make the widget visible, false to hide it |
| */ |
| setVisible(visible) { |
| this.setProperty('visible', visible); |
| } |
| |
| /** |
| * @returns {boolean} whether the widget is visible or not. May depend on other conditions than the visible property only |
| */ |
| isVisible() { |
| return this.visible; |
| } |
| |
| _renderVisible() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.setVisible(this.isVisible()); |
| this.invalidateParentLogicalGrid(); |
| } |
| |
| /** |
| * @returns {boolean} true if every parent within the hierarchy is visible. |
| */ |
| isEveryParentVisible() { |
| var parent = this.parent; |
| while (parent) { |
| if (!parent.isVisible()) { |
| return false; |
| } |
| parent = parent.parent; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * This function does not set the focus to the field. It toggles the 'focused' class on the field container if present. |
| * Objects using widget as prototype must call this function onBlur and onFocus to ensure the class gets toggled. |
| * |
| * Use Widget.focus to set the focus to the widget. |
| */ |
| setFocused(focused) { |
| this.setProperty('focused', focused); |
| } |
| |
| _renderFocused() { |
| if (this.$container) { |
| this.$container.toggleClass('focused', this.focused); |
| } |
| } |
| |
| _setCssClass(cssClass) { |
| if (this.rendered) { |
| this._removeCssClass(); |
| } |
| this._setProperty('cssClass', cssClass); |
| } |
| |
| _removeCssClass() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.removeClass(this.cssClass); |
| } |
| |
| _renderCssClass() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.addClass(this.cssClass); |
| } |
| |
| setCssClass(cssClass) { |
| this.setProperty('cssClass', cssClass); |
| } |
| |
| addCssClass(cssClass) { |
| var cssClasses = this.cssClassAsArray(); |
| var cssClassesToAdd = Widget.cssClassAsArray(cssClass); |
| cssClassesToAdd.forEach(function(newCssClass) { |
| if (cssClasses.indexOf(newCssClass) >= 0) { |
| return; |
| } |
| cssClasses.push(newCssClass); |
| }, this); |
| this.setProperty('cssClass', arrays.format(cssClasses, ' ')); |
| } |
| |
| removeCssClass(cssClass) { |
| var cssClasses = this.cssClassAsArray(); |
| var cssClassesToRemove = Widget.cssClassAsArray(cssClass); |
| if (arrays.removeAll(cssClasses, cssClassesToRemove)) { |
| this.setProperty('cssClass', arrays.format(cssClasses, ' ')); |
| } |
| } |
| |
| toggleCssClass(cssClass, condition) { |
| if (condition) { |
| this.addCssClass(cssClass); |
| } else { |
| this.removeCssClass(cssClass); |
| } |
| } |
| |
| cssClassAsArray() { |
| return Widget.cssClassAsArray(this.cssClass); |
| } |
| |
| /** |
| * Creates nothing by default. If a widget needs loading support, override this method and return a loading support. |
| * @returns {LoadingSupport} |
| */ |
| _createLoadingSupport() { |
| return null; |
| } |
| |
| setLoading(loading) { |
| this.setProperty('loading', loading); |
| } |
| |
| isLoading() { |
| return this.loading; |
| } |
| |
| _renderLoading() { |
| if (!this.loadingSupport) { |
| return; |
| } |
| this.loadingSupport.renderLoading(); |
| } |
| |
| // --- Layouting / HtmlComponent methods --- |
| |
| pack() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.pack(); |
| } |
| |
| invalidateLayout() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.invalidateLayout(); |
| } |
| |
| validateLayout() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.validateLayout(); |
| } |
| |
| revalidateLayout() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.revalidateLayout(); |
| } |
| |
| /** |
| * @param [invalidateParents] optional, default is true |
| */ |
| invalidateLayoutTree(invalidateParents) { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.invalidateLayoutTree(invalidateParents); |
| } |
| |
| validateLayoutTree() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.validateLayoutTree(); |
| } |
| |
| revalidateLayoutTree(invalidateParents) { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.revalidateLayoutTree(invalidateParents); |
| } |
| |
| /** |
| * The layout data contains hints for the layout of the parent container to layout this individual child widget inside the container.<br> |
| * Note: this is not the same as the LayoutConfig. The LayoutConfig contains constraints for the layout itself and is therefore set on the parent container directly. |
| * <p> |
| * Example: The parent container uses a LogicalGridLayout to layout its children. Every child has a LogicalGridLayoutData to tell the layout how this specific child should be layouted. |
| * The parent may have a LogicalGridLayoutConfig to specify constraints which affect either only the container or every child in the container. |
| */ |
| setLayoutData(layoutData) { |
| if (!this.rendered) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.layoutData = layoutData; |
| } |
| |
| /** |
| * If the widget uses a logical grid layout, the grid may be validated using this method. |
| * <p> |
| * If the grid is not dirty, nothing happens. |
| */ |
| validateLogicalGrid() { |
| if (this.logicalGrid) { |
| this.logicalGrid.validate(this); |
| } |
| } |
| |
| /** |
| * Marks the logical grid as dirty.<br> |
| * Does nothing, if there is no logical grid. |
| * @param {boolean} [invalidateLayout] true, to invalidate the layout afterwards, false if not. Default is true. |
| */ |
| invalidateLogicalGrid(invalidateLayout) { |
| if (!this.initialized) { |
| return; |
| } |
| if (!this.logicalGrid) { |
| return; |
| } |
| this.logicalGrid.setDirty(true); |
| if (scout.nvl(invalidateLayout, true)) { |
| this.invalidateLayoutTree(); |
| } |
| } |
| |
| /** |
| * Invalidates the logical grid of the parent widget. Typically done when the visibility of the widget changes. |
| * @param {boolean} [invalidateLayout] true, to invalidate the layout of the parent of this.htmlComp, false if not. Default is true. |
| */ |
| invalidateParentLogicalGrid(invalidateLayout) { |
| this.parent.invalidateLogicalGrid(false); |
| if (!this.rendered || !this.htmlComp) { |
| return; |
| } |
| if (scout.nvl(invalidateLayout, true)) { |
| var htmlCompParent = this.htmlComp.getParent(); |
| if (htmlCompParent) { |
| htmlCompParent.invalidateLayoutTree(); |
| } |
| } |
| } |
| |
| revalidateLogicalGrid(invalidateLayout) { |
| this.invalidateLogicalGrid(invalidateLayout); |
| this.validateLogicalGrid(); |
| } |
| |
| setLogicalGrid(logicalGrid) { |
| this.setProperty('logicalGrid', logicalGrid); |
| } |
| |
| /** |
| * @param logicalGrid an instance of {@link LogicalGrid} or a string representing the object type of a logical grid. |
| */ |
| _setLogicalGrid(logicalGrid) { |
| if (typeof logicalGrid === 'string') { |
| logicalGrid = scout.create(logicalGrid); |
| } |
| this._setProperty('logicalGrid', logicalGrid); |
| this.invalidateLogicalGrid(); |
| } |
| |
| // --- Event handling methods --- |
| _createEventSupport() { |
| return new EventSupport(); |
| } |
| |
| trigger(type, event) { |
| event = event || {}; |
| event.source = this; |
| this.events.trigger(type, event); |
| } |
| |
| one(type, func) { |
| this.events.one(type, func); |
| } |
| |
| on(type, func) { |
| return this.events.on(type, func); |
| } |
| |
| off(type, func) { |
| this.events.off(type, func); |
| } |
| |
| addListener(listener) { |
| this.events.addListener(listener); |
| } |
| |
| removeListener(listener) { |
| this.events.removeListener(listener); |
| } |
| |
| /** |
| * Adds an event handler using {@link #one()} and returns a promise. |
| * The promise is resolved as soon as the event is triggered. |
| */ |
| when(type) { |
| return this.events.when(type); |
| } |
| |
| /** |
| * @returns {$} the entry-point for this Widget or its parent. If the widget is part of the main-window it returns this.session.$entryPoint, |
| * for popup-window this function will return the body of the document in the popup window. |
| */ |
| entryPoint() { |
| var $element = scout.nvl(this.$container, this.parent.$container); |
| if (!$element || !$element.length) { |
| throw new Error('Cannot resolve entryPoint, $element.length is 0 or undefined'); |
| } |
| return $element.entryPoint(); |
| } |
| |
| window(domElement) { |
| var $el = this.$container || this.$parent; |
| return $el ? $el.window(domElement) : domElement ? null : $(null); |
| } |
| |
| document(domElement) { |
| var $el = this.$container || this.$parent; |
| return $el ? $el.document(domElement) : domElement ? null : $(null); |
| } |
| |
| /** |
| * This method attaches the detached $container to the DOM. |
| */ |
| attach() { |
| if (this.attached || !this.rendered) { |
| return; |
| } |
| this._attach(); |
| this._installFocusContext(); |
| this.restoreFocus(); |
| this.attached = true; |
| this._postAttach(); |
| this._onAttach(); |
| this._triggerChildrenOnAttach(this); |
| } |
| |
| /** |
| * Override this method to do something when Widget is attached again. Typically |
| * you will append this.$container to this.$parent. |
| * |
| * @param the event.target property is used to decide if a Widget must attach |
| * its $container. When the parent of the Widget already attaches, the Widget |
| * itself must _not_ attach its own $container. That's why we should only |
| * attach when event.target is === this. |
| */ |
| _attach() { |
| // NOP |
| } |
| |
| /** |
| * Override this method to do something after this widget is attached. |
| * This function is not called on any child of the attached widget. |
| */ |
| _postAttach() { |
| // NOP |
| } |
| |
| _triggerChildrenOnAttach(parent) { |
| this.children.forEach(function(child) { |
| child._onAttach(); |
| child._triggerChildrenOnAttach(parent); |
| }); |
| } |
| |
| /** |
| * Override this method to do something after this widget or any parent of it is attached. |
| * This function is called whether or not the widget is rendered. |
| */ |
| _onAttach() { |
| if (this.rendered) { |
| this._renderOnAttach(); |
| } |
| } |
| |
| /** |
| * Override this method to do something after this widget or any parent of it is attached. |
| * This function is only called when this widget is rendered. |
| */ |
| _renderOnAttach() { |
| this._renderScrollTop(); |
| this._renderScrollLeft(); |
| } |
| |
| /** |
| * This method calls detach() on all child-widgets. It is used to store some data |
| * before a DOM element is detached and propagate the detach "event" to all child- |
| * widgets, because when a DOM element is detached - child elements are not notified |
| */ |
| detach() { |
| if (this.rendering) { |
| // Defer the execution of detach. If it was detached while rendering the attached flag would be wrong. |
| this._postRenderActions.push(this.detach.bind(this)); |
| } |
| if (!this.attached || !this.rendered || this._isRemovalPending()) { |
| return; |
| } |
| |
| this._beforeDetach(); |
| this._onDetach(); |
| this._triggerChildrenOnDetach(this); |
| this._detach(); |
| this.attached = false; |
| } |
| |
| /** |
| * This function is called before a widget gets detached. The function is only called on the detached widget and NOT on |
| * any of its children. |
| */ |
| _beforeDetach(parent) { |
| if (!this.$container) { |
| return; |
| } |
| |
| var activeElement = this.$container.document(true).activeElement; |
| var isFocused = this.$container.isOrHas(activeElement); |
| var focusManager = this.session.focusManager; |
| |
| if (focusManager.isFocusContextInstalled(this.$container)) { |
| this._uninstallFocusContext(); |
| } else if (isFocused) { |
| // exclude the container or any of its child elements to gain focus |
| focusManager.validateFocus(filters.outsideFilter(this.$container)); |
| } |
| } |
| |
| _triggerChildrenOnDetach() { |
| this.children.forEach(function(child) { |
| child._onDetach(); |
| child._triggerChildrenOnDetach(parent); |
| }); |
| } |
| |
| /** |
| * This function is called before a widget or any of its parent getting detached. |
| * This function is thought to be overridden. |
| */ |
| _onDetach() { |
| if (this.rendered) { |
| this._renderOnDetach(); |
| } |
| } |
| |
| _renderOnDetach() { |
| // NOP |
| } |
| |
| /** |
| * Override this method to do something when Widget is detached. Typically you |
| * will call this.$container.detach(). The default |
| * implementation sets this.attached to false. |
| */ |
| _detach() { |
| } |
| |
| _uninstallFocusContext() { |
| // NOP |
| } |
| |
| _installFocusContext() { |
| // NOP |
| } |
| |
| /** |
| * Does nothing by default. If a widget needs keystroke support override this method and return a keystroke context, e.g. the default KeyStrokeContext. |
| * @returns {KeyStrokeContext} |
| */ |
| _createKeyStrokeContext() { |
| return null; |
| } |
| |
| updateKeyStrokes(newKeyStrokes, oldKeyStrokes) { |
| this.unregisterKeyStrokes(oldKeyStrokes); |
| this.registerKeyStrokes(newKeyStrokes); |
| } |
| |
| registerKeyStrokes(keyStrokes) { |
| this.keyStrokeContext.registerKeyStrokes(keyStrokes); |
| } |
| |
| unregisterKeyStrokes(keyStrokes) { |
| this.keyStrokeContext.unregisterKeyStrokes(keyStrokes); |
| } |
| |
| /** |
| * Triggers a property change for a single property. The event is only triggered when |
| * old and new value are the same. |
| */ |
| triggerPropertyChange(propertyName, oldValue, newValue) { |
| scout.assertParameter('propertyName', propertyName); |
| var event = new Event({ |
| propertyName: propertyName, |
| oldValue: oldValue, |
| newValue: newValue |
| }); |
| this.trigger('propertyChange', event); |
| return event; |
| } |
| |
| /** |
| * Sets the value of the property 'propertyName' to 'newValue' and then fires a propertyChange event for that property. |
| */ |
| _setProperty(propertyName, newValue) { |
| scout.assertParameter('propertyName', propertyName); |
| var oldValue = this[propertyName]; |
| if (objects.equals(oldValue, newValue)) { |
| return; |
| } |
| this[propertyName] = newValue; |
| var event = this.triggerPropertyChange(propertyName, oldValue, newValue); |
| if (event.defaultPrevented) { |
| // Revert to old value if property change should be prevented |
| this[propertyName] = oldValue; |
| } |
| } |
| |
| /** |
| * Sets a new value for a specific property. If the new value is the same value as the old one, nothing is performed. |
| * Otherwise the following phases are executed: |
| * <p> |
| * 1. Preparation: If the property is a widget property, several actions are performed in _prepareWidgetProperty(). |
| * 2. DOM removal: If the property is a widget property and the widget is rendered, the changed widget(s) are removed unless the property should not be preserved (see _preserveOnPropertyChangeProperties). |
| * If there is a custom remove function (e.g. _removeXY where XY is the property name), it will be called instead of removing the widgets directly. |
| * 3. Model update: If there is a custom set function (e.g. _setXY where XY is the property name), it will be called. Otherwise the default set function _setProperty is called. |
| * 4. DOM rendering: If the widget is rendered and there is a custom render function (e.g. _renderXY where XY is the property name), it will be called. Otherwise nothing happens. |
| */ |
| setProperty(propertyName, value) { |
| if (objects.equals(this[propertyName], value)) { |
| return; |
| } |
| |
| value = this._prepareProperty(propertyName, value); |
| if (this.rendered) { |
| this._callRemoveProperty(propertyName); |
| } |
| this._callSetProperty(propertyName, value); |
| if (this.rendered) { |
| this._callRenderProperty(propertyName); |
| } |
| } |
| |
| _prepareProperty(propertyName, value) { |
| if (!this.isWidgetProperty(propertyName)) { |
| return value; |
| } |
| return this._prepareWidgetProperty(propertyName, value); |
| } |
| |
| _prepareWidgetProperty(propertyName, widgets) { |
| // Create new child widget(s) |
| widgets = this._createChildren(widgets); |
| |
| var oldWidgets = this[propertyName]; |
| if (oldWidgets && Array.isArray(widgets)) { |
| // If new value is an array, old value has to be one as well |
| // Only destroy those which are not in the new array |
| oldWidgets = arrays.diff(oldWidgets, widgets); |
| } |
| |
| if (!this.isPreserveOnPropertyChangeProperty(propertyName)) { |
| // Destroy old child widget(s) |
| this._destroyChildren(oldWidgets); |
| |
| // Link to new parent |
| this.link(widgets); |
| } |
| |
| return widgets; |
| } |
| |
| /** |
| * Does nothing if the property is not a widget property.<p> |
| * If it is a widget property, it removes the existing widgets. Render has to be implemented by the widget itself. |
| */ |
| _callRemoveProperty(propertyName) { |
| if (!this.isWidgetProperty(propertyName)) { |
| return; |
| } |
| if (this.isPreserveOnPropertyChangeProperty(propertyName)) { |
| return; |
| } |
| var widgets = this[propertyName]; |
| if (!widgets) { |
| return; |
| } |
| var removeFuncName = '_remove' + strings.toUpperCaseFirstLetter(propertyName); |
| if (this[removeFuncName]) { |
| this[removeFuncName](); |
| } else { |
| this._internalRemoveWidgets(widgets); |
| } |
| } |
| |
| /** |
| * Removes the given widgets |
| */ |
| _internalRemoveWidgets(widgets) { |
| widgets = arrays.ensure(widgets); |
| widgets.forEach(function(widget) { |
| widget.remove(); |
| }); |
| } |
| |
| _callSetProperty(propertyName, value) { |
| var setFuncName = '_set' + strings.toUpperCaseFirstLetter(propertyName); |
| if (this[setFuncName]) { |
| this[setFuncName](value); |
| } else { |
| this._setProperty(propertyName, value); |
| } |
| } |
| |
| _callRenderProperty(propertyName) { |
| var renderFuncName = '_render' + strings.toUpperCaseFirstLetter(propertyName); |
| if (!this[renderFuncName]) { |
| return; |
| } |
| this[renderFuncName](); |
| } |
| |
| /** |
| * Sets this widget as parent of the given widget(s). |
| * |
| * @param widgets may be a widget or array of widgets |
| */ |
| link(widgets) { |
| if (!widgets) { |
| return; |
| } |
| |
| widgets = arrays.ensure(widgets); |
| widgets.forEach(function(child, i) { |
| child.setParent(this); |
| }, this); |
| } |
| |
| /** |
| * Method required for widgets which are supposed to be directly covered by a glasspane.<p> |
| * |
| * Returns the DOM elements to paint a glassPanes over, once a modal Form, message-box or file-chooser is shown with this widget as its 'displayParent'.<br> |
| * If the widget is not rendered yet, a scout.DerredGlassPaneTarget is returned.<br> |
| * In both cases the method _glassPaneTargets is called which may be overridden by the actual widget. |
| */ |
| glassPaneTargets(element) { |
| var resolveGlassPanes = function(element) { |
| // contributions |
| var targets = arrays.flatMap(this._glassPaneContributions, function(cont) { |
| var $elements = cont(element); |
| if ($elements) { |
| return arrays.ensure($elements); |
| } |
| return []; |
| }); |
| return targets.concat(this._glassPaneTargets(element)); |
| }.bind(this); |
| if (this.rendered) { |
| return resolveGlassPanes(element); |
| } |
| |
| return DeferredGlassPaneTarget.createFor(this, resolveGlassPanes.bind(this, element)); |
| } |
| |
| _glassPaneTargets(element) { |
| // since popups are rendered outside the DOM of the widget parent-child hierarchy, get glassPaneTargets of popups belonging to this widget separately. |
| return [this.$container].concat( |
| this.session.desktop.getPopupsFor(this) |
| .reduce(function(acc, popup) { |
| return acc.concat(popup.glassPaneTargets()); |
| }, [])); |
| } |
| |
| addGlassPaneContribution(contribution) { |
| this._glassPaneContributions.push(contribution); |
| this.trigger('glassPaneContributionAdded', { |
| contribution: contribution |
| }); |
| } |
| |
| /** |
| * @param [contribution] a function which returns glass pane targets (jQuery elements) |
| */ |
| removeGlassPaneContribution(contribution) { |
| arrays.remove(this._glassPaneContributions, contribution); |
| this.trigger('glassPaneContributionRemoved', { |
| contribution: contribution |
| }); |
| } |
| |
| toString() { |
| var attrs = ''; |
| attrs += 'id=' + this.id; |
| attrs += ' objectType=' + this.objectType; |
| attrs += ' rendered=' + this.rendered; |
| if (this.$container) { |
| attrs += ' $container=' + graphics.debugOutput(this.$container); |
| } |
| return 'Widget[' + attrs.trim() + ']'; |
| } |
| |
| /** |
| * Returns the ancestors as string delimited by '\n'. |
| * @param [count] the number of ancestors to be processed. Default is -1 which means all. |
| */ |
| ancestorsToString(count) { |
| var str = '', |
| ancestors = this.ancestors(); |
| |
| count = scout.nvl(count, -1); |
| ancestors.some(function(ancestor, i) { |
| if (count > -1 && i >= count) { |
| return true; |
| } |
| if (i > 0 && i < ancestors.length - 1) { |
| str += '\n'; |
| } |
| str += ancestor.toString(); |
| return false; |
| }); |
| return str; |
| } |
| |
| resolveTextKeys(properties) { |
| properties.forEach(function(property) { |
| texts.resolveTextProperty(this, property); |
| }, this); |
| } |
| |
| resolveIconIds(properties) { |
| properties.forEach(function(property) { |
| icons.resolveIconProperty(this, property); |
| }, this); |
| } |
| |
| resolveConsts(configs) { |
| configs.forEach(function(config) { |
| objects.resolveConstProperty(this, config); |
| }, this); |
| } |
| |
| /** |
| * A so called widget property is a property with a widget as value incl. automatic resolution of that widget. |
| * This means the property not only accepts the actual widget, but also a widget model or a widget reference (id) |
| * and then either creates a new widget based on the model or resolves the id and uses the referenced widget as value. |
| * Furthermore it will take care of its lifecycle which means, the widget will automatically be removed and destroyed (as long as the parent is also the owner). |
| * <p> |
| * If only the resolve operations without the lifecycle actions should be performed, you need to add the property to the list _preserveOnPropertyChangeProperties as well. |
| */ |
| _addWidgetProperties(properties) { |
| this._addProperties('_widgetProperties', properties); |
| } |
| |
| isWidgetProperty(propertyName) { |
| return this._widgetProperties.indexOf(propertyName) > -1; |
| } |
| |
| _addCloneProperties(properties) { |
| this._addProperties('_cloneProperties', properties); |
| } |
| |
| isCloneProperty(propertyName) { |
| return this._cloneProperties.indexOf(propertyName) > -1; |
| } |
| |
| /** |
| * Properties in this list won't be affected by the automatic lifecycle actions performed for regular widget properties. |
| * This means, the widget won't be removed, destroyed and also not linked, which means the parent stays the same. |
| * But the resolve operations are still applied, as for regular widget properties. |
| * <p> |
| * The typical use case for such properties is referencing another widget without taking care of that widget. |
| */ |
| _addPreserveOnPropertyChangeProperties(properties) { |
| this._addProperties('_preserveOnPropertyChangeProperties', properties); |
| } |
| |
| isPreserveOnPropertyChangeProperty(propertyName) { |
| return this._preserveOnPropertyChangeProperties.indexOf(propertyName) > -1; |
| } |
| |
| _addProperties(propertyName, properties) { |
| properties = arrays.ensure(properties); |
| properties.forEach(function(property) { |
| if (this[propertyName].indexOf(property) > -1) { |
| throw new Error(propertyName + ' already contains the property ' + property); |
| } |
| this[propertyName].push(property); |
| }, this); |
| } |
| |
| _eachProperty(model, func) { |
| var propertyName, value, i; |
| |
| // Loop through primitive properties |
| for (propertyName in model) { |
| if (this._widgetProperties.indexOf(propertyName) > -1) { |
| continue; // will be handled below |
| } |
| value = model[propertyName]; |
| func(propertyName, value); |
| } |
| |
| // Loop through adapter properties (any order will do). |
| for (i = 0; i < this._widgetProperties.length; i++) { |
| propertyName = this._widgetProperties[i]; |
| value = model[propertyName]; |
| if (value === undefined) { |
| continue; |
| } |
| |
| func(propertyName, value, true); |
| } |
| } |
| |
| _removeWidgetProperties(properties) { |
| if (Array.isArray(properties)) { |
| arrays.removeAll(this._widgetProperties, properties); |
| } else { |
| arrays.remove(this._widgetProperties, properties); |
| } |
| } |
| |
| /** |
| * Clones the widget and mirrors the events, see this.clone() and this.mirror() for details. |
| */ |
| cloneAndMirror(model) { |
| return this.clone(model, { |
| delegateAllPropertiesToClone: true |
| }); |
| } |
| |
| /** |
| * @returns {Widget} the original widget from which this one was cloned. If it is not a clone, itself is returned. |
| */ |
| original() { |
| var original = this; |
| while (original.cloneOf) { |
| original = original.cloneOf; |
| } |
| return original; |
| } |
| |
| /** |
| * Clones the widget and returns the clone. Only the properties defined in this._cloneProperties are copied to the clone. |
| * The parameter model has to contain at least the property 'parent'. |
| * |
| * OPTION DEFAULT VALUE DESCRIPTION |
| * -------------------------------------------------------------------------------------------------------- |
| * delegatePropertiesToClone [] An array of all properties to be delegated from the original |
| * to the to the clone when changed on the original widget. |
| * |
| * delegatePropertiesToOriginal [] An array of all properties to be delegated from the clone |
| * to the original when changed on the clone widget. |
| * |
| * excludePropertiesToOriginal [] An array of all properties to be excluded from delegating |
| * from the clone to the original in any cases. |
| * |
| * delegateEventsToOriginal [] An array of all events to be delegated from the clone to |
| * the original when fired on the clone widget. |
| * |
| * delegateAllPropertiesToClone false True to delegate all property changes from the original to |
| * the clone. |
| * |
| * delegateAllPropertiesToOriginal false True to delegate all property changes from the clone to |
| * the original. |
| * |
| * @param model The model used to create the clone is a combination of the clone properties and this model. |
| * Therefore this model may be used to override the cloned properties or to add additional properties. |
| * @param options Options used for the clone widgets. See above. |
| * |
| */ |
| clone(model, options) { |
| var clone, cloneModel; |
| model = model || {}; |
| options = options || {}; |
| |
| cloneModel = objects.extractProperties(this, model, this._cloneProperties); |
| clone = scout.create(this.objectType, cloneModel); |
| clone.cloneOf = this; |
| this._mirror(clone, options); |
| |
| if (this.logicalGrid) { |
| // Create a new logical grid to make sure it does not influence the original widget |
| // This also creates the correct grid config for the specific widget |
| clone.setLogicalGrid(this.logicalGrid.objectType); |
| } else { |
| // Remove the grid if the original does not have one either |
| clone.setLogicalGrid(null); |
| } |
| |
| return clone; |
| } |
| |
| _deepCloneProperties(clone, properties, options) { |
| if (!properties) { |
| return clone; |
| } |
| properties = arrays.ensure(properties); |
| properties.forEach(function(property) { |
| var propertyValue = this[property], |
| clonedProperty = null; |
| if (propertyValue === undefined) { |
| throw new Error('Property \'' + property + '\' is undefined. Deep copy not possible.'); |
| } |
| if (this._widgetProperties.indexOf(property) > -1) { |
| if (Array.isArray(propertyValue)) { |
| clonedProperty = propertyValue.map(function(val) { |
| return val.clone({ |
| parent: clone |
| }, options); |
| }); |
| } else { |
| clonedProperty = propertyValue.clone({ |
| parent: clone |
| }, options); |
| } |
| } else if (Array.isArray(propertyValue)) { |
| clonedProperty = propertyValue.map(function(val) { |
| return val; |
| }); |
| } else { |
| clonedProperty = propertyValue; |
| } |
| clone[property] = clonedProperty; |
| }.bind(this)); |
| } |
| |
| /** |
| * Delegates every property change event from the original widget to this cloned widget by calling the appropriate setter. |
| * If no target is set it works only if this widget is a clone. |
| */ |
| mirror(options, target) { |
| target = target || this.cloneOf; |
| if (!target) { |
| throw new Error('No target for mirroring.'); |
| } |
| this._mirror(target, options); |
| } |
| |
| _mirror(clone, options) { |
| var eventDelegator = arrays.find(this.eventDelegators, function(eventDelegator) { |
| return eventDelegator.clone === clone; |
| }); |
| if (eventDelegator) { |
| throw new Error('_mirror can only be called on not mirrored widgets. call unmirror first.'); |
| } |
| options = options || {}; |
| eventDelegator = { |
| clone: clone, |
| originalToClone: EventDelegator.create(this, clone, { |
| delegateProperties: options.delegatePropertiesToClone, |
| delegateAllProperties: options.delegateAllPropertiesToClone |
| }), |
| cloneToOriginal: EventDelegator.create(clone, this, { |
| delegateProperties: options.delegatePropertiesToOriginal, |
| delegateAllProperties: options.delegateAllPropertiesToOriginal, |
| excludeProperties: options.excludePropertiesToOriginal, |
| delegateEvents: options.delegateEventsToOriginal |
| }) |
| }; |
| this.eventDelegators.push(eventDelegator); |
| clone.one('destroy', function() { |
| this._unmirror(clone); |
| }.bind(this)); |
| } |
| |
| unmirror(target) { |
| target = target || this.cloneOf; |
| if (!target) { |
| throw new Error('No target for unmirroring.'); |
| } |
| this._unmirror(target); |
| } |
| |
| _unmirror(target) { |
| var eventDelegatorIndex = arrays.findIndex(this.eventDelegators, function(eventDelegator) { |
| return eventDelegator.clone === target; |
| }), |
| eventDelegator = eventDelegatorIndex > -1 ? this.eventDelegators.splice(eventDelegatorIndex, 1)[0] : null; |
| if (!eventDelegator) { |
| return; |
| } |
| if (eventDelegator.originalToClone) { |
| eventDelegator.originalToClone.destroy(); |
| } |
| if (eventDelegator.cloneToOriginal) { |
| eventDelegator.cloneToOriginal.destroy(); |
| } |
| } |
| |
| _onParentDestroy(event) { |
| if (this.destroyed) { |
| return; |
| } |
| // If the parent is destroyed but the widget not make sure it gets a new parent |
| // This ensures the old one may be properly garbage collected |
| this.setParent(this.owner); |
| } |
| |
| callSetter(propertyName, value) { |
| var setterFuncName = 'set' + strings.toUpperCaseFirstLetter(propertyName); |
| if (this[setterFuncName]) { |
| this[setterFuncName](value); |
| } else { |
| this.setProperty(propertyName, value); |
| } |
| } |
| |
| /** |
| * Traverses the object-tree (children) of this widget and searches for a widget with the given ID. |
| * Returns the widget with the requested ID or null if no widget has been found. |
| * |
| * @param widgetId |
| * @returns {Widget} the found widget for the given id |
| */ |
| widget(widgetId) { |
| if (predicate(this)) { |
| return this; |
| } |
| return this.findChild(predicate); |
| |
| function predicate(widget) { |
| if (widget.id === widgetId) { |
| return widget; |
| } |
| } |
| } |
| |
| /** |
| * Similar to widget(), but uses "breadth-first" strategy, i.e. it checks all children of the |
| * same depth (level) before it advances to the next level. If multiple widgets with the same |
| * ID exist, the one with the smallest distance to this widget is returned. |
| * |
| * Example: |
| * |
| * Widget ['MyWidget'] #1 |
| * +- GroupBox ['LeftBox'] #2 |
| * +- StringField ['NameField'] #3 |
| * +- StringField ['CityField'] #4 |
| * +- GroupBox ['InnerBox'] #5 |
| * +- GroupBox ['LeftBox'] #6 |
| * +- DateField ['StartDate'] #7 |
| * +- GroupBox ['RightBox'] #8 |
| * +- DateField ['EndDate'] #9 |
| * +- GroupBox ['RightBox'] #10 |
| * +- StringField ['NameField'] #11 |
| * +- DateField ['StartDate'] #12 |
| * |
| * CALL: RESULT: |
| * --------------------------------------------------------------------------------------------- |
| * this.widget('RightBox') #8 (might not be the expected result) |
| * this.nearestWidget('RightBox') #10 |
| * |
| * this.widget('NameField') #3 |
| * this.nearestWidget('NameField') null (because no direct child has the requested id) |
| * this.nearestWidget('NameField', true) #3 (because #3 and #11 have the same distance) |
| * |
| * this.widget('StartDate') #7 |
| * this.nearestWidget('StartDate', true) #12 (#12 has smaller distance than #7) |
| * |
| * @param {string} widgetId |
| * The ID of the widget to find. |
| * @param {boolean} deep |
| * If false, only this widget and the next level are checked. This is the default. |
| * If true, the entire tree is traversed. |
| * @return {Widget} the first found widget, or null if no widget was found. |
| */ |
| nearestWidget(widgetId, deep) { |
| if (this.id === widgetId) { |
| return this; |
| } |
| var widgets = this.children.slice(); // list of widgets to check |
| while (widgets.length) { |
| var widget = widgets.shift(); |
| if (widget.id === widgetId) { |
| return widget; // found |
| } |
| if (deep) { |
| for (var i = 0; i < widget.children.length; i++) { |
| var child = widget.children[i]; |
| if (child.parent === widget) { // same check as in visitChildren() |
| widgets.push(child); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @returns {Widget} the first parent for which the given function returns true. |
| */ |
| findParent(predicate) { |
| var parent = this.parent; |
| while (parent) { |
| if (predicate(parent)) { |
| return parent; |
| } |
| parent = parent.parent; |
| } |
| return parent; |
| } |
| |
| /** |
| * @returns {Widget} the first child for which the given function returns true. |
| */ |
| findChild(predicate) { |
| var foundChild = null; |
| this.visitChildren(function(child) { |
| if (predicate(child)) { |
| foundChild = child; |
| return true; |
| } |
| }); |
| return foundChild; |
| } |
| |
| setTrackFocus(trackFocus) { |
| this.setProperty('trackFocus', trackFocus); |
| } |
| |
| _renderTrackFocus() { |
| if (!this.$container) { |
| return; |
| } |
| if (this.trackFocus) { |
| this.$container.on('focusin', this._focusInListener); |
| } else { |
| this.$container.off('focusin', this._focusInListener); |
| } |
| } |
| |
| restoreFocus() { |
| if (this._$lastFocusedElement) { |
| this.session.focusManager.requestFocus(this._$lastFocusedElement); |
| } else if (this._storedFocusedWidget) { |
| this._storedFocusedWidget.focus(); |
| this._storedFocusedWidget = null; |
| } |
| } |
| |
| /** |
| * Method invoked once a 'focusin' event is fired by this context's $container or one of its child controls. |
| */ |
| _onFocusIn(event) { |
| // do not track focus events during rendering to avoid initial focus to be restored. |
| if (this.rendering) { |
| return; |
| } |
| var $target = $(event.target); |
| if (this.$container.has($target)) { |
| this._$lastFocusedElement = $target; |
| } |
| } |
| |
| /** |
| * Tries to set the focus on the widget. |
| * <p> |
| * By default the focus is set on the container but this may vary from widget to widget. |
| * @returns {boolean} true if the element could be focused, false if not |
| */ |
| focus() { |
| if (!this.rendered) { |
| this.session.layoutValidator.schedulePostValidateFunction(this.focus.bind(this)); |
| return false; |
| } |
| |
| return this.session.focusManager.requestFocus(this.getFocusableElement()); |
| } |
| |
| /** |
| * Calls {@link focus()} and prevents the default behavior of the event if the focusing was successful. |
| */ |
| focusAndPreventDefault(event) { |
| if (this.focus()) { |
| // Preventing blur is bad for touch devices because it prevents that the keyboard can close. |
| // In that case focus() will return false because focus manager is disabled. |
| event.preventDefault(); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * @returns whether the widget is the currently active element |
| */ |
| isFocused() { |
| return this.rendered && focusUtils.isActiveElement(this.getFocusableElement()); |
| } |
| |
| /** |
| * @return {boolean} true if the element is focusable, false if not. |
| */ |
| isFocusable() { |
| if (!this.rendered || !this.visible) { |
| return false; |
| } |
| var elem = this.getFocusableElement(); |
| if (elem) { |
| return $.ensure(elem).is(':focusable'); |
| } |
| return false; |
| } |
| |
| /** |
| * This method returns the HtmlElement to be used when {@link #focus()} is called. |
| * It can be overridden, in case the widget needs to return something other than this.$container[0]. |
| */ |
| getFocusableElement() { |
| if (this.rendered && this.$container) { |
| return this.$container[0]; |
| } |
| return null; |
| } |
| |
| _installScrollbars(options) { |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable) { |
| throw new Error('Scrollable is not defined, cannot install scrollbars'); |
| } |
| if ($scrollable.data('scrollable')) { |
| // already installed |
| return; |
| } |
| options = options || {}; |
| var defaults = { |
| parent: this |
| }; |
| options = $.extend({}, defaults, options); |
| scrollbars.install($scrollable, options); |
| $scrollable.on('scroll', this._scrollHandler); |
| } |
| |
| _uninstallScrollbars() { |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable || !$scrollable.data('scrollable')) { |
| return; |
| } |
| scrollbars.uninstall($scrollable, this.session); |
| $scrollable.off('scroll', this._scrollHandler); |
| if (!this.removing) { |
| // If scrollbars are removed on the fly and not because the widget is removing, reset scroll positions to initial state |
| // Only reset if position is 0 to preserve the position (uninstalling does not reset the position of the scrollable either) |
| if ($scrollable[0].scrollTop === 0) { |
| this.scrollTop = null; |
| } |
| if ($scrollable[0].scrollLeft === 0) { |
| this.scrollLeft = null; |
| } |
| } |
| } |
| |
| _onScroll() { |
| var $scrollable = this.get$Scrollable(); |
| this.scrollTop = $scrollable[0].scrollTop; |
| this.scrollLeft = $scrollable[0].scrollLeft; |
| } |
| |
| setScrollTop(scrollTop) { |
| if (this.getDelegateScrollable()) { |
| this.getDelegateScrollable().setScrollTop(scrollTop); |
| return; |
| } |
| if (this.scrollTop === scrollTop) { |
| return; |
| } |
| this.scrollTop = scrollTop; |
| if (this.rendered) { |
| this._renderScrollTop(); |
| } |
| } |
| |
| _renderScrollTop() { |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable || this.scrollTop === null) { |
| // Don't do anything for non scrollable elements. Also, reading $scrollable[0].scrollTop must not be done while rendering because it would provoke a reflow |
| return; |
| } |
| if (this.rendering || this.htmlComp && !this.htmlComp.layouted && !this.htmlComp.layouting) { |
| // If the widget is not layouted yet (which is always true while rendering), the scroll position cannot be updated -> do it after the layout |
| // If scroll top is set while layouting, layout obviously wants to set it -> do it |
| this.session.layoutValidator.schedulePostValidateFunction(this._renderScrollTop.bind(this)); |
| return; |
| } |
| scrollbars.scrollTop($scrollable, this.scrollTop); |
| } |
| |
| setScrollLeft(scrollLeft) { |
| if (this.getDelegateScrollable()) { |
| this.getDelegateScrollable().setScrollLeft(scrollLeft); |
| return; |
| } |
| if (this.scrollLeft === scrollLeft) { |
| return; |
| } |
| this.scrollLeft = scrollLeft; |
| if (this.rendered) { |
| this._renderScrollLeft(); |
| } |
| } |
| |
| _renderScrollLeft() { |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable || this.scrollLeft === null) { |
| // Don't do anything for non scrollable elements. Also, reading $scrollable[0].scrollLeft must not be done while rendering because it would provoke a reflow |
| return; |
| } |
| if (this.rendering || this.htmlComp && !this.htmlComp.layouted && !this.htmlComp.layouting) { |
| // If the widget is not layouted yet (which is always true while rendering), the scroll position cannot be updated -> do it after the layout |
| // If scroll left is set while layouting, layout obviously wants to set it -> do it |
| this.session.layoutValidator.schedulePostValidateFunction(this._renderScrollLeft.bind(this)); |
| return; |
| } |
| scrollbars.scrollLeft($scrollable, this.scrollLeft); |
| } |
| |
| /** |
| * Returns the jQuery element which is supposed to be scrollable. This element will be used by the scroll functions like {@link #_installScrollbars}, {@link #setScrollTop}, {@link #setScrollLeft}, {@link #scrollToBottom} etc.. |
| * The element won't be used unless {@link #_installScrollbars} is called. |
| * If the widget is mainly a wrapper for a scrollable widget and does not have a scrollable element by itself, you can use @{link #getDelegateScrollable} instead. |
| * @return {$} |
| */ |
| get$Scrollable() { |
| return this.$container; |
| } |
| |
| /** |
| * If the widget is mainly a wrapper for another widget, it is often the case that the other widget is scrollable and not the wrapper. |
| * In that case implement this method and return the other widget so that the calls to the scroll functions can be delegated. |
| * @return {Widget} |
| */ |
| getDelegateScrollable() { |
| return null; |
| } |
| |
| scrollToTop() { |
| if (this.getDelegateScrollable()) { |
| this.getDelegateScrollable().scrollToTop(); |
| return; |
| } |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable) { |
| return; |
| } |
| if (!this.rendered) { |
| this.session.layoutValidator.schedulePostValidateFunction(this.scrollToTop.bind(this)); |
| return; |
| } |
| scrollbars.scrollTop($scrollable, 0); |
| } |
| |
| scrollToBottom() { |
| if (this.getDelegateScrollable()) { |
| this.getDelegateScrollable().scrollToBottom(); |
| return; |
| } |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable) { |
| return; |
| } |
| if (!this.rendered) { |
| this.session.layoutValidator.schedulePostValidateFunction(this.scrollToBottom.bind(this)); |
| return; |
| } |
| scrollbars.scrollToBottom($scrollable); |
| } |
| |
| /** |
| * Brings the widget into view by scrolling the first scrollable parent. |
| */ |
| reveal(options) { |
| if (!this.rendered) { |
| return; |
| } |
| var $scrollParent = this.$container.scrollParent(); |
| if ($scrollParent.length === 0) { |
| // No scrollable parent found -> scrolling is not possible |
| return; |
| } |
| scrollbars.scrollTo($scrollParent, this.$container, options); |
| } |
| |
| /** |
| * Visits every child of this widget in pre-order (top-down).<br> |
| * This widget itself is not visited! Only child widgets are visited recursively. |
| * <p> |
| * The children with a different parent are excluded.<br> |
| * This makes sure the child is not visited twice if the owner and the parent are not the same |
| * (in that case the widget would be in the children list of the owner and of the parent). |
| * <p> |
| * In order to abort visiting, the visitor can return true. |
| * @returns {boolean} true if the visitor aborted the visiting, false if the visiting completed without aborting |
| */ |
| visitChildren(visitor) { |
| for (var i = 0; i < this.children.length; i++) { |
| var child = this.children[i]; |
| if (child.parent === this) { |
| var treeVisitResult = visitor(child); |
| if (treeVisitResult === true || treeVisitResult === TreeVisitResult.TERMINATE) { |
| // Visitor wants to abort the visiting |
| return TreeVisitResult.TERMINATE; |
| } else if (treeVisitResult !== TreeVisitResult.SKIP_SUBTREE) { |
| treeVisitResult = child.visitChildren(visitor); |
| if (treeVisitResult === true || treeVisitResult === TreeVisitResult.TERMINATE) { |
| return TreeVisitResult.TERMINATE; |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @returns {boolean} Whether or not the widget is rendered (or rendering) and the DOM $container isAttached() |
| */ |
| isAttachedAndRendered() { |
| return (this.rendered || this.rendering) && this.$container.isAttached(); |
| } |
| |
| /* --- STATIC HELPERS ------------------------------------------------------------- */ |
| |
| /** |
| * @deprecated use {@link widgets.get} |
| */ |
| static getWidgetFor($elem) { |
| return widgets.get($elem); |
| } |
| |
| static cssClassAsArray(cssClass) { |
| var cssClasses = [], |
| cssClassesStr = cssClass || ''; |
| |
| cssClassesStr = cssClassesStr.trim(); |
| if (cssClassesStr.length > 0) { |
| cssClasses = cssClassesStr.split(' '); |
| } |
| return cssClasses; |
| } |
| } |