@eclipse-scout/core
Version:
Eclipse Scout runtime
377 lines (330 loc) • 13.9 kB
text/typescript
/*
* Copyright (c) 2010, 2023 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 {BrowserFieldEventMap, BrowserFieldLayout, BrowserFieldModel, EnumObject, FormField, IFrame, InitModelOf, numbers, PopupBlockerHandler, PropertyChangeEvent, Rectangle, scout, strings} from '../../../index';
import $ from 'jquery';
import TriggeredEvent = JQuery.TriggeredEvent;
export type BrowserFieldWindowStates = EnumObject<typeof BrowserField.WindowStates>;
export class BrowserField extends FormField implements BrowserFieldModel {
declare model: BrowserFieldModel;
declare eventMap: BrowserFieldEventMap;
declare self: BrowserField;
autoCloseExternalWindow: boolean;
externalWindowButtonText: string;
externalWindowFieldText: string;
location: string;
trackLocation: boolean;
sandboxEnabled: boolean;
sandboxPermissions: string;
trustedMessageOrigins: string[];
scrollBarEnabled: boolean;
showInExternalWindow: boolean;
iframe: IFrame;
myWindow: Window;
protected _messageListener: (event: MessageEvent) => any;
protected _popupWindow: Window;
protected _externalWindowTextField: JQuery;
protected _externalWindowButton: JQuery;
constructor() {
super();
this.autoCloseExternalWindow = false;
this.externalWindowButtonText = null;
this.externalWindowFieldText = null;
this.location = null;
this.trackLocation = false;
this.sandboxEnabled = true;
this.sandboxPermissions = null;
this.trustedMessageOrigins = [];
this.scrollBarEnabled = true;
this.showInExternalWindow = false;
this._messageListener = null;
this._popupWindow = null;
this._externalWindowTextField = null;
this._externalWindowButton = null;
}
static WindowStates = {
WINDOW_OPEN: 'true',
WINDOW_CLOSED: 'false'
};
protected override _init(model: InitModelOf<this>) {
super._init(model);
this.iframe = scout.create(IFrame, {
parent: this,
location: this.location,
sandboxEnabled: this.sandboxEnabled,
sandboxPermissions: this.sandboxPermissions,
scrollBarEnabled: this.scrollBarEnabled,
trackLocation: this.trackLocation
});
this.iframe.on('propertyChange', this._onIFramePropertyChange.bind(this));
}
protected override _render() {
this.addContainer(this.$parent, 'browser-field', new BrowserFieldLayout(this));
this.addLabel();
this.addStatus();
if (!this.showInExternalWindow) {
// mode 1: <iframe>
this.iframe.render();
this.addFieldContainer(this.iframe.$container);
this.addField(this.iframe.$iframe);
this.$field.on('load', this._onLoad.bind(this));
} else {
// mode 2: separate window
this.addField(this.$parent.makeDiv());
this._externalWindowTextField = this.$field.appendDiv()
.addClass('alt');
this._externalWindowButton = this.$field.appendDiv()
.addClass('button')
.on('click', event => this._openPopupWindow(true));
}
this.myWindow = this.$parent.window(true);
this._messageListener = this._onMessage.bind(this);
this.myWindow.addEventListener('message', this._messageListener);
if (this.enabledComputed) {
// use setTimeout to call method, because _openPopupWindow must be called after layouting
setTimeout(this._openPopupWindow.bind(this, true), 20);
}
}
protected override _renderProperties() {
super._renderProperties();
this._renderExternalWindowButtonText();
this._renderExternalWindowFieldText();
}
protected override _remove() {
super._remove();
this.myWindow.removeEventListener('message', this._messageListener);
this._messageListener = null;
// if content is shown in an external window and auto close is set to true
if (this.showInExternalWindow && this.autoCloseExternalWindow) {
// try to close popup window (if it is not already closed)
if (this._popupWindow && !this._popupWindow.closed) {
this._popupWindow.close();
}
}
}
setLocation(location: string) {
this.setProperty('location', location);
this.iframe.setLocation(location);
}
protected _renderLocation() {
// Convert empty locations to 'about:blank', because in Firefox (maybe others, too?),
// empty locations simply remove the src attribute but don't remove the old content.
let location = this.location || 'about:blank';
if (this.showInExternalWindow) {
// fallback: separate window
if (this._popupWindow && !this._popupWindow.closed) {
this._popupWindow.location = location;
}
}
}
setAutoCloseExternalWindow(autoCloseExternalWindow: boolean) {
this.setProperty('autoCloseExternalWindow', autoCloseExternalWindow);
}
setExternalWindowButtonText(externalWindowButtonText: string) {
this.setProperty('externalWindowButtonText', externalWindowButtonText);
}
protected _renderExternalWindowButtonText() {
if (this.showInExternalWindow) {
this._externalWindowButton.text(this.externalWindowButtonText || '');
}
}
setExternalWindowFieldText(externalWindowFieldText: string) {
this.setProperty('externalWindowFieldText', externalWindowFieldText);
}
protected _renderExternalWindowFieldText() {
if (this.showInExternalWindow) {
this._externalWindowTextField.text(this.externalWindowFieldText || '');
}
}
/**
* Note: this function is designed to deliver good results to position a popup over a BrowserField in Internet Explorer.
* Other browsers may not perfectly position the popup, since they return different values for screenX/screenY. Also
* there's no way to retrieve all required values from the window or screen object, that's why we have to use hard coded
* values here. In order to make this function more flexible you could implement it as a strategy which has different
* browser dependent implementations.
*
* This implementation does also deal with a multi screen setup (secondary monitor). An earlier implementation used
* screen.availWidth to make sure the popup is within the visible area of the screen. However, screen.availWidth only
* returns the size of the primary monitor, so we cannot use it. There's no way to check for a secondary monitor from
* a HTML document. So we removed the check entirely, which shouldn't be an issue since the browser itself does prevent
* popups from having an invalid position.
*/
protected _calcPopupBounds(): Rectangle {
let myWindow = this.$container.window(true);
let POPUP_WINDOW_TOP_HEIGHT = 30;
let POPUP_WINDOW_BOTTOM_HEIGHT = 8;
let POPUP_WINDOW_CHROME_HEIGHT = POPUP_WINDOW_TOP_HEIGHT + POPUP_WINDOW_BOTTOM_HEIGHT;
let BROWSER_WINDOW_TOP_HEIGHT = 55;
// Don't limit screenX/Y in any way. Coordinates can be negative (if we have a secondary monitor on the left side
// of the primary monitor) or larger then the availSize of the screen (if we have a secondary monitor on the right
// side of the primary monitor). Note that IE cannot properly place the popup on a monitor on the left. It seems
// to ignore negative X coordinates somehow (but not entirely).
let browserBounds = new Rectangle(
myWindow.screenX,
myWindow.screenY,
$(myWindow).width(),
$(myWindow).height() + BROWSER_WINDOW_TOP_HEIGHT);
let fieldBounds = new Rectangle(
this.$field.offset().left,
this.$field.offset().top,
this.$field.width(),
this.$field.height());
let popupX = browserBounds.x + fieldBounds.x;
let popupY = browserBounds.y + fieldBounds.y + BROWSER_WINDOW_TOP_HEIGHT;
let popupWidth = fieldBounds.width;
let popupHeight = fieldBounds.height + POPUP_WINDOW_CHROME_HEIGHT;
// ensure that the lower Y of the new popup is not below the lower Y of the browser window
let popupLowerY = popupY + popupHeight;
let browserLowerY = browserBounds.y + browserBounds.height;
if (popupLowerY > browserLowerY) {
popupHeight -= (popupLowerY - browserLowerY) + POPUP_WINDOW_CHROME_HEIGHT;
}
return new Rectangle(
numbers.round(popupX),
numbers.round(popupY),
numbers.round(popupWidth),
numbers.round(popupHeight)
);
}
protected _openPopupWindow(reopenIfClosed?: boolean) {
reopenIfClosed = scout.nvl(reopenIfClosed, true);
if (!this.showInExternalWindow) {
return;
}
if (!this._popupWindow || (reopenIfClosed && this._popupWindow.closed)) {
let popupBlockerHandler = scout.create(PopupBlockerHandler, {session: this.session});
let popupBounds = this._calcPopupBounds();
// (b) window specifications
let windowSpecs = strings.join(',',
'directories=no',
'location=no',
'menubar=no',
'resizable=yes',
'status=no',
'scrollbars=' + (this.scrollBarEnabled ? 'yes' : 'no'),
'toolbar=no',
'dependent=yes',
'left=' + popupBounds.x,
'top=' + popupBounds.y,
'width=' + popupBounds.width,
'height=' + popupBounds.height
);
let location = this.location || 'about:blank';
popupBlockerHandler.openWindow(location, undefined, windowSpecs, this._popupWindowOpen.bind(this));
} else if (reopenIfClosed) {
this._popupWindow.focus();
}
}
protected _popupWindowOpen(popup: Window) {
this._popupWindow = popup;
if (this._popupWindow && !this._popupWindow.closed) {
this.trigger('externalWindowStateChange', {
windowState: BrowserField.WindowStates.WINDOW_OPEN
});
let popupInterval = window.setInterval(() => {
let popupWindowClosed = false;
try {
popupWindowClosed = this._popupWindow === null || this._popupWindow.closed;
} catch (e) {
// for some unknown reason, IE sometimes throws a "SCRIPT16386" error while trying to read '._popupWindow.closed'.
$.log.isInfoEnabled() && $.log.info('Reading the property popupWindow.closed threw an error (Retry in 500ms)');
return;
}
if (popupWindowClosed) {
window.clearInterval(popupInterval);
this.trigger('externalWindowStateChange', {
windowState: BrowserField.WindowStates.WINDOW_CLOSED
});
}
}, 500);
}
}
protected _onMessage(event: MessageEvent) {
// Only handle event originating form "our" iframe
if (!this._isValidMessageSource(event.source)) {
return;
}
// Check if the origin is trusted before we do anything else with the data
if (this.trustedMessageOrigins && this.trustedMessageOrigins.length &&
!this.trustedMessageOrigins.some(origin => origin === event.origin)) {
$.log.warn('blocked message from untrusted origin ' + event.origin);
return;
}
$.log.isDebugEnabled() && $.log.debug('received post-message: data=' + event.data + ', origin=' + event.origin);
this.trigger('message', {
data: event.data,
origin: event.origin
});
}
protected _isValidMessageSource(source: MessageEventSource): boolean {
let iframeWindow = (this.$field[0] as HTMLIFrameElement).contentWindow;
if (source === iframeWindow) {
return true; // same source
}
// Check parents of window in case event source is an inner iframe
// parent window of topmost window is itself (https://developer.mozilla.org/en-US/docs/Web/API/Window/parent)
let win = source as Window;
while (win && win !== win.parent) {
win = win.parent;
if (win === iframeWindow) {
return true;
}
}
return false; // no valid parent window found
}
/**
* Sends a message to the embedded web page (`iframe`).
*
* @param message
* The message to send.
* @param targetOrigin
* The expected origin of the receiving `window`. If the origin does not match, the browser will not
* dispatch the message for security reasons. See the
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">documentation</a> for
* details.
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">window.postMessage (MDN)</a>
*/
postMessage(message: any, targetOrigin: string) {
$.log.isDebugEnabled() && $.log.debug('send post-message: message=' + message + ', targetOrigin=' + targetOrigin);
this.iframe && this.iframe.postMessage(message, targetOrigin);
}
/** @see BrowserFieldModel.sandboxPermissions */
setTrackLocation(trackLocation: boolean) {
this.setProperty('trackLocation', trackLocation);
this.iframe.setTrackLocation(trackLocation);
}
protected _onIFramePropertyChange(event: PropertyChangeEvent<IFrame>) {
if (!this.trackLocation) {
return;
}
if (event.propertyName === 'location') {
this._setProperty('location', event.newValue);
}
}
protected _onLoad(event: TriggeredEvent) {
if (!this.rendered) { // check needed, because this is an async callback
return;
}
this.invalidateLayoutTree();
}
/** @see BrowserFieldModel.sandboxEnabled */
setSandboxEnabled(sandboxEnabled: boolean) {
this.setProperty('sandboxEnabled', sandboxEnabled);
this.iframe.setSandboxEnabled(sandboxEnabled);
}
/** @see BrowserFieldModel.sandboxPermissions */
setSandboxPermissions(sandboxPermissions: string) {
this.setProperty('sandboxPermissions', sandboxPermissions);
this.iframe.setSandboxPermissions(sandboxPermissions);
}
setScrollBarEnabled(scrollBarEnabled: boolean) {
this.setProperty('scrollBarEnabled', scrollBarEnabled);
this.iframe.setScrollBarEnabled(scrollBarEnabled);
}
}