poc to support moving a dialog after a popup upon activation
The code in Iframe.js was a test to fix the losing state problem
diff --git a/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js b/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js
index 6ccfa26..262c3b8 100644
--- a/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js
+++ b/org.eclipse.scout.rt.ui.html.test/src/test/js/scout/desktop/DesktopSpec.js
@@ -56,7 +56,7 @@
expect(ntfc.fadeIn).toHaveBeenCalled();
expect(desktop.notifications.indexOf(ntfc)).toBe(0);
expect(desktop.$container.find('.desktop-notifications').length).toBe(1);
- expect(desktop.$notification).not.toBe(null);
+ expect(desktop.$notifications).not.toBe(null);
});
it('schedules addNotification when desktop is not rendered', function() {
@@ -598,10 +598,6 @@
return desktop.$container.children('.overlay-separator').nextAll().toArray();
};
- var formElt = function(form) {
- return form.$container[0];
- };
-
var widgetHtmlElements = function(forms) {
var formElts = [];
forms.forEach(function(form) {
@@ -1159,6 +1155,36 @@
expect(tabBox.currentView).toEqual(viewForm0);
expect(desktopOverlayHtmlElements()).toEqual(widgetHtmlElements([dialog0]));
});
+
+ it('brings dialog on top of a top level popup upon activation', function() {
+ expect(desktop.activeForm).toBe(null);
+
+ var dialog0 = formHelper.createFormWithOneField({
+ modal: false
+ });
+ desktop.showForm(dialog0);
+
+ var popup = scout.create('WidgetPopup', {
+ closeOnMouseDownOutside: false,
+ closeOnOtherPopupOpen: false,
+ widget: {
+ objectType: 'Label'
+ },
+ parent: desktop
+ });
+ popup.open();
+
+ // expect dialogs to be in the same order as opened
+ expect(desktopOverlayHtmlElements()).toEqual(widgetHtmlElements([dialog0, popup]));
+ expect(desktop.activeForm).toBe(dialog0);
+
+ desktop.activateForm(dialog0);
+ // expect dialog0 to be on top (= last in the DOM)
+ expect(desktopOverlayHtmlElements()).toEqual(widgetHtmlElements([popup, dialog0]));
+ expect(desktop.activeForm).toBe(dialog0);
+
+ popup.close();
+ });
});
describe('activeForm', function() {
@@ -1250,7 +1276,6 @@
var outline = outlineHelper.createOutlineWithOneDetailForm();
desktop.setOutline(outline);
outline.selectNodes(outline.nodes[0]);
- var detailForm = outline.nodes[0].detailForm;
var dialog = formHelper.createFormWithOneField({
displayHint: 'dialog'
});
@@ -1881,7 +1906,7 @@
describe('modal form', function() {
- var view1, view2, view3;
+ var view1;
beforeEach(function() {
session._renderDesktop();
diff --git a/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js b/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js
index e46f6be..910c388 100644
--- a/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js
+++ b/org.eclipse.scout.rt.ui.html/src/main/js/scout/desktop/Desktop.js
@@ -52,6 +52,7 @@
this.openUriHandler = null;
this.theme = null;
this.dense = false;
+ this._glassPaneTargetFilters = [];
this._addWidgetProperties(['viewButtons', 'menus', 'views', 'selectedViewTabs', 'dialogs', 'outline', 'messageBoxes', 'notifications', 'fileChoosers', 'addOns', 'keyStrokes', 'activeForm']);
@@ -109,6 +110,12 @@
this.openUriHandler = scout.create('OpenUriHandler', {
session: this.session
});
+ this._glassPaneTargetFilters.push(function(targetElem, element) {
+ // Exclude all child elements of the given widget
+ // Use case: element is a popup and has tooltip open. The tooltip is displayed in the desktop and considered as glass pane target by the selector above
+ var target = scout.widget(targetElem);
+ return !element.has(target);
+ });
};
/**
@@ -831,12 +838,11 @@
if (element.$container) {
$glassPaneTargets = $glassPaneTargets.not(element.$container);
}
- // Exclude all child elements of the given widget
- // Use case: element is a popup and has tooltip open. The tooltip is displayed in the desktop and considered as glass pane target by the selector above
$glassPaneTargets = $glassPaneTargets.filter(function(i, targetElem) {
- var target = scout.widget(targetElem);
- return !element.has(target);
- });
+ return this._glassPaneTargetFilters.every(function(filter) {
+ return filter(targetElem, element);
+ }, this);
+ }.bind(this));
}
var glassPaneTargets;
@@ -862,6 +868,20 @@
};
/**
+ * Adds a filter which is applied when the glass pane targets are collected.
+ * If the filter returns false, the target won't be accepted and not covered by a glass pane.
+ * @param filter a function with the parameter target and element. Target is the element which would be covered by a glass pane, element is the element the user interacts with (e.g. the modal dialog).
+ * @see _glassPaneTargets
+ */
+scout.Desktop.prototype.addGlassPaneTargetFilter = function(filter) {
+ this._glassPaneTargetFilters.push(filter);
+};
+
+scout.Desktop.prototype.removeGlassPaneTargetFilter = function(filter) {
+ scout.arrays.remove(this._glassPaneTargetFilters, filter);
+};
+
+/**
* This 'deferred' object is used because popup windows are not immediately usable when they're opened.
* That's why we must render the glass-pane of a popup window later. Which means, at the point in time
* when its $container is created and ready for usage. To avoid race conditions we must also wait until
@@ -1264,3 +1284,35 @@
clearBody: false
});
};
+
+/**
+ * Moves all the given overlays (popups, dialogs, message boxes etc.) before the target overlay and activates the focus context of the target overlay.
+ *
+ * @param overlaysToMove {HTMLElement[]} the overlays which should be moved before the target overlay
+ * @param $targetOverlay {$|HTMLElement} the overlay which should eventually be on top of the movable overlays
+ */
+scout.Desktop.prototype.moveOverlaysBehindAndFocus = function(overlaysToMove, $targetOverlay) {
+ $targetOverlay = $.ensure($targetOverlay);
+ $targetOverlay.nextAll().toArray()
+ .forEach(function(overlay) {
+ if (scout.arrays.containsAll(overlaysToMove, [overlay])) {
+ this.trigger('overlayMoveStart', {
+ overlay: overlay
+ });
+ $(overlay).insertBefore($targetOverlay);
+ this.trigger('overlayMoveEnd', {
+ overlay: overlay
+ });
+ }
+ }.bind(this));
+
+ // Activate the focus context of the form (will restore the previously focused field)
+ // This must not be done when the currently focused element is part of this dialog's DOM
+ // subtree, even if it has a separate focus context. Otherwise, the dialog would be
+ // (unnecessarily) activated, causing the current focus context to lose the focus.
+ // Example: editable table with a cell editor popup --> editor should keep the focus
+ // when the user clicks the clear icon ("x") inside the editor field.
+ if (!$targetOverlay.isOrHas($targetOverlay.activeElement())) {
+ this.session.focusManager.activateFocusContext($targetOverlay);
+ }
+};
diff --git a/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js b/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js
index f4bbb34..d426980 100644
--- a/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js
+++ b/org.eclipse.scout.rt.ui.html/src/main/js/scout/form/FormController.js
@@ -271,51 +271,41 @@
return;
}
+ var 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 triggerd this function.
- var movableSiblings = dialog.$container.nextAll().toArray()
- .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
- var siblingWidget = scout.widget(sibling);
- return siblingWidget instanceof scout.Form &&
- (!siblingWidget.modal ||
- (!dialog.has(siblingWidget) && siblingWidget.displayParent !== this.session.desktop));
- }, this);
+ // mousedown-DOM-event that triggered this function.
+ var movableSiblings = siblings.filter(function(sibling) {
+ var siblingWidget = scout.widget(sibling);
+ if (siblingWidget.displayParent === this.session.desktop && (siblingWidget.modal || siblingWidget instanceof scout.MessageBox || siblingWidget instanceof scout.FileChooser)) {
+ // Modal overlays with displayParent = desktop (= block the whole desktop) are never movable
+ // MessageBoxes and FileChooser are actually modal but don't have a modal property
+ return false;
+ }
- // All descendants of the so far determined movableSiblings are moveable as well. (E.g. MessageBox, FileChooser)
- var movableSiblingsDescendants = dialog.$container.nextAll().toArray()
- .filter(function(sibling) {
- return scout.arrays.find(movableSiblings, function(movableSibling) {
- var siblingWidget = scout.widget(sibling);
- return !(siblingWidget instanceof scout.Form) && // all movable forms are already captured by the filter above
- scout.widget(movableSibling).has(siblingWidget);
- });
+ if (siblingWidget instanceof scout.Form) {
+ // A form is movable if it is not modal
+ // Or if it is modal it must not belong to the dialog to activate
+ return !siblingWidget.modal || !dialog.has(siblingWidget);
+ }
+
+ // Accept the overlay if it is not a form and also not a child of a form (e.g. top level popups)
+ // Children of forms are collected later
+ return !siblingWidget.getForm();
+ }, this);
+
+ // All descendants of the so far determined movableSiblings are movable as well. (E.g. MessageBox, FileChooser)
+ var movableSiblingsDescendants = siblings.filter(function(sibling) {
+ return scout.arrays.find(movableSiblings, function(movableSibling) {
+ var siblingWidget = scout.widget(sibling);
+ return !(siblingWidget instanceof scout.Form) && // all movable forms are already captured by the filter above
+ scout.widget(movableSibling).has(siblingWidget);
});
+ });
movableSiblings = movableSiblings.concat(movableSiblingsDescendants);
- dialog.$container.nextAll().toArray()
- .forEach(function(sibling) {
- if (scout.arrays.containsAll(movableSiblings, [sibling])) {
- $(sibling).insertBefore(dialog.$container);
- }
- }.bind(this));
-
- // Activate the focus context of the form (will restore the previously focused field)
- // This must not be done when the currently focused element is part of this dialog's DOM
- // subtree, even if it has a separate focus context. Otherwise, the dialog would be
- // (unnecessarily) activated, causing the current focus context to lose the focus.
- // Example: editable table with a cell editor popup --> editor should keep the focus
- // when the user clicks the clear icon ("x") inside the editor field.
- if (!dialog.$container.isOrHas(dialog.$container.activeElement())) {
- this.session.focusManager.activateFocusContext(dialog.$container);
- }
+ this.session.desktop.moveOverlaysBehindAndFocus(movableSiblings, dialog.$container);
};
/**
diff --git a/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js b/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js
index 4ee7d60..5c2db36 100644
--- a/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js
+++ b/org.eclipse.scout.rt.ui.html/src/main/js/scout/iframe/IFrame.js
@@ -21,6 +21,7 @@
this.wrapIframe = scout.device.isIosPlatform();
this.$iframe = null;
this._loadHandler = this._onLoad.bind(this);
+ this.mutationObserver = null;
};
scout.inherits(scout.IFrame, scout.Widget);
@@ -33,6 +34,25 @@
this.$container = this.$iframe;
}
this.htmlComp = scout.HtmlComponent.install(this.$container, this.session);
+
+ this.$iframe.one('remove', function() {
+ if (!this.rendered || this.removing) {
+ return;
+ }
+ this.mutationObserver = new MutationObserver(this._onDomMutation.bind(this));
+ this.mutationObserver.observe(this.$iframe.document(true), {
+ subtree: true,
+ childList: true
+ });
+ }.bind(this));
+};
+
+scout.IFrame.prototype._remove = function() {
+ if (this.mutationObserver) {
+ this.mutationObserver.disconnect();
+ this.mutationObserver = null;
+ }
+ scout.IFrame.parent.prototype._remove.call(this);
};
/**
@@ -69,6 +89,22 @@
}
};
+scout.IFrame.prototype._onDomMutation = function(mutationList) {
+ mutationList.forEach(function(mutation) {
+ for (var i = 0; i < mutation.addedNodes.length; i++) {
+ var elem = mutation.addedNodes[i];
+ var $elem = $(elem);
+ if ($elem.isOrHas(this.$iframe)) {
+ this._onNodeAdded();
+ }
+ }
+ }, this);
+};
+
+scout.IFrame.prototype._onDomMutation = function() {
+ this._renderLocation();
+};
+
scout.IFrame.prototype._onLoad = function(event) {
if (!this.rendered) { // check needed, because this is an async callback
return;