UNPKG

@eclipse-scout/core

Version:
331 lines (289 loc) 12.5 kB
/* * 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';