diff --git a/packages/base/src/FeaturesRegistry.ts b/packages/base/src/FeaturesRegistry.ts index 85c8afcb3503..33e12e9243ab 100644 --- a/packages/base/src/FeaturesRegistry.ts +++ b/packages/base/src/FeaturesRegistry.ts @@ -1,4 +1,21 @@ +import EventProvider from "./EventProvider.js"; +import type UI5Element from "./UI5Element.js"; + +abstract class ComponentFeature { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty-function + constructor(...args: any[]) {} + static define?: () => Promise; + static dependencies?: Array +} + const features = new Map(); +const componentFeatures = new Map(); +const subscribers = new Map>(); + +const EVENT_NAME = "componentFeatureLoad"; +const eventProvider = new EventProvider(); + +const featureLoadEventName = (name: string) => `${EVENT_NAME}_${name}`; const registerFeature = (name: string, feature: object) => { features.set(name, feature); @@ -8,7 +25,44 @@ const getFeature = (name: string): T => { return features.get(name) as T; }; +const registerComponentFeature = async (name: string, feature: typeof ComponentFeature) => { + await Promise.all(feature.dependencies?.map(dep => dep.define()) || []); + await feature.define?.(); + + componentFeatures.set(name, feature); + notifyForFeatureLoad(name); +}; + +const getComponentFeature = (name: string): T => { + return componentFeatures.get(name) as T; +}; + +const subscribeForFeatureLoad = (name: string, klass: typeof UI5Element, callback: () => void) => { + const subscriber = subscribers.get(klass); + const isSubscribed = subscriber?.includes(name); + + if (isSubscribed) { + return; + } + + if (!subscriber) { + subscribers.set(klass, [name]); + } else { + subscriber.push(name); + } + + eventProvider.attachEvent(featureLoadEventName(name), callback); +}; + +const notifyForFeatureLoad = (name: string) => { + eventProvider.fireEvent(featureLoadEventName(name), undefined); +}; + export { registerFeature, getFeature, + registerComponentFeature, + getComponentFeature, + subscribeForFeatureLoad, + ComponentFeature, }; diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts index cae08029c808..306f3e85aac4 100644 --- a/packages/base/src/UI5Element.ts +++ b/packages/base/src/UI5Element.ts @@ -37,6 +37,7 @@ import type { } from "./types.js"; import { attachFormElementInternals, setFormValue } from "./features/InputElementsFormSupport.js"; import type { IFormInputElement } from "./features/InputElementsFormSupport.js"; +import { subscribeForFeatureLoad } from "./FeaturesRegistry.js"; const DEV_MODE = true; let autoId = 0; @@ -1168,6 +1169,11 @@ abstract class UI5Element extends HTMLElement { return []; } + static cacheUniqueDependencies(this: typeof UI5Element): void { + const filtered = this.dependencies.filter((dep, index, deps) => deps.indexOf(dep) === index); + uniqueDependenciesCache.set(this, filtered); + } + /** * Returns a list of the unique dependencies for this UI5 Web Component * @@ -1175,8 +1181,7 @@ abstract class UI5Element extends HTMLElement { */ static getUniqueDependencies(this: typeof UI5Element): Array { if (!uniqueDependenciesCache.has(this)) { - const filtered = this.dependencies.filter((dep, index, deps) => deps.indexOf(dep) === index); - uniqueDependenciesCache.set(this, filtered); + this.cacheUniqueDependencies(); } return uniqueDependenciesCache.get(this) || []; @@ -1212,6 +1217,12 @@ abstract class UI5Element extends HTMLElement { const tag = this.getMetadata().getTag(); + const features = this.getMetadata().getFeatures(); + + features.forEach(feature => { + subscribeForFeatureLoad(feature, this, this.cacheUniqueDependencies.bind(this)); + }); + const definedLocally = isTagRegistered(tag); const definedGlobally = customElements.get(tag); diff --git a/packages/base/src/UI5ElementMetadata.ts b/packages/base/src/UI5ElementMetadata.ts index 0a3a889dcf07..b9ff81fee95b 100644 --- a/packages/base/src/UI5ElementMetadata.ts +++ b/packages/base/src/UI5ElementMetadata.ts @@ -41,6 +41,7 @@ type Metadata = { languageAware?: boolean, formAssociated?: boolean, shadowRootOptions?: Partial + features?: Array }; type State = Record>; @@ -96,6 +97,14 @@ class UI5ElementMetadata { return this.metadata.tag || ""; } + /** + * Returns the tag of the UI5 Element without the scope + * @private + */ + getFeatures(): Array { + return this.metadata.features || []; + } + /** * Returns the tag of the UI5 Element * @public diff --git a/packages/base/src/decorators/customElement.ts b/packages/base/src/decorators/customElement.ts index d00e19bc3a5f..4f4808f1280b 100644 --- a/packages/base/src/decorators/customElement.ts +++ b/packages/base/src/decorators/customElement.ts @@ -20,6 +20,7 @@ const customElement = (tagNameOrComponentSettings: string | { fastNavigation?: boolean, formAssociated?: boolean, shadowRootOptions?: Partial, + features?: Array, } = {}): ClassDecorator => { return (target: any) => { if (!Object.prototype.hasOwnProperty.call(target, "metadata")) { @@ -38,12 +39,18 @@ const customElement = (tagNameOrComponentSettings: string | { fastNavigation, formAssociated, shadowRootOptions, + features, } = tagNameOrComponentSettings; target.metadata.tag = tag; if (languageAware) { target.metadata.languageAware = languageAware; } + + if (features) { + target.metadata.features = features; + } + if (themeAware) { target.metadata.themeAware = themeAware; } diff --git a/packages/main/src/ColorPalette.ts b/packages/main/src/ColorPalette.ts index 78df1bf7aac1..fd2bc56b3359 100644 --- a/packages/main/src/ColorPalette.ts +++ b/packages/main/src/ColorPalette.ts @@ -17,7 +17,7 @@ import { isUp, isTabNext, } from "@ui5/webcomponents-base/dist/Keys.js"; -import { getFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js"; +import { getComponentFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js"; import ColorPaletteTemplate from "./generated/templates/ColorPaletteTemplate.lit.js"; import ColorPaletteItem from "./ColorPaletteItem.js"; import Button from "./Button.js"; @@ -73,10 +73,11 @@ type ColorPaletteItemClickEventDetail = { @customElement({ tag: "ui5-color-palette", renderer: litRender, + features: ["ColorPaletteMoreColors"], template: ColorPaletteTemplate, styles: [ColorPaletteCss, ColorPaletteDialogCss], get dependencies() { - const colorPaletteMoreColors = getFeature("ColorPaletteMoreColors"); + const colorPaletteMoreColors = getComponentFeature("ColorPaletteMoreColors"); return ([ColorPaletteItem, Button] as Array).concat(colorPaletteMoreColors ? colorPaletteMoreColors.dependencies : []); }, }) @@ -172,19 +173,14 @@ class ColorPalette extends UI5Element { _itemNavigation: ItemNavigation; _itemNavigationRecentColors: ItemNavigation; _recentColors: Array; - moreColorsFeature: ColorPaletteMoreColors | Record = {}; + moreColorsFeature?: ColorPaletteMoreColors; _currentlySelected?: ColorPaletteItem; _shouldFocusRecentColors = false; static i18nBundle: I18nBundle; static async onDefine() { - const colorPaletteMoreColors = getFeature("ColorPaletteMoreColors"); - - [ColorPalette.i18nBundle] = await Promise.all([ - getI18nBundle("@ui5/webcomponents"), - colorPaletteMoreColors ? colorPaletteMoreColors.init() : Promise.resolve(), - ]); + ColorPalette.i18nBundle = await getI18nBundle("@ui5/webcomponents"); } constructor() { @@ -218,17 +214,19 @@ class ColorPalette extends UI5Element { }); if (this.showMoreColors) { - const ColorPaletteMoreColorsClass = getFeature("ColorPaletteMoreColors"); + const ColorPaletteMoreColorsClass = getComponentFeature("ColorPaletteMoreColors"); if (ColorPaletteMoreColorsClass) { this.moreColorsFeature = new ColorPaletteMoreColorsClass(); - } else { - throw new Error(`You have to import "@ui5/webcomponents/dist/features/ColorPaletteMoreColors.js" module to use the more-colors functionality.`); } } this.onPhone = isPhone(); } + get _effectiveShowMoreColors() { + return !!(this.showMoreColors && this.moreColorsFeature); + } + onAfterRendering() { if (this._shouldFocusRecentColors && this.hasRecentColors) { this.recentColorsElements[0].selected = true; @@ -528,6 +526,18 @@ class ColorPalette extends UI5Element { return [...this.effectiveColorItems, ...this.recentColorsElements]; } + get colorPaletteDialogTitle() { + return this.moreColorsFeature?.colorPaletteDialogTitle; + } + + get colorPaletteDialogOKButton() { + return this.moreColorsFeature?.colorPaletteDialogOKButton; + } + + get colorPaletteCancelButton() { + return this.moreColorsFeature?.colorPaletteCancelButton; + } + /** * Returns the selected color. */ diff --git a/packages/main/src/ColorPaletteDialog.hbs b/packages/main/src/ColorPaletteDialog.hbs index d4c61ed69d11..cbf27bff12c5 100644 --- a/packages/main/src/ColorPaletteDialog.hbs +++ b/packages/main/src/ColorPaletteDialog.hbs @@ -1,20 +1,20 @@ -{{#if _showMoreColors}} - -
- -
+{{#if _effectiveShowMoreColors}} + +
+ +
- -
+ +
{{/if}} diff --git a/packages/main/src/Input.hbs b/packages/main/src/Input.hbs index e72102f68241..579882f05e9c 100644 --- a/packages/main/src/Input.hbs +++ b/packages/main/src/Input.hbs @@ -60,7 +60,7 @@ {{> postContent }} - {{#if showSuggestions}} + {{#if _effectiveShowSuggestions}} {{suggestionsText}} {{availableSuggestionsCount}} diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 544843f21d34..71a263b0b1ff 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -16,7 +16,7 @@ import { isAndroid, } from "@ui5/webcomponents-base/dist/Device.js"; import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; -import { getFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js"; +import { getComponentFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js"; import { isUp, isDown, @@ -201,8 +201,9 @@ type InputSuggestionScrollEventDetail = { ValueStateMessageCss, SuggestionsCss, ], + features: ["InputSuggestions"], get dependencies() { - const Suggestions = getFeature("InputSuggestions"); + const Suggestions = getComponentFeature("InputSuggestions"); return ([Popover, ResponsivePopover, Icon] as Array).concat(Suggestions ? Suggestions.dependencies : []); }, }) @@ -585,6 +586,10 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement return Input.i18nBundle.getText(FORM_TEXTFIELD_REQUIRED); } + get _effectiveShowSuggestions() { + return !!(this.showSuggestions && this.Suggestions); + } + get formValidity(): ValidityStateFlags { return { valueMissing: this.required && !this.value }; } @@ -716,7 +721,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement onAfterRendering() { const innerInput = this.getInputDOMRefSync()!; - if (this.Suggestions && this.showSuggestions && this.Suggestions._getPicker()) { + if (this.showSuggestions && this.Suggestions?._getPicker()) { this._listWidth = this.Suggestions._getListWidth(); } @@ -800,13 +805,13 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } _handleUp(e: KeyboardEvent) { - if (this.Suggestions && this.Suggestions.isOpened()) { + if (this.Suggestions?.isOpened()) { this.Suggestions.onUp(e); } } _handleDown(e: KeyboardEvent) { - if (this.Suggestions && this.Suggestions.isOpened()) { + if (this.Suggestions?.isOpened()) { this.Suggestions.onDown(e); } } @@ -825,7 +830,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handleEnter(e: KeyboardEvent) { // if a group item is focused, this is false - const suggestionItemPressed = !!(this.Suggestions && this.Suggestions.onEnter(e)); + const suggestionItemPressed = !!(this.Suggestions?.onEnter(e)); const innerInput = this.getInputDOMRefSync()!; const matchingItem = this._selectableItems.find(item => { return item.text === this.value; @@ -861,7 +866,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handlePageUp(e: KeyboardEvent) { if (this._isSuggestionsFocused) { - this.Suggestions!.onPageUp(e); + this.Suggestions?.onPageUp(e); } else { e.preventDefault(); } @@ -869,7 +874,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handlePageDown(e: KeyboardEvent) { if (this._isSuggestionsFocused) { - this.Suggestions!.onPageDown(e); + this.Suggestions?.onPageDown(e); } else { e.preventDefault(); } @@ -877,13 +882,13 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handleHome(e: KeyboardEvent) { if (this._isSuggestionsFocused) { - this.Suggestions!.onHome(e); + this.Suggestions?.onHome(e); } } _handleEnd(e: KeyboardEvent) { if (this._isSuggestionsFocused) { - this.Suggestions!.onEnd(e); + this.Suggestions?.onEnd(e); } } @@ -900,7 +905,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement return; } - if (isOpen && this.Suggestions!._isItemOnTarget()) { + if (isOpen && this.Suggestions?._isItemOnTarget()) { // Restore the value. this.value = this.typedInValue || this.valueBeforeSelectionStart; @@ -971,8 +976,8 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._isValueStateFocused = false; this.hasSuggestionItemSelected = false; - this.Suggestions._deselectItems(); - this.Suggestions._clearItemFocus(); + this.Suggestions?._deselectItems(); + this.Suggestions?._clearItemFocus(); } _click() { @@ -1216,12 +1221,9 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement return; } - const Suggestions = getFeature("InputSuggestions"); - + const Suggestions = getComponentFeature("InputSuggestions"); if (Suggestions) { this.Suggestions = new Suggestions(this, "suggestionItems", true, false); - } else { - throw new Error(`You have to import "@ui5/webcomponents/dist/features/InputSuggestions.js" module to use ui5-input suggestions`); } } @@ -1337,7 +1339,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement return Promise.resolve(false); } - return this.Suggestions._isScrollable(); + return this.Suggestions?._isScrollable(); } onItemMouseDown(e: MouseEvent) { @@ -1573,7 +1575,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } get availableSuggestionsCount() { - if (this.showSuggestions && (this.value || this.Suggestions!.isOpened())) { + if (this.showSuggestions && (this.value || this.Suggestions?.isOpened())) { const nonGroupItems = this._selectableItems; switch (nonGroupItems.length) { @@ -1600,7 +1602,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } get _isSuggestionsFocused() { - return !this.focused && this.Suggestions && this.Suggestions.isOpened(); + return !this.focused && this.Suggestions?.isOpened(); } /** @@ -1683,12 +1685,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } static async onDefine() { - const Suggestions = getFeature("InputSuggestions"); - - [Input.i18nBundle] = await Promise.all([ - getI18nBundle("@ui5/webcomponents"), - Suggestions ? Suggestions.init() : Promise.resolve(), - ]); + Input.i18nBundle = await getI18nBundle("@ui5/webcomponents"); } } diff --git a/packages/main/src/InputPopover.hbs b/packages/main/src/InputPopover.hbs index 5d50ac15c5fe..b18d4a33d5b2 100644 --- a/packages/main/src/InputPopover.hbs +++ b/packages/main/src/InputPopover.hbs @@ -1,4 +1,4 @@ -{{#if showSuggestions}} +{{#if _effectiveShowSuggestions}}