@eclipse-scout/core
Version:
Eclipse Scout runtime
588 lines (535 loc) • 22.3 kB
text/typescript
/*
* 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 {
AdapterData, App, Device, GroupBox, Locale, locales, LogicalGridLayout, ModelAdapterLike, ObjectCreator, ObjectFactory, ObjectFactoryOptions, ObjectIdProvider, objects, ObjectType, Session, strings, TileGrid, UuidPathOptions, ValueField,
Widget, widgets
} from './index';
import $ from 'jquery';
let $activeElements = null;
/**
* The minimal model declaration (usually extends {@link ObjectModel}) as it would be used in a nested declaration (e.g. a {@link FormField} within a {@link GroupBox}).
* The {@link objectType} is optional as sometimes it might be already given by the context (e.g. when passing a {@link MenuModel} to a method {@link insertMenu()} where the method sets a default {@link objectType} if missing).
*/
export type ModelOf<TObject> = TObject extends { model?: infer TModel } ? TModel : object;
/**
* Model used to initialize an object instance. Usually the same as {@link ModelOf} but with some minimal required properties (mandatory properties).
* The definition of the required properties can be done by the object itself by declaring a property called `initModel`.
* A typical object with an `initModel` adds an e.g. {@link parent} or {@link session} property which needs to be present when initializing an already created instance.
*/
export type InitModelOf<TObject> = TObject extends { initModel?: infer TInitModel } ? TInitModel : ModelOf<TObject>;
/**
* Model required to create a new object as child of an existing. To identify the object an {@link objectType} is mandatory.
* But as the properties required to initialize the instance are derived from the parent, no other mandatory properties are required.
*/
export type ChildModelOf<TObject> = ModelOf<TObject> & { objectType: ObjectType<TObject> };
/**
* A full object model declaring all mandatory properties. Such models contain all information to create ({@link objectType}) and initialize (e.g. {@link parent}) a new object.
*/
export type FullModelOf<TObject> = InitModelOf<TObject> & ChildModelOf<TObject>;
/**
* Represents an instance of an object or its minimal model ({@link ModelOf}).
*/
export type ObjectOrModel<T> = T | ModelOf<T>;
/**
* Represents an instance of an object or its child model ({@link ChildModelOf}).
*/
export type ObjectOrChildModel<T> = T | ChildModelOf<T>;
/**
* Represents an instance of an object or its type ({@link ObjectType}).
*/
export type ObjectOrType<T> = T | ObjectType<T>;
export type Constructor<T = object> = new(...args: any[]) => T;
export type AbstractConstructor<T = object> = abstract new(...args: any[]) => T;
export type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
export interface ObjectWithType {
objectType: string;
}
/**
* Represents an object having an uuid.
*/
export interface ObjectWithUuid {
/**
* The unique identifier property of the object.
*
* This property alone may not be unique within a widget tree (e.g. if template widgets are used).
* To get a more unique id use {@link buildUuidPath} instead.
*/
uuid: string;
/**
* Parent to be used instead of the default parent when computing the uuid path for the object.
*
* @see buildUuidPath
*/
uuidParent?: ObjectWithUuid;
/**
* Computes a unique identifier for the object.
*
* Compared to {@link uuid} it also considers the {@link classId} property and may use a fallback logic if none of these two properties are available, see {@link ObjectIdProvider.uuid}.
*
* This property alone may not be unique within a widget tree (e.g. if template widgets are used).
* To get a more unique id use {@link buildUuidPath} instead.
*
* @param useFallback Optional boolean specifying if a fallback identifier may be created in case an object has no specific identifier set. The fallback may be less stable. Default is true.
* @returns the uuid for the object or null.
*/
buildUuid(useFallback?: boolean): string;
/**
* Computes a unique identifier for the object considering parent objects (if existing).
*
* Note: The returned id may not be unique within the application! E.g. if the same form is opened twice, its children will share the same ids.
* @param options Optional {@link UuidPathOptions} controlling the computation of the path.
* @see ObjectIdProvider.uuidPath.
*/
buildUuidPath(options?: UuidPathOptions): string;
/**
* Sets the {@link uuid} property.
*/
setUuid(uuid: string);
}
export interface ObjectWithId<TId = string> {
id: TId;
}
export interface ObjectModel<TObject = object> {
objectType?: ObjectType<TObject>;
}
export interface ObjectModelWithId<TId = string> {
id?: TId;
}
export interface ObjectModelWithUuid<TObject = object> extends ObjectModel<TObject> {
/**
* A unique identifier for the object. Typically, a new random UUID can be used.
*/
uuid?: string;
/**
* Parent to be used instead of the default parent when computing the uuid path for the object.
*
* @see ObjectWithUuid.buildUuidPath
*/
uuidParent?: ObjectWithUuid;
}
export interface ReloadPageOptions {
/**
* If true, the page reload is not executed in the current thread but scheduled using setTimeout().
* This is useful if the caller wants to execute some other code before reload. The default is false.
*/
schedule?: boolean;
/**
* If true, the body is cleared first before reload. This is useful to prevent
* showing "old" content in the browser until the new content arrives. The default is true.
*/
clearBody?: boolean;
/**
* The new URL to load. If not specified, the current location is used (window.location).
*/
redirectUrl?: string;
}
function create<T>(objectType: Constructor<T>, model?: InitModelOf<T>, options?: ObjectFactoryOptions): T;
function create<T>(model: FullModelOf<T>, options?: ObjectFactoryOptions): T;
function create(objectType: string, model?: object, options?: ObjectFactoryOptions): any;
function create(model: { objectType: string; [key: string]: any }, options?: ObjectFactoryOptions): any;
function create<T>(objectType: ObjectType<T> | FullModelOf<T>, model?: InitModelOf<T>, options?: ObjectFactoryOptions): T;
/**
* Creates a new object instance.
*
* Delegates the create call to {@link ObjectFactory.create}.
*/
function create<T extends object>(objectType: ObjectType<T> | FullModelOf<T>, model?: InitModelOf<T>, options?: ObjectFactoryOptions): T {
return ObjectFactory.get().create(objectType, model, options);
}
function widget<T extends Widget>(widgetIdOrElement: string | number | HTMLElement | JQuery, partIdOrType?: string | Constructor<T>): T;
function widget(widgetIdOrElement: string | number | HTMLElement | JQuery, partId?: string): Widget;
/**
* Resolves the widget using the given widget id or HTML element.
*
* If the argument is a string or a number, it will search the widget hierarchy for the given id using {@link Widget.widget}.
* If the argument is a {@link HTMLElement} or {@link JQuery} element, it will use {@link widgets.get(elem)} to get the widget which belongs to the given element.
*
* @param widgetIdOrElement
* a widget ID or an HTML or jQuery element
* @param partId
* partId of the session the widget belongs to (optional, only relevant if the
* argument is a widget ID). If omitted, the first session is used.
* @returns the widget for the given element or id
*/
function widget<T extends Widget>(widgetIdOrElement: string | number | HTMLElement | JQuery, partIdOrType?: string | Constructor<T>): T {
if (objects.isNullOrUndefined(widgetIdOrElement)) {
return null;
}
let $elem = widgetIdOrElement;
if (typeof widgetIdOrElement === 'string' || typeof widgetIdOrElement === 'number') {
// Find widget for ID
let partId = typeof partIdOrType === 'string' ? partIdOrType : null;
let session = scout.getSession(partId);
if (session) {
let id = strings.asString(widgetIdOrElement);
return session.root.widget(id) as T;
}
}
return widgets.get($elem as (HTMLElement | JQuery)) as T;
}
export const scout = {
objectFactories: new Map<string | Constructor, ObjectCreator>(),
/**
* Returns the first of the given arguments that is not null or undefined. If no such element
* is present, the last argument is returned. If no arguments are given, undefined is returned.
*/
nvl(...args: any[]): any {
let result;
for (let i = 0; i < args.length; i++) {
result = args[i];
if (result !== undefined && result !== null) {
break;
}
}
return result;
},
/**
* Use this method in your functions to assert that a mandatory parameter is passed
* to the function. Throws an error when value is not set.
*
* @param type if this optional parameter is set, the given value must be of this type (instanceof check)
* @returns the value (for direct assignment)
*/
assertParameter<T>(parameterName: string, value?: T, type?: Constructor | AbstractConstructor): T {
if (objects.isNullOrUndefined(value)) {
throw new Error('Missing required parameter \'' + parameterName + '\'');
}
if (type && !(value instanceof type)) {
throw new Error('Parameter \'' + parameterName + '\' has wrong type');
}
return value;
},
/**
* Use this method to assert that a mandatory property is set. Throws an error when value is not set.
*
* @param type if this parameter is set, the value must be of this type (instanceof check)
* @returns the value (for direct assignment)
*/
assertProperty(object: object, propertyName: string, type?: Constructor | AbstractConstructor) {
let value = object[propertyName];
if (objects.isNullOrUndefined(value)) {
throw new Error('Missing required property \'' + propertyName + '\'');
}
if (type && !(value instanceof type)) {
throw new Error('Property \'' + propertyName + '\' has wrong type');
}
return value;
},
/**
* Throws an error if the given value is null or undefined. Otherwise, the value is returned.
*
* @param value value to check
* @param msg optional error message when the assertion fails
*/
assertValue<T>(value: T, msg?: string): T {
if (objects.isNullOrUndefined(value)) {
throw new Error(msg || 'Missing value');
}
return value;
},
/**
* Throws an error if the given value is not an instance of the given type. Otherwise, the value is returned.
*
* @param value value to check
* @param type type to check against with "instanceof"
* @param msg optional error message when the assertion fails
*/
assertInstance<T>(value: any, type: Constructor<T> | AbstractConstructor<T>, msg?: string): T {
if (!(value instanceof type)) {
throw new Error(msg || 'Value has wrong type');
}
return value;
},
/**
* Checks if one of the arguments from 1-n is equal to the first argument.
* @param args to check against the value, may be an array or a variable argument list.
*/
isOneOf(value: any, ...args /* explicit any produces warning at calling js code */): boolean {
if (args.length === 0) {
return false;
}
let argsToCheck = args;
if (args.length === 1 && Array.isArray(args[0])) {
argsToCheck = args[0];
}
return argsToCheck.indexOf(value) !== -1;
},
create,
/**
* Ensures the given parameter is an object.
*
* If it is an object, it will be returned as it is.
* If it is an {@link ObjectType}, {@link scout.create} is used to create the object.
*
* @param model will be passed to {@link scout.create} if an object needs to be created and ignored otherwise.
*/
ensure<T extends object>(objectOrType: ObjectOrType<T>, model?: InitModelOf<T>): T {
if (typeof objectOrType === 'string' || typeof objectOrType === 'function') {
return scout.create(objectOrType, model);
}
return objectOrType;
},
/**
* Prepares the DOM for scout in the given document. This should be called once while initializing scout.
* If the target document is not specified, the global "document" variable is used instead.
*
* This is used by apps (App, LoginApp, LogoutApp)
*
* Currently, it does the following:
* - Remove the <noscript> tag (obviously there is no need for it).
* - Remove <scout-text> tags (they must have been processed before, see texts.readFromDOM())
* - Remove <scout-version> tag (it must have been processed before, see App._initVersion())
* - Add a device / browser class to the body tag to allow for device specific CSS rules.
* - Add browser locale to DOM so screen readers read text correctly (may get replaced if actual locale of user is loaded)
* - If the browser is Google Chrome, add a special meta header to prevent automatic translation.
*/
prepareDOM(targetDocument: Document) {
targetDocument = targetDocument || document;
// Cleanup DOM
$('noscript', targetDocument).remove();
$('scout-text', targetDocument).remove();
$('scout-version', targetDocument).remove();
$('body', targetDocument).addDeviceClass();
// Set locale of the document so screen readers read text correctly
scout.setDocumentLocale(locales.getNavigatorLocale());
// Prevent "Do you want to translate this page?" in Google Chrome
if (Device.get().browser === Device.Browser.CHROME) {
let metaNoTranslate = '<meta name="google" content="notranslate" />';
let $title = $('head > title', targetDocument);
if ($title.length === 0) {
// Add to end of head
$('head', targetDocument).append(metaNoTranslate);
} else {
$title.after(metaNoTranslate);
}
}
},
/**
* Installs a global 'mousedown' interceptor to invoke 'aboutToBlurByMouseDown' on value field before anything else gets executed.
*/
installGlobalMouseDownInterceptor(myDocument: Document) {
myDocument.addEventListener('mousedown', event => {
ValueField.invokeValueFieldAboutToBlurByMouseDown(event.target as Element);
}, true); // true=the event handler is executed in the capturing phase
},
/**
* Because Firefox does not set the active state of a DOM element when the mousedown event
* for that element is prevented, we set an 'active' CSS class instead. This means in the
* CSS we must deal with :active and with .active, where we need same behavior for the
* active state across all browsers.
*
* Typically, you'd write something like this in your CSS:
* ```
* button:active, button.active { ... }
* ```
*/
installSyntheticActiveStateHandler(myDocument: Document) {
if (Device.get().requiresSyntheticActiveState()) {
$activeElements = [];
$(myDocument)
.on('mousedown', (event: JQuery.MouseDownEvent) => {
let $element = $(event.target);
while ($element.length) {
$activeElements.push($element.addClass('active'));
$element = $element.parent();
}
})
.on('mouseup', () => {
$activeElements.forEach($element => {
$element.removeClass('active');
});
$activeElements = [];
});
}
},
/**
* Sets locale of the document, important for screen readers
* @param locale
*/
setDocumentLocale(locale: Locale) {
if (!objects.isNullOrUndefined(locale)) {
document.documentElement.lang = locale.languageTag;
}
},
widget,
/**
* Helper to get the model adapter for a given adapterId. If there is more than one
* session, e.g. in case of portlets, the second argument specifies the partId of the session
* to be queried. If not specified explicitly, the first session is used. If the session or
* the adapter could not be found, null is returned.
*/
adapter(adapterId: string, partId: string): ModelAdapterLike {
if (objects.isNullOrUndefined(adapterId)) {
return null;
}
let session = scout.getSession(partId);
if (session && session.modelAdapterRegistry) {
return session.modelAdapterRegistry[adapterId];
}
return null;
},
/**
* @returns the session for the given partId. If the partId is omitted, the first session is returned.
*/
getSession(partId?: string): Session {
let sessions = App.get().sessions;
if (!sessions) {
return null;
}
if (objects.isNullOrUndefined(partId)) {
return sessions[0];
}
for (let i = 0; i < sessions.length; i++) {
let session = sessions[i];
// eslint-disable-next-line eqeqeq
if (session.partId == partId) { // <-- compare with '==' is intentional! (NOSONAR)
return session;
}
}
return null;
},
/**
* This method exports the adapter with the given ID as JSON, it returns a plain object containing the
* configuration of the adapter. You can transform that object into JSON by calling <code>JSON.stringify</code>.
* This method can only be called through the browser JavaScript console.
* Here's an example of how to call the method:
*
* JSON.stringify(exportAdapter(4))
*/
exportAdapter(adapterId: string, partId: string): AdapterData {
let session = scout.getSession(partId);
if (session && session.modelAdapterRegistry) {
let adapter = session.getModelAdapter(adapterId);
if (!adapter) {
return null;
}
let adapterData = cloneAdapterData(adapterId);
resolveAdapterReferences(adapter, adapterData);
adapterData.type = 'model'; // property 'type' is required for models.ts
return adapterData;
}
// ----- Helper functions -----
function cloneAdapterData(adapterId: string): AdapterData {
let adapterData = session.getAdapterData(adapterId);
adapterData = $.extend(true, {}, adapterData);
return adapterData;
}
function resolveAdapterReferences(adapter: ModelAdapterLike, adapterData: AdapterData) {
let tmpAdapter: ModelAdapterLike, tmpAdapterData: AdapterData;
adapter.widget.widgetProperties.forEach(widgetPropertyName => {
let widgetPropertyValue = adapterData[widgetPropertyName];
if (!widgetPropertyValue) {
return; // nothing to do when property is null
}
if (Array.isArray(widgetPropertyValue)) {
// value is an array of adapter IDs
let adapterDataArray = [];
widgetPropertyValue.forEach(adapterId => {
tmpAdapter = session.getModelAdapter(adapterId);
tmpAdapterData = cloneAdapterData(adapterId);
resolveAdapterReferences(tmpAdapter, tmpAdapterData);
adapterDataArray.push(tmpAdapterData);
});
adapterData[widgetPropertyName] = adapterDataArray;
} else {
// value is an adapter ID
tmpAdapter = session.getModelAdapter(widgetPropertyValue);
tmpAdapterData = cloneAdapterData(widgetPropertyValue);
resolveAdapterReferences(tmpAdapter, tmpAdapterData);
adapterData[widgetPropertyName] = tmpAdapterData;
}
});
adapterData = adapter.exportAdapterData(adapterData);
}
return null;
},
/**
* Reloads the entire browser window.
*/
reloadPage(options?: ReloadPageOptions) {
options = options || {} as ReloadPageOptions;
if (options.schedule) {
setTimeout(reloadPageImpl);
} else {
reloadPageImpl();
}
// ----- Helper functions -----
function reloadPageImpl() {
// Hide everything (on entire page, not only $entryPoint)
if (scout.nvl(options.clearBody, true)) {
$('body').html('');
}
if (options.redirectUrl) {
window.location.href = options.redirectUrl;
} else {
window.location.reload();
}
}
},
/**
* @param factories Object that contains the object type as key and the that constructs the object as value.
* If you prefer using a class reference as object type rather than a string, please use {@link addObjectFactory} to register your factory.
* @see create
*/
addObjectFactories(factories: Record<string, ObjectCreator>) {
for (let [objectType, factory] of Object.entries(factories)) {
scout.addObjectFactory(objectType, factory);
}
},
/**
* @param objectType ObjectType to register the factory for.
* @param factory Function that constructs the object.
* @see create
*/
addObjectFactory(objectType: ObjectType, factory: ObjectCreator) {
scout.objectFactories.set(objectType, factory);
},
cloneShallow(template: object, properties?: object, createUniqueId?: boolean): object {
scout.assertParameter('template', template);
let clone = Object.create(Object.getPrototypeOf(template));
Object.getOwnPropertyNames(template)
.forEach(key => {
clone[key] = template[key];
});
if (properties) {
for (let key in properties) {
clone[key] = properties[key];
}
}
if (scout.nvl(createUniqueId, true)) {
clone.id = ObjectIdProvider.get().createUiSeqId();
}
if (clone.cloneOf === undefined) {
clone.cloneOf = template;
}
return clone;
},
/**
* Enables or disables the layout spy that visualizes the cell bounds of the logical grid for debugging purposes.
* The spy can be enabled on elements that belong to a widget using a {@link LogicalGridLayout}, e.g. {@link GroupBox}, {@link TileGrid}, etc.
*/
setLogicalGridSpyEnabled(elem: HTMLElement, enabled: boolean) {
let layout;
let widget = scout.widget(elem);
if (widget instanceof GroupBox) {
layout = widget.htmlBody.layout;
widget.htmlBody.invalidateLayoutTree(false);
} else {
layout = widget.htmlComp.layout;
widget.htmlComp.invalidateLayoutTree(false);
}
if (!(layout instanceof LogicalGridLayout)) {
throw new Error('Layout needs to be a LogicalGridLayout');
}
layout.setSpyEnabled(enabled);
}
};