@eclipse-scout/core
Version:
Eclipse Scout runtime
331 lines (289 loc) • 12.5 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 {Constructor, numbers, ObjectFactory, ObjectModel, objects, ObjectWithType, ObjectWithUuid, scout, SomeRequired, strings, Widget} from '../index';
/**
* Helper class to extract IDs of objects and to compute uuidPaths.
*/
export class ObjectIdProvider implements ObjectIdProviderModel, ObjectWithType {
declare model: ObjectIdProviderModel;
declare initModel: SomeRequired<this['model'], 'object'>;
declare self: ObjectIdProvider;
objectType: string;
protected _uiSeqNo = 0;
/**
* Prefix for all ids based on the ui sequence.
*/
static UI_SEQ_ID_PREFIX = '_ui_'; // must not contain any dots ('.') so that the id can be used as css selector "#..." and for the RegExp '_UI_SEQ_ID_PATTERN'.
/**
* Delimiter for the segments of a uuidPath.
*/
static UUID_PATH_DELIMITER = '|'; // "-" is used by UUID, "." by ClassNames, "_" by ClassId path from Java (see ITypeWithClassId.ID_CONCAT_SYMBOL).
/**
* Delimiter for dependent uuids
*/
static DEPENDENT_UUID_DELIMITER = '@';
protected static _UI_SEQ_ID_PATTERN = new RegExp('^' + ObjectIdProvider.UI_SEQ_ID_PREFIX + '\\d+$');
protected static _INSTANCE: ObjectIdProvider;
/**
* Modifiable set of widgets which will be skipped when building the uuidPath.
* A widget is skipped if its class is exactly one of these (*not* `instanceof`!).
*
* A widget may be skipped if it is not relevant for computing the uuidPath, e.g. if it is only a layouting component.
* For example: A group box is skipped because the id or uuid of a widget is normally unique inside a form so the group
* box would unnecessarily enlarge the uuidPath. Also, if the widget is moved into another group box, the uuidPath won't
* be affected. If the group box is extracted into a separate widget and gets its own class (aka. template field), it must
* not be skipped anymore because this template can be used multiple times on the same form and must therefore be part of
* the uuidPath. This template use-case is the reason why the subclasses of the registered widgets are not considered.
*/
static uuidPathSkipWidgets: Set<Constructor<Widget>> = new Set<Constructor<Widget>>();
/**
* Modifiable list of rules (predicates) which are used to determine if a parent should be skipped when building the uuidPath.
*
* These rules are always applied, even if {@link UuidPathOptions.considerSkipWidgets} is set to false.
*/
static uuidPathAlwaysSkipRules: ((widget: Widget) => boolean)[] = [];
/**
* Computes a path starting with the {@link uuid} of this object. If a parent is available, its {@link uuidPath} is appended to the right (recursively).
* {@link UUID_PATH_DELIMITER} is used as delimiter between the segments.
* By default, if the object is a remote (Scout Classic) object having a classId, its value is directly returned without appending the parent path,
* because classIds typically already include their parents.
*
* @param object The object for which the uuidPath should be computed.
* @param options Optional {@link UuidPathOptions} controlling the computation of the path.
*
* @returns the uuid path starting with this object's uuid or null if no path can be created.
*/
uuidPath(object: ObjectUuidSource, options: UuidPathOptions = {}) {
const uuid = this._buildUuid(object, options.useFallback);
// Abort if the starting element (not a parent) does not have an uuid
if (!uuid && scout.nvl(options.abortIfNoUuidFound, true)) {
return null;
}
// Abort if there is no parent
let parent = options.parent || object.uuidParent || object.parent;
if (!parent) {
return uuid;
}
// By default, stop on classIds as they typically include their parents already
const appendParent = !object.classId;
if (!appendParent) {
return uuid;
}
// Find the next relevant parent (some parents may be skipped)
let considerSkipWidgets = this._computeConsiderSkipWidgets(options, object);
if (parent instanceof Widget) {
parent = this._findUuidPathParent(parent, considerSkipWidgets);
}
// Prepare options for the next iteration
options.considerSkipWidgets = considerSkipWidgets;
options.abortIfNoUuidFound = scout.nvl(options.abortIfNoUuidFound, false); // Skip parents without an uuid
options.parent = null; // The next iteration must not use the parent passed by options.parent, only the parent of the starting element can be replaced
// Build the parent path and join it with the current uuid
return strings.join(ObjectIdProvider.UUID_PATH_DELIMITER, uuid, parent?.buildUuidPath(options));
}
protected _computeConsiderSkipWidgets(options: UuidPathOptions, object: ObjectUuidSource): UuidPathConsiderSkipWidgets {
if (objects.isNullOrUndefined(options.considerSkipWidgets) || options.considerSkipWidgets === 'dynamicFalse') {
// Do not skip parent widgets if the object only has an object type because the objectType normally is not unique enough
return (object.uuid || object.classId || this._considerId(object)) ? 'dynamicTrue' : 'dynamicFalse';
}
return options.considerSkipWidgets;
}
protected _findUuidPathParent(parent: Widget, considerSkipWidgets: UuidPathConsiderSkipWidgets): Widget {
if (!parent) {
return null;
}
if (this._isPathRelevantParent(parent, considerSkipWidgets)) {
return parent;
}
return parent.findParent(p => this._isPathRelevantParent(p, considerSkipWidgets));
}
protected _isPathRelevantParent(parent: Widget, considerSkipWidgets: UuidPathConsiderSkipWidgets): boolean {
if (this._skipParent(parent, scout.isOneOf(considerSkipWidgets, true, 'dynamicTrue'))) {
return false; // always uninteresting parents, event if they have a stable ID.
}
return true;
}
protected _buildUuid(object: ObjectUuidSource, useFallback?: boolean) {
if (object.buildUuid) {
return object.buildUuid(useFallback);
}
return this.uuid(object, useFallback);
}
/**
* Computes an uuid for the given object. The result may be a 'classId' for remote objects (Scout Classic) or an 'uuid' for Scout JS elements (if available).
* If the fallback is enabled, an id might be created using the 'id' property and 'objectType' property.
*
* @param useFallback Specifies if a fallback identifier may be created in case the object has no specific identifier set. The fallback may be less stable. Default is true.
* @returns the uuid for the object or null.
*/
uuid(object: ObjectUuidSource, useFallback = true): string {
if (!object) {
return null;
}
// Scout Classic ID
if (object.classId) {
return object.classId;
}
// Scout JS ID
if (object.uuid) {
return object.uuid;
}
// Fallback
if (!useFallback) {
return null; // no fallback
}
if (this._considerId(object)) {
return object.id;
}
let objectType: string;
if (typeof object.objectType === 'string') {
objectType = object.objectType;
} else {
const objectFactory = ObjectFactory.get();
objectType = objectFactory.getObjectType(object.constructor as Constructor) || objectFactory.getObjectType(object.objectType);
}
if (objectType) {
return objectType;
}
return null;
}
protected _considerId(object: ObjectUuidSource) {
let id = object.id;
if (strings.empty(id)) {
return false;
}
if (this.isUiSeqId(id)) {
return false;
}
if (numbers.isNumber(parseInt(id))) {
// Model adapter ids
return false;
}
return true;
}
/**
* @returns true if the given widget should be skipped when computing the {@link uuidPath}.
*/
protected _skipParent(obj: Widget, considerSkipWidgets = true): boolean {
if (!obj) {
return true;
}
let skip = considerSkipWidgets && ObjectIdProvider.uuidPathSkipWidgets.has(obj.constructor as Constructor<Widget>);
if (skip) {
return true;
}
return ObjectIdProvider.uuidPathAlwaysSkipRules.some(rule => rule(obj));
}
/**
* Builds the uuid of the object and prepends the given prefix.
*
* This is useful for objects not having an own uuid but need to be referenced nevertheless.
*/
createDependentUuid(prefix: string, source: ObjectUuidSource): string {
const uuid = this._buildUuid(source);
if (!uuid) {
return null;
}
return strings.join(ObjectIdProvider.DEPENDENT_UUID_DELIMITER, prefix, uuid);
}
/**
* Builds the uuid of the object, prepends it with the given prefix and sets it to the target.
*
* This is useful for objects not having an own uuid but need to be referenced nevertheless.
* @see createDependentUuid
*/
setDependentUuid(prefix: string, source: ObjectUuidSource, target: Required<ObjectUuidSource>): string {
if (target.uuid || target.classId) {
return;
}
const uuid = this.createDependentUuid(prefix, source);
if (!uuid) {
return;
}
return target.setUuid(uuid);
}
/**
* Checks if the given id is an id created from the ui sequence.
*
* @param id The id to check or null.
* @returns true if the id follows the format of ui sequence ids (e.g. starts with {@link UI_SEQ_ID_PREFIX}).
*/
isUiSeqId(id: string): boolean {
return ObjectIdProvider._UI_SEQ_ID_PATTERN.test(id);
}
/**
* Returns a new id based on the ui sequence.
*
* @returns id with prefix {@link ObjectIdProvider.UI_SEQ_ID_PREFIX}.
*/
createUiSeqId(): string {
return ObjectIdProvider.UI_SEQ_ID_PREFIX + (++this._uiSeqNo).toString();
}
/**
* @returns current ui sequence number
*/
get uiSeqNo(): number {
return this._uiSeqNo;
}
/**
* @returns The shared singleton {@link ObjectIdProvider} instance.
*/
static get(): ObjectIdProvider {
if (!ObjectIdProvider._INSTANCE) {
ObjectIdProvider._INSTANCE = scout.create(ObjectIdProvider);
}
return ObjectIdProvider._INSTANCE;
}
}
export interface UuidPathOptions {
/**
* Specifies 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.
*/
useFallback?: boolean;
/**
* Specifies whether computation of the uuidPath should be aborted as soon as {@link ObjectIdProvider.uuid} returns null.
* By default, the computation will be aborted if no uuid can be computed for the starting element.
* If no uuid can be computed for a parent, the parent will be skipped and the computation continues with the next parent.
*/
abortIfNoUuidFound?: boolean;
/**
* Specifies whether the {@link ObjectIdProvider.uuidPathSkipWidgets} should be considered when building the uuidPath.
* By default, the skipWidgets are considered once an object (either the starting element or a parent) is found with a relevant id (id, uuid or classId).
*
* For example:
* - When the starting element contains a relevant id, the skipWidgets are considered for the parents -> all parents that are part of skipWidgets will be skipped.
* - When the starting element does not contain a relevant id, the parents are not skipped even if they are part of the skipWidgets until a parent is reached with a relevant id.
* All further parents may be skipped again if they are part of the skipWidgets.
*/
considerSkipWidgets?: UuidPathConsiderSkipWidgets;
/**
* Specifies a {@link Widget} that should be used as parent of the given object instead of `object.parent` when building the uuidPath.
*
* By the default `object.parent` is used.
*/
parent?: Widget;
}
/**
* An object for which an uuid and/or uuidPath can be computed using {@link ObjectIdProvider}.
*/
export interface ObjectUuidSource extends Partial<ObjectWithUuid>, Partial<ObjectWithType> {
id?: string;
classId?: string;
parent?: Widget;
uuidParent?: ObjectWithUuid;
}
export interface ObjectIdProviderModel extends ObjectModel<ObjectIdProvider> {
object?: ObjectUuidSource;
parent?: Widget;
}
export type UuidPathConsiderSkipWidgets = boolean | 'dynamicTrue' | 'dynamicFalse';