blob: 17d6dfa8159d94613be46c1f35cb4c607fe8b5b4 [file] [log] [blame]
/*
* Copyright (c) 2014-2020 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,
BenchColumnLayoutData,
cookies,
DeferredGlassPaneTarget,
DesktopLayout,
DesktopNavigation,
Device,
DisableBrowserF5ReloadKeyStroke,
DisableBrowserTabSwitchingKeyStroke,
Event,
FileChooserController,
Form,
HtmlComponent,
HtmlEnvironment,
KeyStrokeContext,
MessageBoxController,
objects,
Outline,
scout,
strings,
styles,
Tree,
URL,
webstorage,
Widget,
widgets
} from '../index';
import * as $ from 'jquery';
export default class Desktop extends Widget {
constructor() {
super();
this.desktopStyle = Desktop.DisplayStyle.DEFAULT;
this.title = null;
this.selectViewTabsKeyStrokesEnabled = true;
this.selectViewTabsKeyStrokeModifier = 'control';
this.cacheSplitterPosition = true;
this.browserHistoryEntry = null;
this.logoId = null;
this.navigationVisible = true;
this.navigationHandleVisible = true;
this.logoActionEnabled = false;
this.benchVisible = true;
this.headerVisible = true;
this.geolocationServiceAvailable = Device.get().supportsGeolocation();
this.benchLayoutData = null;
this.menus = [];
this.addOns = [];
this.dialogs = [];
this.views = [];
this.keyStrokes = [];
this.viewButtons = [];
this.messageBoxes = [];
this.fileChoosers = [];
this.outline = null;
this.activeForm = null;
this.selectedViewTabs = [];
this.notifications = [];
this.navigation = null;
this.header = null;
this.bench = null;
this.splitter = null;
this.splitterVisible = false;
this.formController = null;
this.messageBoxController = null;
this.fileChooserController = null;
this.initialFormRendering = false;
this.offline = false;
this.inBackground = false;
this.openUriHandler = null;
this.theme = null;
this.dense = false;
this._glassPaneTargetFilters = [];
this.url = null;
this._addWidgetProperties(['viewButtons', 'menus', 'views', 'selectedViewTabs', 'dialogs', 'outline', 'messageBoxes', 'notifications', 'fileChoosers', 'addOns', 'keyStrokes', 'activeForm']);
// event listeners
this._benchActiveViewChangedHandler = this._onBenchActivateViewChanged.bind(this);
}
static DisplayStyle = {
DEFAULT: 'default',
BENCH: 'bench',
COMPACT: 'compact'
};
static UriAction = {
DOWNLOAD: 'download',
OPEN: 'open',
NEW_WINDOW: 'newWindow',
POPUP_WINDOW: 'popupWindow',
SAME_WINDOW: 'sameWindow'
};
static DEFAULT_THEME = 'default';
_init(model) {
// Note: session and desktop are tightly coupled. Because a lot of widgets want to register
// a listener on the desktop in their init phase, they access the desktop by calling 'this.session.desktop'
// that's why we need this instance as early as possible. When that happens they access a desktop which is
// not yet fully initialized. But anyway, it's already possible to attach a listener, for instance.
// Because of this line of code here, we don't have to set the variable in App.js, after the desktop has been
// created. Also note that Scout Java uses a different pattern to solve the same problem, there a VirtualDesktop
// is used during initialization. When initialization is done, all registered listeners on the virtual desktop
// are copied to the real desktop instance.
var session = model.session || model.parent.session;
session.desktop = this;
super._init(model);
this.url = new URL();
this._initTheme();
this.formController = scout.create('DesktopFormController', {
displayParent: this,
session: this.session
});
this.messageBoxController = new MessageBoxController(this, this.session);
this.fileChooserController = new FileChooserController(this, this.session);
this._resizeHandler = this.onResize.bind(this);
this._popstateHandler = this.onPopstate.bind(this);
this.updateSplitterVisibility();
this.resolveTextKeys(['title']);
this._setViews(this.views);
this._setViewButtons(this.viewButtons);
this._setMenus(this.menus);
this._setKeyStrokes(this.keyStrokes);
this._setBenchLayoutData(this.benchLayoutData);
this._setDisplayStyle(this.displayStyle);
this._setDense(this.dense);
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);
});
}
/**
* @override
*/
_createKeyStrokeContext() {
return new KeyStrokeContext();
}
/**
* @override
*/
_initKeyStrokeContext() {
super._initKeyStrokeContext();
this.keyStrokeContext.invokeAcceptInputOnActiveValueField = true;
this.keyStrokeContext.registerKeyStroke([
new DisableBrowserF5ReloadKeyStroke(this),
new DisableBrowserTabSwitchingKeyStroke(this)
]);
}
_onBenchActivateViewChanged(event) {
if (this.initialFormRendering) {
return;
}
var view = event.view;
if (view instanceof Form && this.bench.outlineContent !== view && !view.detailForm) {
// Notify model that this form is active (only for regular views, not detail forms)
this._setFormActivated(view);
}
}
_render() {
this.$container = this.$parent;
this.$container.addClass('desktop');
this.htmlComp = HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(this._createLayout());
// Attach resize listener before other elements can add their own resize listener (e.g. an addon) to make sure it is executed first
this.$container.window()
.on('resize', this._resizeHandler)
.on('popstate', this._popstateHandler);
// Desktop elements are added before this separator, all overlays are opened after (dialogs, popups, tooltips etc.)
this.$overlaySeparator = this.$container.appendDiv('overlay-separator').setVisible(false);
this._renderNavigationVisible();
this._renderHeaderVisible();
this._renderBenchVisible();
this._renderTitle();
this._renderLogoUrl();
this._renderSplitterVisible();
this._renderInBackground();
this._renderNavigationHandleVisible();
this._renderNotifications();
this._renderBrowserHistoryEntry();
this._renderDense();
this.addOns.forEach(function(addOn) {
addOn.render();
}, this);
// prevent general drag and drop, dropping a file anywhere in the application must not open this file in browser
this._setupDragAndDrop();
this._disableContextMenu();
}
_remove() {
this.formController.remove();
this.messageBoxController.remove();
this.fileChooserController.remove();
this.$container.window()
.off('resize', this._resizeHandler)
.off('popstate', this._popstateHandler);
super._remove();
}
_postRender() {
super._postRender();
// Render attached forms, message boxes and file choosers.
this.initialFormRendering = true;
this._renderDisplayChildrenOfOutline();
this.formController.render();
this.messageBoxController.render();
this.fileChooserController.render();
this.initialFormRendering = false;
}
_setDisplayStyle(displayStyle) {
this._setProperty('displayStyle', displayStyle);
var isCompact = this.displayStyle === Desktop.DisplayStyle.COMPACT;
if (this.header) {
this.header.setToolBoxVisible(!isCompact);
this.header.animateRemoval = isCompact;
}
if (this.navigation) {
this.navigation.setToolBoxVisible(isCompact);
this.navigation.htmlComp.layoutData.fullWidth = isCompact;
}
if (this.bench) {
this.bench.setOutlineContentVisible(!isCompact);
}
if (this.outline) {
this.outline.setCompact(isCompact);
this.outline.setEmbedDetailContent(isCompact);
}
}
setDense(dense) {
this.setProperty('dense', dense);
}
_setDense(dense) {
this._setProperty('dense', dense);
styles.clearCache();
HtmlEnvironment.get().init(this.dense ? 'dense' : null);
}
_renderDense() {
this.$container.toggleClass('dense', this.dense);
}
_createLayout() {
return new DesktopLayout(this);
}
/**
* Displays attached forms, message boxes and file choosers.
* Outline does not need to be rendered to show the child elements, it needs to be active (necessary if navigation is invisible)
*/
_renderDisplayChildrenOfOutline() {
if (!this.outline) {
return;
}
this.outline.formController.render();
this.outline.messageBoxController.render();
this.outline.fileChooserController.render();
if (this.outline.selectedViewTabs) {
this.outline.selectedViewTabs.forEach(function(selectedView) {
this.formController._activateView(selectedView);
}.bind(this));
}
}
_removeDisplayChildrenOfOutline() {
if (!this.outline) {
return;
}
this.outline.formController.remove();
this.outline.messageBoxController.remove();
this.outline.fileChooserController.remove();
}
computeParentForDisplayParent(displayParent) {
// Outline must not be used as parent, otherwise the children (form, messageboxes etc.) would be removed if navigation is made invisible
// The functions _render/removeDisplayChildrenOfOutline take care that the elements are correctly rendered/removed on an outline switch
var parent = displayParent;
if (displayParent instanceof Outline) {
parent = this;
}
return parent;
}
_renderTitle() {
var title = this.title;
if (title === undefined || title === null) {
return;
}
var $scoutDivs = $('div.scout');
if ($scoutDivs.length <= 1) { // only set document title in non-portlet case
$scoutDivs.document(true).title = title;
}
}
_renderActiveForm() {
// NOP -> is handled in _setFormActivated when ui changes active form or if model changes form in _onFormShow/_onFormActivate
}
_renderBench() {
if (this.bench) {
return;
}
this.bench = scout.create('DesktopBench', {
parent: this,
animateRemoval: true,
headerTabArea: this.header ? this.header.tabArea : undefined,
outlineContentVisible: this.displayStyle !== Desktop.DisplayStyle.COMPACT
});
this.bench.on('viewActivate', this._benchActiveViewChangedHandler);
this.bench.render();
this.bench.$container.insertBefore(this.$overlaySeparator);
this.invalidateLayoutTree();
}
_removeBench() {
if (!this.bench) {
return;
}
this.bench.off('viewActivate', this._benchActiveViewChangedHandler);
this.bench.on('destroy', function() {
this.bench = null;
this.invalidateLayoutTree();
}.bind(this));
this.bench.destroy();
}
_renderBenchVisible() {
this.animateLayoutChange = this.rendered;
if (this.benchVisible) {
this._renderBench();
this._renderInBackground();
} else {
this._removeBench();
}
}
_renderNavigation() {
if (this.navigation) {
return;
}
this.navigation = scout.create('DesktopNavigation', {
parent: this,
outline: this.outline,
toolBoxVisible: this.displayStyle === Desktop.DisplayStyle.COMPACT,
layoutData: {
fullWidth: this.displayStyle === Desktop.DisplayStyle.COMPACT
}
});
this.navigation.render();
this.navigation.$container.prependTo(this.$container);
this.invalidateLayoutTree();
}
_removeNavigation() {
if (!this.navigation) {
return;
}
this.navigation.destroy();
this.navigation = null;
this.invalidateLayoutTree();
}
_renderNavigationVisible() {
this.animateLayoutChange = this.rendered;
if (this.navigationVisible) {
this._renderNavigation();
} else {
if (!this.animateLayoutChange) {
this._removeNavigation();
} else {
// re layout to trigger animation
this.invalidateLayoutTree();
}
}
}
_renderHeader() {
if (this.header) {
return;
}
this.header = scout.create('DesktopHeader', {
parent: this,
logoUrl: this.logoUrl,
animateRemoval: this.displayStyle === Desktop.DisplayStyle.COMPACT,
toolBoxVisible: this.displayStyle !== Desktop.DisplayStyle.COMPACT
});
this.header.render();
if (this.navigation && this.navigation.rendered) {
this.header.$container.insertAfter(this.navigation.$container);
} else {
this.header.$container.prependTo(this.$container);
}
// register header tab area
if (this.bench) {
this.bench._setTabArea(this.header.tabArea);
}
this.invalidateLayoutTree();
}
_removeHeader() {
if (!this.header) {
return;
}
this.header.on('destroy', function() {
this.invalidateLayoutTree();
this.header = null;
}.bind(this));
this.header.destroy();
}
_renderHeaderVisible() {
if (this.headerVisible) {
this._renderHeader();
} else {
this._removeHeader();
}
}
_renderLogoUrl() {
if (this.header) {
this.header.setLogoUrl(this.logoUrl);
}
}
_renderSplitterVisible() {
if (this.splitterVisible) {
this._renderSplitter();
} else {
this._removeSplitter();
}
}
_renderSplitter() {
if (this.splitter || !this.navigation) {
return;
}
this.splitter = scout.create('Splitter', {
parent: this,
$anchor: this.navigation.$container,
$root: this.$container
});
this.splitter.render();
this.splitter.$container.insertBefore(this.$overlaySeparator);
this.splitter.on('move', this._onSplitterMove.bind(this));
this.splitter.on('moveEnd', this._onSplitterMoveEnd.bind(this));
this.splitter.on('positionChange', this._onSplitterPositionChange.bind(this));
this.updateSplitterPosition();
}
_removeSplitter() {
if (!this.splitter) {
return;
}
this.splitter.destroy();
this.splitter = null;
}
_renderInBackground() {
if (this.bench) {
this.bench.$container.toggleClass('drop-shadow', this.inBackground);
}
}
_renderBrowserHistoryEntry() {
if (!Device.get().supportsHistoryApi()) {
return;
}
var myWindow = this.$container.window(true),
history = this.browserHistoryEntry;
if (history) {
var historyPath = this._createHistoryPath(history);
var setStateFunc = (this.rendered ? myWindow.history.pushState : myWindow.history.replaceState).bind(myWindow.history);
setStateFunc({
deepLinkPath: history.deepLinkPath
}, history.title, historyPath);
}
}
/**
* Takes the history.path provided by the browserHistoryEvent and appends additional URL parameters.
*/
_createHistoryPath(history) {
if (!history.pathVisible) {
return '';
}
var historyPath = history.path;
var cloneUrl = this.url.clone();
cloneUrl.removeParameter('dl');
cloneUrl.removeParameter('i');
if (objects.countOwnProperties(cloneUrl.parameterMap) > 0) {
var pathUrl = new URL(historyPath);
for (var paramName in cloneUrl.parameterMap) {
pathUrl.addParameter(paramName, cloneUrl.getParameter(paramName));
}
historyPath = pathUrl.toString({alwaysFirst: ['dl', 'i']});
}
return historyPath;
}
_setupDragAndDrop() {
var dragEnterOrOver = function(event) {
event.stopPropagation();
event.preventDefault();
// change cursor to forbidden (no dropping allowed)
event.originalEvent.dataTransfer.dropEffect = 'none';
};
this.$container.on('dragenter', dragEnterOrOver);
this.$container.on('dragover', dragEnterOrOver);
this.$container.on('drop', function(event) {
event.stopPropagation();
event.preventDefault();
});
}
updateSplitterVisibility() {
// Splitter should only be visible if navigation and bench are visible, but never in compact mode (to prevent unnecessary splitter rendering)
this.setSplitterVisible(this.navigationVisible && this.benchVisible && this.displayStyle !== Desktop.DisplayStyle.COMPACT);
}
setSplitterVisible(visible) {
this.setProperty('splitterVisible', visible);
}
updateSplitterPosition() {
if (!this.splitter) {
return;
}
var storedSplitterPosition = this.cacheSplitterPosition && this._loadCachedSplitterPosition();
if (storedSplitterPosition) {
// Restore splitter position
var splitterPosition = parseInt(storedSplitterPosition, 10);
this.splitter.setPosition(splitterPosition);
this.invalidateLayoutTree();
} else {
// Set initial splitter position (default defined by css)
this.splitter.setPosition();
this.invalidateLayoutTree();
}
}
_disableContextMenu() {
// Switch off browser's default context menu for the entire scout desktop (except input fields)
this.$container.on('contextmenu', function(event) {
if (event.target.nodeName !== 'INPUT' && event.target.nodeName !== 'TEXTAREA' && !event.target.isContentEditable) {
event.preventDefault();
}
});
}
setOutline(outline) {
if (this.outline === outline) {
return;
}
try {
if (this.bench) {
this.bench.setChanging(true);
}
if (this.rendered) {
this._removeDisplayChildrenOfOutline();
}
this.outline = outline;
this._setDisplayStyle(this.displayStyle);
this._setOutlineActivated();
if (this.navigation) {
this.navigation.setOutline(this.outline);
}
// call render after triggering event so glasspane rendering taking place can refer to the current outline content
this.trigger('outlineChange');
if (this.rendered) {
this._renderDisplayChildrenOfOutline();
}
} finally {
if (this.bench) {
this.bench.setChanging(false);
}
}
}
_setViews(views) {
if (views) {
views.forEach(function(view) {
view.setDisplayParent(this);
}.bind(this));
}
this._setProperty('views', views);
}
_setViewButtons(viewButtons) {
this.updateKeyStrokes(viewButtons, this.viewButtons);
this._setProperty('viewButtons', viewButtons);
}
setMenus(menus) {
if (this.header) {
this.header.setMenus(menus);
}
}
_setMenus(menus) {
this.updateKeyStrokes(menus, this.menus);
this._setProperty('menus', menus);
}
_setKeyStrokes(keyStrokes) {
this.updateKeyStrokes(keyStrokes, this.keyStrokes);
this._setProperty('keyStrokes', keyStrokes);
}
setNavigationHandleVisible(visible) {
this.setProperty('navigationHandleVisible', visible);
}
_renderNavigationHandleVisible() {
this.$container.toggleClass('has-navigation-handle', this.navigationHandleVisible);
}
setNavigationVisible(visible) {
this.setProperty('navigationVisible', visible);
this.updateSplitterVisibility();
}
setBenchVisible(visible) {
this.setProperty('benchVisible', visible);
this.updateSplitterVisibility();
}
setHeaderVisible(visible) {
this.setProperty('headerVisible', visible);
}
_setBenchLayoutData(layoutData) {
layoutData = BenchColumnLayoutData.ensure(layoutData);
this._setProperty('benchLayoutData', layoutData);
}
_setInBackground(inBackground) {
this._setProperty('inBackground', inBackground);
}
outlineDisplayStyle() {
if (this.outline) {
return this.outline.displayStyle;
}
}
shrinkNavigation() {
if (this.outline.toggleBreadcrumbStyleEnabled && this.navigationVisible &&
this.outlineDisplayStyle() === Tree.DisplayStyle.DEFAULT) {
this.outline.setDisplayStyle(Tree.DisplayStyle.BREADCRUMB);
} else {
this.setNavigationVisible(false);
}
}
enlargeNavigation() {
if (this.navigationVisible && this.outlineDisplayStyle() === Tree.DisplayStyle.BREADCRUMB) {
this.outline.setDisplayStyle(Tree.DisplayStyle.DEFAULT);
} else {
this.setNavigationVisible(true);
// Layout immediately to have view tabs positioned correctly before animation starts
this.validateLayoutTree();
}
}
switchToBench() {
this.setHeaderVisible(true);
this.setBenchVisible(true);
this.setNavigationVisible(false);
}
switchToNavigation() {
this.setNavigationVisible(true);
this.setHeaderVisible(false);
this.setBenchVisible(false);
}
revalidateHeaderLayout() {
if (this.header) {
this.header.revalidateLayout();
}
}
goOffline() {
if (this.offline) {
return;
}
this.offline = true;
this._removeOfflineNotification();
this._offlineNotification = scout.create('DesktopNotification:Offline', {
parent: this
});
this._offlineNotification.show();
}
goOnline() {
this._removeOfflineNotification();
}
_removeOfflineNotification() {
if (this._offlineNotification) {
setTimeout(this.removeNotification.bind(this, this._offlineNotification), 3000);
this._offlineNotification = null;
}
}
addNotification(notification) {
if (!notification) {
return;
}
this.notifications.push(notification);
if (this.rendered) {
this._renderNotification(notification);
}
}
_renderNotification(notification) {
if (this.$notifications) {
// Bring to front
this.$notifications.appendTo(this.$container);
} else {
this.$notifications = this.$container.appendDiv('desktop-notifications');
}
notification.fadeIn(this.$notifications);
if (notification.duration > 0) {
notification.removeTimeout = setTimeout(notification.hide.bind(notification), notification.duration);
notification.one('remove', function() {
this.removeNotification(notification);
}.bind(this));
}
}
_renderNotifications() {
this.notifications.forEach(function(notification) {
this._renderNotification(notification);
}.bind(this));
}
/**
* Removes the given notification.
* @param notification Either an instance of DesktopNavigation or a String containing an ID of a notification instance.
*/
removeNotification(notification) {
if (typeof notification === 'string') {
var notificationId = notification;
notification = arrays.find(this.notifications, function(n) {
return notificationId === n.id;
});
}
if (!notification) {
return;
}
if (notification.removeTimeout) {
clearTimeout(notification.removeTimeout);
}
arrays.remove(this.notifications, notification);
if (!this.rendered) {
return;
}
if (this.$notifications) {
notification.fadeOut();
notification.one('remove', this._onNotificationRemove.bind(this, notification));
}
}
getPopupsFor(widget) {
var popups = [];
this.$container.children('.popup').each(function(i, elem) {
var $popup = $(elem),
popup = widgets.get($popup);
if (widget.has(popup)) {
popups.push(popup);
}
});
return popups;
}
/**
* Destroys every popup which is a descendant of the given widget.
*/
destroyPopupsFor(widget) {
this.getPopupsFor(widget).forEach(function(popup) {
popup.destroy();
});
}
openUri(uri, action) {
if (!this.rendered) {
this._postRenderActions.push(this.openUri.bind(this, uri, action));
return;
}
this.openUriHandler.openUri(uri, action);
}
bringOutlineToFront() {
if (!this.rendered) {
this._postRenderActions.push(this.bringOutlineToFront.bind(this));
return;
}
if (!this.inBackground || this.displayStyle === Desktop.DisplayStyle.BENCH) {
return;
}
this._setInBackground(false);
this._setOutlineActivated();
if (this.navigationVisible) {
this.navigation.bringToFront();
}
if (this.benchVisible) {
this.bench.bringToFront();
}
if (this.headerVisible) {
this.header.bringToFront();
}
this._renderInBackground();
}
sendOutlineToBack() {
if (this.inBackground) {
return;
}
this._setInBackground(true);
if (this.navigationVisible) {
this.navigation.sendToBack();
}
if (this.benchVisible) {
this.bench.sendToBack();
}
if (this.headerVisible) {
this.header.sendToBack();
}
this._renderInBackground();
}
/**
* === Method required for objects that act as 'displayParent' ===
*
* Returns 'true' if the Desktop is currently accessible to the user.
*/
inFront() {
return true; // Desktop is always available to the user.
}
/**
* === Method required for objects that act as 'displayParent' ===
*
* Returns the DOM elements to paint a glassPanes over, once a modal Form, message-box, file-chooser or wait-dialog is showed with the Desktop as its 'displayParent'.
*/
_glassPaneTargets(element) {
// Do not return $container, because this is the parent of all forms and message boxes. Otherwise, no form could gain focus, even the form requested desktop modality.
var $glassPaneTargets = this.$container
.children()
.not('.splitter') // exclude splitter to be locked
.not('.desktop-notifications') // exclude notification box like 'connection interrupted' to be locked
.not('.overlay-separator'); // exclude overlay separator (marker element)
if (element) {
if (element.$container) {
$glassPaneTargets = $glassPaneTargets.not(element.$container);
}
$glassPaneTargets = $glassPaneTargets.filter(function(i, targetElem) {
return this._glassPaneTargetFilters.every(function(filter) {
return filter(targetElem, element);
}, this);
}.bind(this));
}
var glassPaneTargets;
if (element instanceof Form && element.displayHint === Form.DisplayHint.VIEW) {
$glassPaneTargets = $glassPaneTargets
.not('.desktop-bench')
.not('.desktop-header');
if (this.header && this.header.toolBox && this.header.toolBox.$container) {
$glassPaneTargets.push(this.header.toolBox.$container);
}
glassPaneTargets = $.makeArray($glassPaneTargets);
arrays.pushAll(glassPaneTargets, this._getBenchGlassPaneTargetsForView(element));
} else {
glassPaneTargets = $.makeArray($glassPaneTargets);
}
// When a popup-window is opened its container must also be added to the result
this._pushPopupWindowGlassPaneTargets(glassPaneTargets, element);
return glassPaneTargets;
}
/**
* 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
*/
addGlassPaneTargetFilter(filter) {
this._glassPaneTargetFilters.push(filter);
}
removeGlassPaneTargetFilter(filter) {
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
* the glass pane renderer is ready. Only when both conditions are fullfilled, we can render the glass
* pane.
*/
_deferredGlassPaneTarget(popupWindow) {
var deferred = new DeferredGlassPaneTarget();
popupWindow.one('init', function() {
deferred.ready([popupWindow.$container]);
});
return deferred;
}
_getBenchGlassPaneTargetsForView(view) {
var $glassPanes = [];
$glassPanes = $glassPanes.concat(this._getTabGlassPaneTargetsForView(view, this.header));
if (this.bench) {
this.bench.visibleTabBoxes().forEach(function(tabBox) {
if (!tabBox.rendered) {
return;
}
if (tabBox.hasView(view)) {
$glassPanes = $glassPanes.concat(this._getTabGlassPaneTargetsForView(view, tabBox));
} else {
$glassPanes.push(tabBox.$container);
}
}, this);
}
return $glassPanes;
}
_getTabGlassPaneTargetsForView(view, tabBox) {
var $glassPanes = [];
if (tabBox && tabBox.tabArea) {
tabBox.tabArea.tabs.forEach(function(tab) {
if (tab.view !== view) {
$glassPanes.push(tab.$container);
// Workaround for javascript not being able to prevent hover event propagation:
// In case of tabs, the hover selector is defined on the element that is the direct parent
// of the glass pane. Under these circumstances, the hover style isn't be prevented by the glass pane.
tab.$container.addClass('no-hover');
}
});
}
return $glassPanes;
}
_pushPopupWindowGlassPaneTargets(glassPaneTargets, element) {
this.formController._popupWindows.forEach(function(popupWindow) {
if (element === popupWindow.form) {
// Don't block form itself
return;
}
glassPaneTargets.push(popupWindow.initialized ?
popupWindow.$container[0] : this._deferredGlassPaneTarget(popupWindow));
}, this);
}
showForm(form, position) {
var displayParent = form.displayParent || this;
form.setDisplayParent(displayParent);
this._setFormActivated(form);
// register listener to recover active form when child dialog is removed
displayParent.formController.registerAndRender(form, position, true);
}
hideForm(form) {
if (!form.displayParent) {
// showForm has probably never been called -> nothing to do here
// May happen if form.close() is called immediately after form.open() without waiting for the open promise to resolve
// Hint: it is not possible to check whether the form is rendered and then return (which would be the obvious thing to do).
// Reason: Forms in popup windows are removed before getting closed, see DesktopFormController._onPopupWindowUnload
return;
}
if (this.displayStyle === Desktop.DisplayStyle.COMPACT && form.isView() && this.benchVisible) {
var openViews = this.bench.getViews().slice();
arrays.remove(openViews, form);
if (openViews.length === 0) {
// Hide bench and show navigation if this is the last view to be hidden
this.switchToNavigation();
}
}
form.displayParent.formController.unregisterAndRemove(form);
if (!this.benchVisible || this.bench.getViews().length === 0) {
// Bring outline to front if last view has been closed,
// even if bench is invisible (compact case) to update state correctly and reshow elements (dialog etc.) linked to the outline
this.bringOutlineToFront();
}
}
activateForm(form) {
var displayParent = form.displayParent || this;
displayParent.formController.activateForm(form);
this._setFormActivated(form);
// If the form has a modal child dialog, this dialog needs to be activated as well.
form.dialogs.forEach(function(dialog) {
if (dialog.modal) {
this.activateForm(dialog);
}
}, this);
}
_setOutlineActivated() {
this._setFormActivated(null);
if (this.outline) {
this.outline.activateCurrentPage();
}
}
_setFormActivated(form) {
// If desktop is in rendering process the can not set a new active form. instead the active form from the model is set selected.
if (!this.rendered || this.initialFormRendering) {
return;
}
if (this.activeForm === form) {
return;
}
this.activeForm = form;
if (!form) {
// no form is activated -> show outline
this.bringOutlineToFront();
} else if (form.displayHint === Form.DisplayHint.VIEW && !form.detailForm && this.bench && this.bench.hasView(form)) {
// view form was activated. send the outline to back to ensure the form is attached
// exclude detail forms even though detail forms usually are not activated
// Also only consider "real" views used in the bench and ignore other views (e.g. used in a form menu)
this.sendOutlineToBack();
}
this.triggerFormActivate(form);
}
triggerFormActivate(form) {
this.trigger('formActivate', {
form: form
});
}
cancelViews(forms) {
var event = new Event();
event.forms = forms;
this.trigger('cancelForms', event);
if (!event.defaultPrevented) {
this._cancelViews(forms);
}
}
_cancelViews(forms) {
// do not cancel forms when the form child hierarchy does not get canceled.
forms = forms.filter(function(form) {
return !arrays.find(form.views, function(view) {
return view.modal;
});
});
// if there's only one form simply cancel it directly
if (forms.length === 1) {
forms[0].cancel();
return;
}
// collect all forms in the display child hierarchy with unsaved changes.
var unsavedForms = forms.filter(function(form) {
var requiresSaveChildDialogs = false;
form.visitDisplayChildren(function(dialog) {
if (dialog.lifecycle.requiresSave()) {
requiresSaveChildDialogs = true;
}
}, function(displayChild) {
return displayChild instanceof Form;
});
return form.lifecycle.requiresSave() || requiresSaveChildDialogs;
});
// initialize with a resolved promise in case there are no unsaved forms.
var waitFor = $.resolvedPromise();
if (unsavedForms.length > 0) {
var unsavedFormChangesForm = scout.create('scout.UnsavedFormChangesForm', {
parent: this,
session: this.session,
displayParent: this,
unsavedForms: unsavedForms
});
unsavedFormChangesForm.open();
// promise that is resolved when the UnsavedFormChangesForm is stored
waitFor = unsavedFormChangesForm.whenSave().then(function() {
var formsToSave = unsavedFormChangesForm.openFormsField.value;
formsToSave.forEach(function(form) {
form.visitDisplayChildren(function(dialog) {
// forms should be stored with ok(). Other display children can simply be closed.
if (dialog instanceof Form) {
dialog.ok();
} else {
dialog.close();
}
});
form.ok();
});
return formsToSave;
});
}
waitFor.then(function(formsToSave) {
if (formsToSave) {
// already saved & closed forms (handled by the UnsavedFormChangesForm)
arrays.removeAll(forms, formsToSave);
}
// close the remaining forms that don't require saving.
forms.forEach(function(form) {
form.visitDisplayChildren(function(dialog) {
dialog.close();
});
form.close();
});
});
}
/**
* Called when the animation triggered by animationLayoutChange is complete (e.g. navigation or bench got visible/invisible)
*/
onLayoutAnimationComplete() {
if (!this.headerVisible) {
this._removeHeader();
}
if (!this.navigationVisible) {
this._removeNavigation();
}
if (!this.benchVisible) {
this._removeBench();
}
this.trigger('animationEnd');
this.animateLayoutChange = false;
}
onLayoutAnimationStep() {
this.repositionTooltips();
}
onResize(event) {
this.revalidateLayoutTree();
}
resetPopstateHandler() {
this.setPopstateHandler(this.onPopstate.bind(this));
}
setPopstateHandler(handler) {
if (this.rendered || this.rendering) {
var window = this.$container.window();
if (this._popstateHandler) {
window.off('popstate', this._popstateHandler);
}
if (handler) {
window.on('popstate', handler);
}
}
this._popstateHandler = handler;
}
onPopstate(event) {
var historyState = event.originalEvent.state;
if (historyState && historyState.deepLinkPath) {
this.trigger('historyEntryActivate', historyState);
}
}
_onSplitterMove(event) {
// disallow a position greater than 50%
this.resizing = true;
var max = Math.floor(this.$container.outerWidth(true) / 2);
if (event.position > max) {
event.setPosition(max);
}
}
_onSplitterPositionChange(event) {
// No need to revalidate while layouting (desktop layout sets the splitter position and would trigger a relayout)
if (!this.htmlComp.layouting) {
this.revalidateLayout();
}
}
_onSplitterMoveEnd(event) {
var splitterPosition = event.position;
// Store size
if (this.cacheSplitterPosition) {
this._storeCachedSplitterPosition(this.splitter.position);
}
// Check if splitter is smaller than min size
if (splitterPosition < DesktopNavigation.BREADCRUMB_STYLE_WIDTH) {
// Set width of navigation to BREADCRUMB_STYLE_WIDTH, using an animation.
// While animating, update the desktop layout.
// At the end of the animation, update the desktop layout, and store the splitter position.
this.navigation.$container.animate({
width: DesktopNavigation.BREADCRUMB_STYLE_WIDTH
}, {
progress: function() {
this.resizing = true;
this.splitter.setPosition();
this.revalidateLayout();
this.resizing = false; // progress seems to be called after complete again -> layout requires flag to be properly set
}.bind(this),
complete: function() {
this.resizing = true;
this.splitter.setPosition();
// Store size
this._storeCachedSplitterPosition(this.splitter.position);
this.revalidateLayout();
this.resizing = false;
}.bind(this)
});
} else {
this.resizing = false;
}
}
_loadCachedSplitterPosition() {
return webstorage.getItem(sessionStorage, 'scout:desktopSplitterPosition') ||
webstorage.getItem(localStorage, 'scout:desktopSplitterPosition:' + window.location.pathname);
}
_storeCachedSplitterPosition(splitterPosition) {
webstorage.setItem(sessionStorage, 'scout:desktopSplitterPosition', splitterPosition);
webstorage.setItem(localStorage, 'scout:desktopSplitterPosition:' + window.location.pathname, splitterPosition);
}
_onNotificationRemove(notification) {
if (this.notifications.length === 0 && this.$notifications) {
this.$notifications.remove();
this.$notifications = null;
}
}
onReconnecting() {
if (!this.offline) {
return;
}
this._offlineNotification.reconnect();
}
onReconnectingSucceeded() {
if (!this.offline) {
return;
}
this.offline = false;
this._offlineNotification.reconnectSucceeded();
this._removeOfflineNotification();
}
onReconnectingFailed() {
if (!this.offline) {
return;
}
this._offlineNotification.reconnectFailed();
}
dataChange(dataType) {
this.events.trigger('dataChange', dataType);
}
_activeTheme() {
return cookies.get('scout.ui.theme') || Desktop.DEFAULT_THEME;
}
logoAction() {
if (this.logoActionEnabled) {
this.trigger('logoAction');
}
}
_initTheme() {
var theme = this.theme;
if (this.url.hasParameter('theme')) {
theme = strings.nullIfEmpty(this.url.getParameter('theme')) || Desktop.DEFAULT_THEME;
} else if (theme === null) {
theme = this._activeTheme();
}
this.setTheme(theme);
}
/**
* Changes the current theme.
* <p>
* The theme name is stored in a persistent cookie called scout.ui.theme.
* In order to activate it, the browser is reloaded so that the CSS files for the new theme can be downloaded.
* <p>
* Since it is a persistent cookie, the theme will be activated again the next time the app is started, unless the cookie is deleted.
*/
setTheme(theme) {
this.setProperty('theme', theme);
if (this.theme !== this._activeTheme()) {
this._switchTheme(theme);
}
}
_switchTheme(theme) {
// Add a persistent cookie which expires in 30 days
cookies.set('scout.ui.theme', theme, 30 * 24 * 3600);
// Reload page in order to download the CSS files for the new theme
// Don't remove body but make it invisible, otherwise JS exceptions might be thrown if body is removed while an action executed
$('body').setVisible(false);
var reloadOptions = {
clearBody: false
};
// If parameter 'theme' exists in the URL, remove it now - otherwise the parameter would overrule the cookie settings
if (this.url.hasParameter('theme')) {
this.url.removeParameter('theme');
reloadOptions.redirectUrl = this.url.toString();
}
scout.reloadPage(reloadOptions);
}
/**
* 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
*/
moveOverlaysBehindAndFocus(overlaysToMove, $targetOverlay) {
$targetOverlay = $.ensure($targetOverlay);
$targetOverlay.nextAll().toArray()
.forEach(function(overlay) {
if (arrays.containsAll(overlaysToMove, [overlay])) {
$(overlay).insertBefore($targetOverlay);
}
});
// 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);
}
}
repositionTooltips() {
this.$container.children('.tooltip').each(function() {
scout.widget($(this)).position();
});
}
}