| /******************************************************************************* |
| * Copyright (c) 2014-2017 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 |
| ******************************************************************************/ |
| scout.Widget = function() { |
| 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 = scout.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._parentDestroyHandler = this._onParentDestroy.bind(this); |
| this._parentRemovingWhileAnimatingHandler = this._onParentRemovingWhileAnimating.bind(this); |
| this._scrollHandler = this._onScroll.bind(this); |
| this.events = this._createEventSupport(); |
| this.loadingSupport = this._createLoadingSupport(); |
| this.keyStrokeContext = this._createKeyStrokeContext(); |
| // Widgets using scout.LogicalGridLayout may have a grid to calculate the grid data of the children |
| this.logicalGrid; |
| }; |
| |
| /** |
| * Enum used to define different styles used when the field is disabled. |
| */ |
| scout.Widget.DisabledStyle = { |
| DEFAULT: 0, |
| READ_ONLY: 1 |
| }; |
| |
| scout.Widget.prototype.init = function(model) { |
| var staticModel = this._jsonModel(); |
| if (staticModel) { |
| model = $.extend({}, staticModel, model); |
| } |
| this._init(model); |
| this._initKeyStrokeContext(); |
| this.initialized = true; |
| this.trigger('init'); |
| }; |
| |
| /** |
| * @param options |
| * - parent (required): The parent widget |
| * - session (optional): If not specified the session of the parent is used |
| */ |
| scout.Widget.prototype._init = function(model) { |
| model = 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. |
| */ |
| scout.Widget.prototype._initProperty = function(propertyName, value) { |
| this[propertyName] = value; |
| }; |
| |
| /** |
| * Default implementation simply returns undefined. A Subclass |
| * may override this method to load or extend a JSON model with scout.models.getModel or scout.models.extend. |
| */ |
| scout.Widget.prototype._jsonModel = function() {}; |
| |
| /** |
| * Creates the widgets using the given models, or returns the widgets if the given models already are widgets. |
| * @returns an array of created widgets if models was an array. Or the created widget if models is not an array. |
| */ |
| scout.Widget.prototype._createChildren = function(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 scout.Widget simply returns the widget. |
| * |
| * @param model {Object|scout.Widget} |
| * @returns {scout.Widget} |
| */ |
| scout.Widget.prototype._createChild = function(model) { |
| if (model instanceof scout.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); |
| }; |
| |
| scout.Widget.prototype._initKeyStrokeContext = function() { |
| if (!this.keyStrokeContext) { |
| return; |
| } |
| this.keyStrokeContext.$scopeTarget = function() { |
| return this.$container; |
| }.bind(this); |
| this.keyStrokeContext.$bindTarget = function() { |
| return this.$container; |
| }.bind(this); |
| }; |
| |
| scout.Widget.prototype.destroy = function() { |
| 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. |
| */ |
| scout.Widget.prototype._destroy = function() { |
| // NOP |
| }; |
| |
| /** |
| * @param widgets may be an object or array of objects |
| */ |
| scout.Widget.prototype._destroyChildren = function(widgets) { |
| if (!widgets) { |
| return; |
| } |
| |
| widgets = scout.arrays.ensure(widgets); |
| widgets.forEach(function(widget, i) { |
| this._destroyChild(widget); |
| }, this); |
| }; |
| |
| scout.Widget.prototype._destroyChild = function(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. |
| */ |
| scout.Widget.prototype.render = function($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._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. |
| */ |
| scout.Widget.prototype._render = function() { |
| // 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. |
| */ |
| scout.Widget.prototype._renderProperties = function() { |
| 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 |
| */ |
| scout.Widget.prototype._postRender = function() { |
| var actions = this._postRenderActions; |
| this._postRenderActions = []; |
| actions.forEach(function(action) { |
| action(); |
| }); |
| }; |
| |
| scout.Widget.prototype.remove = function() { |
| 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. |
| */ |
| scout.Widget.prototype._isRemovalPrevented = function() { |
| 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) |
| */ |
| scout.Widget.prototype._isRemovalPending = function() { |
| 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; |
| }; |
| |
| scout.Widget.prototype._removeInternal = function() { |
| if (!this.rendered) { |
| return; |
| } |
| |
| $.log.isTraceEnabled() && $.log.trace('Removing widget: ' + this); |
| this.removing = true; |
| this.removalPending = false; |
| this.trigger('removing'); |
| |
| // 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); |
| 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. |
| */ |
| scout.Widget.prototype._removeAnimated = function() { |
| if (!scout.device.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); |
| }; |
| |
| scout.Widget.prototype._onParentRemovingWhileAnimating = function() { |
| this._removeInternal(); |
| }; |
| |
| scout.Widget.prototype._renderInspectorInfo = function() { |
| if (!this.session.inspector) { |
| return; |
| } |
| scout.inspector.applyInfo(this); |
| }; |
| |
| /** |
| * Links $container with the widget. |
| */ |
| scout.Widget.prototype._linkWithDOM = function() { |
| 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. |
| */ |
| scout.Widget.prototype._cleanup = function() { |
| 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); |
| } |
| }; |
| |
| scout.Widget.prototype._remove = function() { |
| if (this.$container) { |
| this.$container.remove(); |
| this.$container = null; |
| } |
| }; |
| |
| scout.Widget.prototype.setOwner = function(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); |
| }; |
| |
| scout.Widget.prototype.setParent = function(parent) { |
| scout.assertParameter('parent', parent); |
| if (parent === this.parent) { |
| return; |
| } |
| |
| 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); |
| } |
| } |
| this.parent = parent; |
| this.parent._addChild(this); |
| if (this.initialized) { |
| this.parent.recomputeEnabled(); |
| } |
| this.parent.one('destroy', this._parentDestroyHandler); |
| }; |
| |
| scout.Widget.prototype._addChild = function(child) { |
| $.log.isTraceEnabled() && $.log.trace('addChild(' + child + ') to ' + this); |
| if (this.children.indexOf(child) === -1) { |
| this.children.push(child); |
| } |
| }; |
| |
| scout.Widget.prototype._removeChild = function(child) { |
| $.log.isTraceEnabled() && $.log.trace('removeChild(' + child + ') from ' + this); |
| scout.arrays.remove(this.children, child); |
| }; |
| |
| /** |
| * @returns a list of all ancestors |
| */ |
| scout.Widget.prototype.ancestors = function() { |
| var ancestors = []; |
| var parent = this.parent; |
| while (parent) { |
| ancestors.push(parent); |
| parent = parent.parent; |
| } |
| return ancestors; |
| }; |
| |
| /** |
| * @returns true if the given widget is the same as this or a descendant |
| */ |
| scout.Widget.prototype.isOrHas = function(widget) { |
| if (widget === this) { |
| return true; |
| } |
| return this.has(widget); |
| }; |
| |
| /** |
| * @returns true if the given widget is a descendant |
| */ |
| scout.Widget.prototype.has = function(widget) { |
| while (widget) { |
| if (widget.parent === this) { |
| return true; |
| } |
| widget = widget.parent; |
| } |
| |
| return false; |
| }; |
| |
| /** |
| * @returns {scout.Form} the form the widget belongs to (returns the first parent which is a {@link scout.Form}. |
| */ |
| scout.Widget.prototype.getForm = function() { |
| return scout.Form.findForm(this); |
| }; |
| |
| /** |
| * @returns the first form which is not an inner form of a wrapped form field |
| */ |
| scout.Widget.prototype.findNonWrappedForm = function() { |
| return scout.Form.findNonWrappedForm(this); |
| }; |
| |
| /** |
| * @returns 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. |
| */ |
| scout.Widget.prototype.findDesktop = function() { |
| if (this.session.desktop) { |
| return this.session.desktop; |
| } |
| return this.findParent(function(parent) { |
| return parent instanceof scout.Desktop; |
| }); |
| }; |
| |
| /** |
| * Changes the enabled property of this form field to the given value. |
| * |
| * @param enabled |
| * Required. The new enabled value |
| * @param updateParents |
| * (optional) If true, the enabled property of all parent form fields are |
| * updated to same value as well. Default is false. |
| * @param updateChildren |
| * (optional) If true the enabled property of all child form fields (recursive) |
| * are updated to same value as well. Default is false. |
| */ |
| scout.Widget.prototype.setEnabled = function(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); |
| }); |
| } |
| }; |
| |
| scout.Widget.prototype._setEnabled = function(enabled) { |
| this._setProperty('enabled', enabled); |
| this.recomputeEnabled(); |
| }; |
| |
| scout.Widget.prototype.recomputeEnabled = function(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.setProperty('enabledComputed', enabledComputed); |
| |
| // Manually call _renderEnabled(), because _renderEnabledComputed() does not exist |
| if (this.rendered) { |
| this._renderEnabled(); // refresh |
| } |
| |
| // Propagate to children |
| if (this.children.length) { |
| var enabledComputedForChildren = this._computeEnabledForChildren(enabledComputed, parentEnabled); |
| this.children.forEach(function(child) { |
| child.recomputeEnabled(enabledComputedForChildren); |
| }); |
| } |
| }; |
| |
| scout.Widget.prototype._computeEnabled = function(inheritAccessibility, parentEnabled) { |
| return this.enabled && (inheritAccessibility ? parentEnabled : true); |
| }; |
| |
| scout.Widget.prototype._computeEnabledForChildren = function(enabledComputed, parentEnabled) { |
| return enabledComputed; |
| }; |
| |
| scout.Widget.prototype._renderEnabled = function() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.setEnabled(this.enabledComputed); |
| this._renderDisabledStyle(); |
| }; |
| |
| scout.Widget.prototype.setInheritAccessibility = function(inheritAccessibility) { |
| this.setProperty('inheritAccessibility', inheritAccessibility); |
| }; |
| |
| scout.Widget.prototype._setInheritAccessibility = function(inheritAccessibility) { |
| this._setProperty('inheritAccessibility', inheritAccessibility); |
| this.recomputeEnabled(); |
| }; |
| |
| scout.Widget.prototype.setDisabledStyle = function(disabledStyle) { |
| this.setProperty('disabledStyle', disabledStyle); |
| |
| this.children.forEach(function(child) { |
| child.setDisabledStyle(disabledStyle); |
| }); |
| }; |
| |
| scout.Widget.prototype._renderDisabledStyle = function() { |
| 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. |
| */ |
| scout.Widget.prototype._renderDisabledStyleInternal = function($element) { |
| if (!$element) { |
| return; |
| } |
| if (this.enabledComputed) { |
| $element.removeClass('read-only'); |
| } else { |
| $element.toggleClass('read-only', this.disabledStyle === scout.Widget.DisabledStyle.READ_ONLY); |
| } |
| }; |
| |
| scout.Widget.prototype.setVisible = function(visible) { |
| this.setProperty('visible', visible); |
| }; |
| |
| /** |
| * @returns whether the widget is visible or not. May depend on other conditions than the visible property only |
| */ |
| scout.Widget.prototype.isVisible = function() { |
| return this.visible; |
| }; |
| |
| scout.Widget.prototype._renderVisible = function() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.setVisible(this.isVisible()); |
| this.invalidateParentLogicalGrid(); |
| }; |
| |
| /** |
| * 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 scout.Widget.focus to set the focus to the widget. |
| */ |
| scout.Widget.prototype.setFocused = function(focused) { |
| this.setProperty('focused', focused); |
| }; |
| |
| scout.Widget.prototype._renderFocused = function() { |
| if (this.$container) { |
| this.$container.toggleClass('focused', this.focused); |
| } |
| }; |
| |
| scout.Widget.prototype._setCssClass = function(cssClass) { |
| if (this.rendered) { |
| this._removeCssClass(); |
| } |
| this._setProperty('cssClass', cssClass); |
| }; |
| |
| scout.Widget.prototype._removeCssClass = function() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.removeClass(this.cssClass); |
| }; |
| |
| scout.Widget.prototype._renderCssClass = function() { |
| if (!this.$container) { |
| return; |
| } |
| this.$container.addClass(this.cssClass); |
| }; |
| |
| scout.Widget.prototype.setCssClass = function(cssClass) { |
| this.setProperty('cssClass', cssClass); |
| }; |
| |
| scout.Widget.prototype.addCssClass = function(cssClass) { |
| var cssClasses = this.cssClassAsArray(); |
| var cssClassesToAdd = scout.Widget.cssClassAsArray(cssClass); |
| cssClassesToAdd.forEach(function(newCssClass) { |
| if (cssClasses.indexOf(newCssClass) >= 0) { |
| return; |
| } |
| cssClasses.push(newCssClass); |
| }, this); |
| this.setProperty('cssClass', scout.arrays.format(cssClasses, ' ')); |
| }; |
| |
| scout.Widget.prototype.removeCssClass = function(cssClass) { |
| var cssClasses = this.cssClassAsArray(); |
| var cssClassesToRemove = scout.Widget.cssClassAsArray(cssClass); |
| if (scout.arrays.removeAll(cssClasses, cssClassesToRemove)) { |
| this.setProperty('cssClass', scout.arrays.format(cssClasses, ' ')); |
| } |
| }; |
| |
| scout.Widget.prototype.toggleCssClass = function(cssClass, condition) { |
| if (condition) { |
| this.addCssClass(cssClass); |
| } else { |
| this.removeCssClass(cssClass); |
| } |
| }; |
| |
| scout.Widget.prototype.cssClassAsArray = function() { |
| return scout.Widget.cssClassAsArray(this.cssClass); |
| }; |
| |
| /** |
| * Creates nothing by default. If a widget needs loading support, override this method and return a loading support. |
| */ |
| scout.Widget.prototype._createLoadingSupport = function() { |
| return null; |
| }; |
| |
| scout.Widget.prototype.setLoading = function(loading) { |
| this.setProperty('loading', loading); |
| }; |
| |
| scout.Widget.prototype.isLoading = function() { |
| return this.loading; |
| }; |
| |
| scout.Widget.prototype._renderLoading = function() { |
| if (!this.loadingSupport) { |
| return; |
| } |
| this.loadingSupport.renderLoading(); |
| }; |
| |
| //--- Layouting / HtmlComponent methods --- |
| |
| scout.Widget.prototype.pack = function() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.pack(); |
| }; |
| |
| scout.Widget.prototype.invalidateLayout = function() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.invalidateLayout(); |
| }; |
| |
| scout.Widget.prototype.validateLayout = function() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.validateLayout(); |
| }; |
| |
| scout.Widget.prototype.revalidateLayout = function() { |
| 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 |
| */ |
| scout.Widget.prototype.invalidateLayoutTree = function(invalidateParents) { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.invalidateLayoutTree(invalidateParents); |
| }; |
| |
| scout.Widget.prototype.validateLayoutTree = function() { |
| if (!this.rendered || this.removing) { |
| return; |
| } |
| if (!this.htmlComp) { |
| throw new Error('Function expects a htmlComp property'); |
| } |
| this.htmlComp.validateLayoutTree(); |
| }; |
| |
| scout.Widget.prototype.revalidateLayoutTree = function(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. |
| */ |
| scout.Widget.prototype.setLayoutData = function(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. |
| */ |
| scout.Widget.prototype.validateLogicalGrid = function() { |
| 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. |
| */ |
| scout.Widget.prototype.invalidateLogicalGrid = function(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. |
| */ |
| scout.Widget.prototype.invalidateParentLogicalGrid = function(invalidateLayout) { |
| if (!this.rendered || !this.htmlComp) { |
| return; |
| } |
| this.parent.invalidateLogicalGrid(false); |
| if (scout.nvl(invalidateLayout, true)) { |
| var htmlCompParent = this.htmlComp.getParent(); |
| if (htmlCompParent) { |
| htmlCompParent.invalidateLayoutTree(); |
| } |
| } |
| }; |
| |
| scout.Widget.prototype.revalidateLogicalGrid = function(invalidateLayout) { |
| this.invalidateLogicalGrid(invalidateLayout); |
| this.validateLogicalGrid(); |
| }; |
| |
| scout.Widget.prototype.setLogicalGrid = function(logicalGrid) { |
| this.setProperty('logicalGrid', logicalGrid); |
| }; |
| |
| /** |
| * @param logicalGrid an instance of {@link scout.LogicalGrid} or a string representing the object type of a logical grid. |
| */ |
| scout.Widget.prototype._setLogicalGrid = function(logicalGrid) { |
| if (typeof logicalGrid === 'string') { |
| logicalGrid = scout.create(logicalGrid); |
| } |
| this._setProperty('logicalGrid', logicalGrid); |
| this.invalidateLogicalGrid(); |
| }; |
| |
| //--- Event handling methods --- |
| scout.Widget.prototype._createEventSupport = function() { |
| return new scout.EventSupport(); |
| }; |
| |
| scout.Widget.prototype.trigger = function(type, event) { |
| event = event || {}; |
| event.source = this; |
| this.events.trigger(type, event); |
| }; |
| |
| scout.Widget.prototype.one = function(type, func) { |
| this.events.one(type, func); |
| }; |
| |
| scout.Widget.prototype.on = function(type, func) { |
| return this.events.on(type, func); |
| }; |
| |
| scout.Widget.prototype.off = function(type, func) { |
| this.events.off(type, func); |
| }; |
| |
| scout.Widget.prototype.addListener = function(listener) { |
| this.events.addListener(listener); |
| }; |
| |
| scout.Widget.prototype.removeListener = function(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. |
| */ |
| scout.Widget.prototype.when = function(type) { |
| return this.events.when(type); |
| }; |
| |
| /** |
| * @param $element (optional) element from which the entryPoint will be resolved. If not set this.parent.$container is used. |
| * @returns the entry-point for this Widget. 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. |
| */ |
| scout.Widget.prototype.entryPoint = function($element) { |
| $element = scout.nvl($element, this.parent.$container); |
| if (!$element.length) { |
| throw new Error('Cannot resolve entryPoint, $element.length is 0 or undefined'); |
| } |
| return $element.entryPoint(); |
| }; |
| |
| scout.Widget.prototype.window = function(domElement) { |
| var $el = this.$container || this.$parent; |
| return $el ? $el.window(domElement) : (domElement ? null : $(null)); |
| }; |
| |
| scout.Widget.prototype.document = function(domElement) { |
| var $el = this.$container || this.$parent; |
| return $el ? $el.document(domElement) : (domElement ? null : $(null)); |
| }; |
| |
| /** |
| * This method attaches the detached $container to the DOM. |
| */ |
| scout.Widget.prototype.attach = function() { |
| if (this.attached || !this.rendered) { |
| return; |
| } |
| this._attach(); |
| this._afterAttach(); |
| this._triggerChildrenAfterAttach(this); |
| }; |
| |
| scout.Widget.prototype._triggerChildrenAfterAttach = function(parent) { |
| this.children.forEach(function(child) { |
| child._afterAttach(); |
| child._triggerChildrenAfterAttach(parent); |
| }); |
| }; |
| |
| scout.Widget.prototype._afterAttach = function() { |
| // NOP |
| }; |
| |
| /** |
| * Override this method to do something when Widget is attached again. Typically |
| * you will append this.$container to this.$parent. The default implementation |
| * sets this.attached to true. |
| * |
| * @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. |
| */ |
| scout.Widget.prototype._attach = function() { |
| this.attached = true; |
| }; |
| |
| /** |
| * 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 |
| */ |
| scout.Widget.prototype.detach = function() { |
| 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._triggerChildrenBeforeDetach(this); |
| this._beforeDetach(); |
| this._detach(); |
| }; |
| |
| /** |
| * Override this method to do something when Widget is detached. Typically you |
| * will call this.$container.detach() here and use the DetachHelper to store |
| * additional state (focus, scrollbars) for the detached element. The default |
| * implementation sets this.attached to false. |
| */ |
| scout.Widget.prototype._detach = function() { |
| this.attached = false; |
| }; |
| |
| scout.Widget.prototype._triggerChildrenBeforeDetach = function() { |
| this.children.forEach(function(child) { |
| child._beforeDetach(); |
| child._triggerChildrenBeforeDetach(parent); |
| }); |
| }; |
| |
| scout.Widget.prototype._beforeDetach = function(parent) { |
| // NOP |
| }; |
| |
| /** |
| * Does nothing by default. If a widget needs keystroke support override this method and return a keystroke context, e.g. the default scout.KeyStrokeContext. |
| */ |
| scout.Widget.prototype._createKeyStrokeContext = function() { |
| return null; |
| }; |
| |
| scout.Widget.prototype.updateKeyStrokes = function(newKeyStrokes, oldKeyStrokes) { |
| this.unregisterKeyStrokes(oldKeyStrokes); |
| this.registerKeyStrokes(newKeyStrokes); |
| }; |
| |
| scout.Widget.prototype.registerKeyStrokes = function(keyStrokes) { |
| this.keyStrokeContext.registerKeyStrokes(keyStrokes); |
| }; |
| |
| scout.Widget.prototype.unregisterKeyStrokes = function(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. |
| */ |
| scout.Widget.prototype.triggerPropertyChange = function(propertyName, oldValue, newValue) { |
| scout.assertParameter('propertyName', propertyName); |
| var event = new scout.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. |
| */ |
| scout.Widget.prototype._setProperty = function(propertyName, newValue) { |
| scout.assertParameter('propertyName', propertyName); |
| var oldValue = this[propertyName]; |
| if (scout.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. |
| */ |
| scout.Widget.prototype.setProperty = function(propertyName, value) { |
| if (scout.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); |
| } |
| }; |
| |
| scout.Widget.prototype._prepareProperty = function(propertyName, value) { |
| if (!this.isWidgetProperty(propertyName)) { |
| return value; |
| } |
| return this._prepareWidgetProperty(propertyName, value); |
| }; |
| |
| scout.Widget.prototype._prepareWidgetProperty = function(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 = scout.arrays.diff(oldWidgets, widgets); |
| } |
| |
| // Destroy old child widget(s) |
| if (!this.isPreserveOnPropertyChangeProperty(propertyName)) { |
| 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. |
| */ |
| scout.Widget.prototype._callRemoveProperty = function(propertyName) { |
| if (!this.isWidgetProperty(propertyName)) { |
| return; |
| } |
| if (this.isPreserveOnPropertyChangeProperty(propertyName)) { |
| return; |
| } |
| var widgets = this[propertyName]; |
| if (!widgets) { |
| return; |
| } |
| var removeFuncName = '_remove' + scout.strings.toUpperCaseFirstLetter(propertyName); |
| if (this[removeFuncName]) { |
| this[removeFuncName](); |
| } else { |
| this._internalRemoveWidgets(widgets); |
| } |
| }; |
| |
| /** |
| * Removes the given widgets |
| */ |
| scout.Widget.prototype._internalRemoveWidgets = function(widgets) { |
| widgets = scout.arrays.ensure(widgets); |
| widgets.forEach(function(widget) { |
| widget.remove(); |
| }); |
| }; |
| |
| scout.Widget.prototype._callSetProperty = function(propertyName, value) { |
| var setFuncName = '_set' + scout.strings.toUpperCaseFirstLetter(propertyName); |
| if (this[setFuncName]) { |
| this[setFuncName](value); |
| } else { |
| this._setProperty(propertyName, value); |
| } |
| }; |
| |
| scout.Widget.prototype._callRenderProperty = function(propertyName) { |
| var renderFuncName = '_render' + scout.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 |
| */ |
| scout.Widget.prototype.link = function(widgets) { |
| if (!widgets) { |
| return; |
| } |
| |
| widgets = scout.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. |
| */ |
| scout.Widget.prototype.glassPaneTargets = function(element) { |
| if (this.rendered) { |
| return this._glassPaneTargets(element); |
| } |
| |
| return scout.DeferredGlassPaneTarget.createFor(this, this._glassPaneTargets.bind(this, element)); |
| }; |
| |
| scout.Widget.prototype._glassPaneTargets = function(element) { |
| return [this.$container]; |
| }; |
| |
| scout.Widget.prototype.toString = function() { |
| var attrs = ''; |
| attrs += 'id=' + this.id; |
| attrs += ' objectType=' + this.objectType; |
| attrs += ' rendered=' + this.rendered; |
| if (this.$container) { |
| attrs += ' $container=' + scout.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. |
| */ |
| scout.Widget.prototype.ancestorsToString = function(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 str; |
| }; |
| |
| scout.Widget.prototype.resolveTextKeys = function(properties) { |
| properties.forEach(function(property) { |
| scout.texts.resolveTextProperty(this, property); |
| }, this); |
| }; |
| |
| scout.Widget.prototype.resolveIconIds = function(properties) { |
| properties.forEach(function(property) { |
| scout.icons.resolveIconProperty(this, property); |
| }, this); |
| }; |
| |
| scout.Widget.prototype.resolveConsts = function(configs) { |
| configs.forEach(function(config) { |
| scout.objects.resolveConstProperty(this, config); |
| }, this); |
| }; |
| |
| scout.Widget.prototype._addWidgetProperties = function(properties) { |
| this._addProperties('_widgetProperties', properties); |
| }; |
| |
| scout.Widget.prototype.isWidgetProperty = function(propertyName) { |
| return this._widgetProperties.indexOf(propertyName) > -1; |
| }; |
| |
| scout.Widget.prototype._addCloneProperties = function(properties) { |
| this._addProperties('_cloneProperties', properties); |
| }; |
| |
| scout.Widget.prototype.isCloneProperty = function(propertyName) { |
| return this._cloneProperties.indexOf(propertyName) > -1; |
| }; |
| |
| scout.Widget.prototype._addPreserveOnPropertyChangeProperties = function(properties) { |
| this._addProperties('_preserveOnPropertyChangeProperties', properties); |
| }; |
| |
| scout.Widget.prototype.isPreserveOnPropertyChangeProperty = function(propertyName) { |
| return this._preserveOnPropertyChangeProperties.indexOf(propertyName) > -1; |
| }; |
| |
| scout.Widget.prototype._addProperties = function(propertyName, properties) { |
| properties = scout.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); |
| }; |
| |
| scout.Widget.prototype._eachProperty = function(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); |
| } |
| }; |
| |
| scout.Widget.prototype._removeWidgetProperties = function(properties) { |
| if (Array.isArray(properties)) { |
| scout.arrays.removeAll(this._widgetProperties, properties); |
| } else { |
| scout.arrays.remove(this._widgetProperties, properties); |
| } |
| }; |
| |
| /** |
| * Clones the widget and mirrors the events, see this.clone() and this.mirror() for details. |
| */ |
| scout.Widget.prototype.cloneAndMirror = function(model) { |
| var clone = this.clone(model, { |
| delegateAllPropertiesToClone: true |
| }); |
| return clone; |
| }; |
| |
| /** |
| * @returns the original widget from which this one was cloned. If it is not a clone, itself is returned. |
| */ |
| scout.Widget.prototype.original = function() { |
| 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. |
| * |
| */ |
| scout.Widget.prototype.clone = function(model, options) { |
| var clone, cloneModel; |
| model = model || {}; |
| options = options || {}; |
| |
| cloneModel = scout.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; |
| }; |
| |
| scout.Widget.prototype._deepCloneProperties = function(clone, properties, options) { |
| if (!properties) { |
| return clone; |
| } |
| properties = scout.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); |
| }.bind(this)); |
| |
| } 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. |
| */ |
| scout.Widget.prototype.mirror = function(options, target) { |
| target = target || this.cloneOf; |
| if (!target) { |
| throw new Error('No target for mirroring.'); |
| } |
| this._mirror(target, options); |
| }; |
| |
| scout.Widget.prototype._mirror = function(clone, options) { |
| var eventDelegator = scout.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: scout.EventDelegator.create(this, clone, { |
| delegateProperties: options.delegatePropertiesToClone, |
| delegateAllProperties: options.delegateAllPropertiesToClone |
| }), |
| cloneToOriginal: scout.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)); |
| }; |
| |
| scout.Widget.prototype.unmirror = function(target) { |
| target = target || this.cloneOf; |
| if (!target) { |
| throw new Error('No target for unmirroring.'); |
| } |
| this._unmirror(target); |
| }; |
| |
| scout.Widget.prototype._unmirror = function(target) { |
| var eventDelegatorIndex = scout.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(); |
| } |
| }; |
| |
| scout.Widget.prototype._onParentDestroy = function(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); |
| }; |
| |
| scout.Widget.prototype.callSetter = function(propertyName, value) { |
| var setterFuncName = 'set' + scout.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 |
| */ |
| scout.Widget.prototype.widget = function(widgetId) { |
| return findWidgetRec(this); |
| |
| // ------ Helper functions ----- |
| |
| function findWidgetRec(widget) { |
| if (widget.id === widgetId) { |
| return widget; |
| } |
| for (var i = 0; i < widget.children.length; i++) { |
| var result = findWidgetRec(widget.children[i]); |
| if (result) { |
| return result; |
| } |
| } |
| return null; // not found |
| } |
| }; |
| |
| /** |
| * @returns the parent for which the given function returns true. |
| */ |
| scout.Widget.prototype.findParent = function(func) { |
| var parent = this.parent; |
| while (parent) { |
| if (func(parent)) { |
| return parent; |
| } |
| parent = parent.parent; |
| } |
| return parent; |
| }; |
| |
| /** |
| * 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 true if the element could be focused, false if not |
| */ |
| scout.Widget.prototype.focus = function() { |
| if (!this.rendered) { |
| this._postRenderActions.push(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. |
| */ |
| scout.Widget.prototype.focusAndPreventDefault = function(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 |
| */ |
| scout.Widget.prototype.isFocused = function() { |
| return this.rendered && scout.focusUtils.isActiveElement(this.getFocusableElement()); |
| }; |
| |
| /** |
| * @return true if the element is focusable, false if not. |
| */ |
| scout.Widget.prototype.isFocusable = function() { |
| 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]. |
| */ |
| scout.Widget.prototype.getFocusableElement = function() { |
| if (this.rendered && this.$container) { |
| return this.$container[0]; |
| } |
| return null; |
| }; |
| |
| scout.Widget.prototype._installScrollbars = function(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); |
| scout.scrollbars.install($scrollable, options); |
| $scrollable.on('scroll', this._scrollHandler); |
| }; |
| |
| scout.Widget.prototype._uninstallScrollbars = function() { |
| var $scrollable = this.get$Scrollable(); |
| if (!$scrollable || !$scrollable.data('scrollable')) { |
| return; |
| } |
| scout.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; |
| } |
| } |
| }; |
| |
| scout.Widget.prototype._onScroll = function() { |
| var $scrollable = this.get$Scrollable(); |
| this.scrollTop = $scrollable[0].scrollTop; |
| this.scrollLeft = $scrollable[0].scrollLeft; |
| }; |
| |
| scout.Widget.prototype.setScrollTop = function(scrollTop) { |
| if (this.getDelegateScrollable()) { |
| this.getDelegateScrollable().setScrollTop(scrollTop); |
| return; |
| } |
| if (this.scrollTop === scrollTop) { |
| return; |
| } |
| this.scrollTop = scrollTop; |
| if (this.rendered) { |
| this._renderScrollTop(); |
| } |
| }; |
| |
| scout.Widget.prototype._renderScrollTop = function() { |
| 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; |
| } |
| scout.scrollbars.scrollTop($scrollable, this.scrollTop); |
| }; |
| |
| scout.Widget.prototype.setScrollLeft = function(scrollLeft) { |
| if (this.getDelegateScrollable()) { |
| this.getDelegateScrollable().setScrollLeft(scrollLeft); |
| return; |
| } |
| if (this.scrollLeft === scrollLeft) { |
| return; |
| } |
| this.scrollLeft = scrollLeft; |
| if (this.rendered) { |
| this._renderScrollLeft(); |
| } |
| }; |
| |
| scout.Widget.prototype._renderScrollLeft = function() { |
| 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; |
| } |
| scout.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 {$} |
| */ |
| scout.Widget.prototype.get$Scrollable = function() { |
| 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 {scout.Widget} |
| */ |
| scout.Widget.prototype.getDelegateScrollable = function() { |
| return null; |
| }; |
| |
| scout.Widget.prototype.scrollToTop = function() { |
| 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; |
| } |
| scout.scrollbars.scrollTop($scrollable, 0); |
| }; |
| |
| scout.Widget.prototype.scrollToBottom = function() { |
| 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; |
| } |
| scout.scrollbars.scrollToBottom($scrollable); |
| }; |
| |
| /** |
| * Brings the widget into view by scrolling the first scrollable parent. |
| */ |
| scout.Widget.prototype.reveal = function() { |
| if (!this.rendered) { |
| return; |
| } |
| var $scrollParent = this.$container.scrollParent(); |
| if ($scrollParent.length === 0) { |
| // No scrollable parent found -> scrolling is not possible |
| return; |
| } |
| scout.scrollbars.scrollTo($scrollParent, this.$container); |
| }; |
| |
| /** |
| * 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). |
| */ |
| scout.Widget.prototype.visitChildren = function(visitor) { |
| this.children.forEach(function(child) { |
| if (child.parent === this) { |
| visitor(child); |
| child.visitChildren(visitor); |
| } |
| }, this); |
| }; |
| |
| /** |
| * @returns {boolean} Whether or not the widget is rendered (or rendering) and the DOM $container isAttached() |
| */ |
| scout.Widget.prototype.isAttachedAndRendered = function() { |
| return (this.rendered || this.rendering) && this.$container.isAttached(); |
| }; |
| |
| /* --- STATIC HELPERS ------------------------------------------------------------- */ |
| |
| /** |
| * @deprecated use {@link scout.widgets.get} |
| */ |
| scout.Widget.getWidgetFor = function($elem) { |
| return scout.widgets.get($elem); |
| }; |
| |
| scout.Widget.cssClassAsArray = function(cssClass) { |
| var cssClasses = [], |
| cssClassesStr = cssClass || ''; |
| |
| cssClassesStr = cssClassesStr.trim(); |
| if (cssClassesStr.length > 0) { |
| cssClasses = cssClassesStr.split(' '); |
| } |
| return cssClasses; |
| }; |