UNPKG

@eclipse-scout/core

Version:
377 lines (330 loc) 13 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {arrays, Desktop, DisplayChildController, DisplayParent, Form, Outline, scout} from '../index'; /** * Controller with functionality to register and render views and dialogs. * * The forms are put into the list 'views' and 'dialogs' contained in 'displayParent'. */ export class FormController extends DisplayChildController { /** * Adds the given view or dialog to this controller and renders it. * position is only used if form is a view. this position determines at which position the tab is placed. * if select view is set the view rendered in _renderView is also selected. */ override registerAndRender(form: Form, position?: number, selectView?: boolean) { scout.assertProperty(form, 'displayParent'); if (form.isPopupWindow()) { this._renderPopupWindow(form); } else if (form.isView()) { this._renderView(form, true, position, selectView); } else { this._renderDialog(form, true); } } isFormShown(form: Form): boolean { if (form.isView()) { return this.displayParent.views.includes(form); } return this.displayParent.dialogs.includes(form); } protected _renderPopupWindow(form: Form, position?: number) { throw new Error('popup window only supported by DesktopFormController'); } /** * Removes the given view or dialog from this controller and DOM. However, the form's adapter is not destroyed. That only happens once the Form is closed. */ override unregisterAndRemove(form: Form) { if (!form) { return; } if (form.isPopupWindow()) { this._removePopupWindow(form); } else if (form.isView()) { this._removeView(form); } else { this._removeDialog(form); } } protected _removePopupWindow(form: Form) { throw new Error('popup window only supported by DesktopFormController'); } /** * Renders all dialogs and views registered with this controller. */ render() { this._renderViews(); this._renderDialogs(); } protected _renderViews() { this.displayParent.views.forEach((view, position) => { view.setDisplayParent(this.displayParent); this._renderView(view, false, position, false); }); } protected _renderDialogs() { this.displayParent.dialogs.forEach(dialog => { dialog.setDisplayParent(this.displayParent); this._renderDialog(dialog, false); }); } /** * Removes all dialogs and views registered with this controller. */ remove() { this.displayParent.dialogs.forEach(dialog => { this._removeDialog(dialog, false); }); this.displayParent.views.forEach((view, position) => { this._removeView(view, false, false); }); } /** * Activates the given view or dialog. */ activateForm(form: Form) { if (!form) { this.session.desktop._setFormActivated(null); return; } // If the form has a modal child dialog, this dialog needs to be activated as well. let activatedModalDialog = false; form.dialogs.forEach(dialog => { if (dialog.modal) { this.activateForm(dialog); activatedModalDialog = true; } }); // As this._activateForm activates all display parents for a dialog the current form was already activated as it is the display parent of its modal child dialogs. if (activatedModalDialog) { return; } let displayParent: DisplayParent = this.displayParent; while (displayParent) { if (displayParent instanceof Outline) { this.session.desktop.setOutline(displayParent); break; } displayParent = displayParent instanceof Form ? displayParent.displayParent : null; } this._activateForm(form); } protected _activateForm(form: Form) { if (form.displayHint === Form.DisplayHint.VIEW) { this._activateView(form); } else { this._activateDialog(form); } this.session.desktop._setFormActivated(form); } protected _renderView(view: Form, register: boolean, position?: number, selectView?: boolean) { // Prevent "Already rendered" errors (see #162954). if (view.rendered || view.blockRendering) { return; } if (register) { this._registerView(view, position); } // Display parent may implement acceptView, if not implemented -> use default if (this.displayParent.acceptView) { if (!this.displayParent.acceptView(view)) { return; } } else if (!this.acceptView(view)) { return; } let desktop = this.session.desktop; if (desktop.displayStyle === Desktop.DisplayStyle.COMPACT && !desktop.bench) { // Show bench and hide navigation if this is the first view to be shown desktop.sendOutlineToBack(); // Don't show header if the view itself already has a header. Additionally, DesktopTabBoxController takes care of not rendering a tab if there is a view header. desktop.switchToBench(!view.headerVisible); } else if (desktop.bench.removalPending) { // If a new form should be shown while the bench is being removed because the last form was closed, schedule the rendering to make sure the bench and the new form will be opened right after the bench has been removed setTimeout(this._renderView.bind(this, view, register, position, selectView)); return; } desktop.bench.addView(view, selectView); } acceptDialog(dialog: Form): boolean { // Only render dialog if 'displayParent' is rendered yet; if not, the dialog will be rendered once 'displayParent' is rendered. return this.displayParent.rendered; } protected _renderDialog(dialog: Form, register: boolean) { // Prevent "Already rendered" errors (see #162954). if (dialog.rendered || dialog.blockRendering) { return; } if (register) { this._registerDialog(dialog); } // Display parent may implement acceptDialog, if not implemented -> use default if (this.displayParent.acceptDialog) { if (!this.displayParent.acceptDialog(dialog)) { return; } } else if (!this.acceptDialog(dialog)) { return; } let desktop = this.session.desktop; dialog.on('remove', e => { let formToActivate = this._findFormToActivateAfterDialogRemove(e.source); if (formToActivate) { desktop._setFormActivated(formToActivate); } else { desktop._setOutlineActivated(); } }); if (dialog.isPopupWindow()) { this._renderPopupWindow(dialog); } else { // start focus tracking if not already started. dialog.setTrackFocus(true); dialog.render(desktop.$container); this._layoutDialog(dialog); desktop._setFormActivated(dialog); // Only display the dialog if its 'displayParent' is visible to the user. if (!this.displayParent.inFront()) { dialog.detach(); } } } protected _findFormToActivateAfterDialogRemove(removedDialog: Form): Form { const dialogs = this.displayParent.dialogs.filter(d => d !== removedDialog); if (dialogs.length > 0) { return dialogs[dialogs.length - 1]; } if (this.displayParent instanceof Form && !this.displayParent.detailForm) { // activate display parent, but not if it is the detail form return this.displayParent; } let desktop = this.session.desktop; if (desktop.bench) { let form = desktop.bench.activeViews()[0]; if (form instanceof Form && !form.detailForm) { return form; } } } protected _removeView(view: Form, unregister?: boolean, showSiblingView?: boolean) { unregister = scout.nvl(unregister, true); if (unregister) { this._unregisterView(view); } // in COMPACT case views are already removed. if (this.session.desktop.bench) { this.session.desktop.bench.removeView(view, showSiblingView); } } protected _removeDialog(dialog: Form, unregister?: boolean) { unregister = scout.nvl(unregister, true); if (unregister) { this._unregisterDialog(dialog); } if (dialog.rendered) { dialog.remove(); } } /** @internal */ _activateView(view: Form) { let bench = this.session.desktop.bench; if (bench) { // Bench may be null (e.g. in mobile mode). This may probably only happen if the form is not really a view, because otherwise the bench would already be open. // Example: form of a FormToolButton has display style set to view but is opened as menu popup rather than in the bench. // So this null check is actually a workaround because a better solution would be to never call this function for fake views, but currently it is not possible to identify them easily. bench.activateView(view); } } protected _activateDialog(dialog: Form) { // If the display-parent is a view-form --> activate it always. // If it is another dialog --> activate it only if the dialog to activate is modal if (dialog.displayParent instanceof Form && (dialog.displayParent.displayHint === Form.DisplayHint.VIEW || (dialog.displayParent.displayHint === Form.DisplayHint.DIALOG && dialog.modal))) { this._activateForm(dialog.displayParent); } if (!dialog.rendered) { return; } let siblings = dialog.$container.nextAll().toArray(); // Now the approach is to move all eligible siblings that are in the DOM after the given dialog. // It is important not to move the given dialog itself, because this would interfere with the further handling of the // mousedown-DOM-event that triggered this function. let movableSiblings = siblings.filter(function(sibling) { // siblings of a dialog are movable if they meet the following criteria: // - they are forms (sibling forms of a dialog are always dialogs) // - they are either // - not modal // - modal // - and not a descendant of the dialog to activate // - and their display parent is not the desktop let siblingWidget = scout.widget(sibling); return siblingWidget instanceof Form && (!siblingWidget.modal || (!dialog.has(siblingWidget) && siblingWidget.displayParent !== this.session.desktop)); }, this); // All descendants of the so far determined movableSiblings are movable as well. (E.g. MessageBox, FileChooser) let movableSiblingsDescendants = siblings.filter(sibling => { return arrays.find(movableSiblings, movableSibling => { let siblingWidget = scout.widget(sibling); return !(siblingWidget instanceof Form) && // all movable forms are already captured by the filter above scout.widget(movableSibling).has(siblingWidget); }); }); movableSiblings = movableSiblings.concat(movableSiblingsDescendants); this.session.desktop.moveOverlaysBehindAndFocus(movableSiblings, dialog.$container); } /** * Attaches all dialogs to their original DOM parents. * In contrast to 'render', this method uses 'JQuery detach mechanism' to retain CSS properties, so that the model must not be interpreted anew. * * This method has no effect if already attached. */ attachDialogs() { this.displayParent.dialogs.forEach(dialog => { dialog.attach(); }); } /** * Detaches all dialogs from their DOM parents. Thereby, modality glassPanes are not detached. * In contrast to 'remove', this method uses 'JQuery detach mechanism' to retain CSS properties, so that the model must not be interpreted anew. * * This method has no effect if already detached. */ detachDialogs() { this.displayParent.dialogs.forEach(dialog => { dialog.detach(); }); } protected _layoutDialog(dialog: Form) { dialog.htmlComp.validateLayout(); dialog.position(); // If not validated anew, focus on single-button forms is not gained. // Maybe, this is the same problem as in BusyIndicator.js this.session.focusManager.validateFocus(); // Animate _after_ the layout is valid (otherwise, the position would be wrong, because // HtmlComponent defers the layout when a component is currently being animated) if (dialog.animateOpening) { dialog.$container.addClassForAnimation('animate-open'); } } protected _registerDialog(dialog: Form) { this._registerChild(dialog, 'dialogs'); } protected _unregisterDialog(dialog: Form) { this._unregisterChild(dialog, 'dialogs'); } protected _registerView(view: Form, position: number) { this._registerChild(view, 'views', position); } protected _unregisterView(view: Form) { this._unregisterChild(view, 'views'); } }