UNPKG

@progress/kendo-angular-buttons

Version:
523 lines (522 loc) 18.8 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgZone, Output, Renderer2 } from '@angular/core'; import { from, Observable, of, Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { microphoneOutlineIcon, stopSmIcon } from '@progress/kendo-svg-icons'; import { KendoSpeechRecognition } from '@progress/kendo-webspeech-common'; import { IconWrapperComponent } from '@progress/kendo-angular-icons'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { anyChanged, isChanged, isDocumentAvailable, isFirefox, isSafari } from '@progress/kendo-angular-common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { getStylingClasses, getThemeColorClasses, toggleClass } from '../util'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; const DEFAULT_ROUNDED = 'medium'; const DEFAULT_SIZE = 'medium'; const DEFAULT_THEME_COLOR = 'base'; const DEFAULT_FILL_MODE = 'solid'; /** * Represents the Kendo UI SpeechToTextButton component for Angular. * * @example * ```html * <button kendoSpeechToTextButton></button> * ``` */ export class SpeechToTextButtonComponent { renderer; ngZone; /** * When `true`, disables the SpeechToTextButton and prevents user interaction. * * @default false */ set disabled(disabled) { //Required, because in FF focused buttons are not blurred on disabled if (disabled && isDocumentAvailable() && isFirefox(navigator.userAgent)) { this.blur(); } this.isDisabled = disabled; this.renderer.setProperty(this.element, 'disabled', disabled); } get disabled() { return this.isDisabled; } /** * Sets the padding of the SpeechToTextButton. * * @default 'medium' */ set size(size) { const newSize = size || DEFAULT_SIZE; this.handleClasses(newSize, 'size'); this._size = newSize; } get size() { return this._size; } /** * Sets the border radius of the SpeechToTextButton. * * @default 'medium' */ set rounded(rounded) { const newRounded = rounded || DEFAULT_ROUNDED; this.handleClasses(newRounded, 'rounded'); this._rounded = newRounded; } get rounded() { return this._rounded; } /** * Sets the background and border styles of the SpeechToTextButton. * * @default 'solid' */ set fillMode(fillMode) { const newFillMode = fillMode || DEFAULT_FILL_MODE; this.handleClasses(newFillMode, 'fillMode'); this._fillMode = newFillMode; } get fillMode() { return this._fillMode; } /** * Sets a predefined theme color for the SpeechToTextButton. * The theme color applies as a background and border color and adjusts the text color. * * @default 'base' */ set themeColor(themeColor) { const newThemeColor = themeColor || DEFAULT_THEME_COLOR; this.handleThemeColor(newThemeColor); this._themeColor = newThemeColor; } get themeColor() { return this._themeColor; } /** * Specifies which speech recognition engine or integration the component should use. Allows the component to operate in different environments or use alternative implementations. */ integrationMode = 'webSpeech'; /** * Specifies a `BCP 47` language tag (e.g., 'en-US', 'bg-BG') used for speech recognition. * * @default 'en-US' */ lang = 'en-US'; /** * Specifies whether continuous results are returned for each recognition, or only a single result once recognition stops. * * @default false */ continuous = false; /** * Specifies whether interim results should be returned or not. Interim results are results that are not yet final. * * @default false */ interimResults = false; /** * Represents the maximum number of alternative transcriptions to return for each result. * * @default 1 */ maxAlternatives = 1; /** * Fires when the speech recognition service has begun listening to incoming audio. */ start = new EventEmitter(); /** * Fires when the speech recognition service has disconnected. */ end = new EventEmitter(); /** * Fires when the speech recognition service returns a result - a word or phrase has been positively recognized. */ result = new EventEmitter(); /** * Fires when a speech recognition error occurs. The event argument is a string, containing the error message. */ error = new EventEmitter(); /** * Fires when the user clicks the SpeechToTextButton. */ click = new EventEmitter(); get iconButtonClass() { return !this.hasText; } get listeningClass() { return this.isListening; } speechToTextButtonClass = true; classButton = true; get classDisabled() { return this.isDisabled; } get getDirection() { return this.direction; } get ariaPressed() { return this.isListening; } /** * @hidden */ onFocus() { this.isFocused = true; } /** * @hidden */ onBlur() { this.isFocused = false; } /** * Focuses the SpeechToTextButton component. */ focus() { if (isDocumentAvailable()) { this.element.focus(); this.isFocused = true; } } /** * Removes focus from the SpeechToTextButton component. */ blur() { if (isDocumentAvailable()) { this.element.blur(); this.isFocused = false; } } ngOnInit() { this.ngZone.runOutsideAngular(() => { this.subs.add(this.renderer.listen(this.element, 'click', this.onClick.bind(this))); this.subs.add(this.renderer.listen(this.element, 'mousedown', (event) => { const isBrowserSafari = isDocumentAvailable() && isSafari(navigator.userAgent); if (!this.isDisabled && isBrowserSafari) { event.preventDefault(); this.element.focus(); } })); if (this.integrationMode !== 'webSpeech') { return; } this.createWebSpeech(); }); } ngOnChanges(changes) { if (isChanged("integrationMode", changes, false)) { if (this.integrationMode === 'webSpeech') { if (!this.speechRecognition) { this.ngZone.runOutsideAngular(() => { this.createWebSpeech(); }); } } else { this.destroyWebSpeech(); } } if (anyChanged(['lang', 'interimResults', 'maxAlternatives', 'continuous'], changes)) { if (this.speechRecognition) { this.speechRecognition.setOptions({ lang: this.lang, interimResults: this.interimResults, maxAlternatives: this.maxAlternatives, continuous: this.continuous }); } } } ngAfterViewInit() { const stylingOptions = ['size', 'rounded', 'fillMode']; stylingOptions.forEach(input => { this.handleClasses(this[input], input); }); } ngOnDestroy() { this.destroyWebSpeech(); this.subs.unsubscribe(); } constructor(element, renderer, localization, ngZone) { this.renderer = renderer; this.ngZone = ngZone; validatePackage(packageMetadata); this.direction = localization.rtl ? 'rtl' : 'ltr'; this.subs.add(localization.changes.subscribe(({ rtl }) => (this.direction = rtl ? 'rtl' : 'ltr'))); this.element = element.nativeElement; } /** * Indicates whether the button is actively listening for incoming audio. */ isListening = false; /** * Indicates whether web speech functionality is supported. */ get isWebSpeechSupported() { return this.speechRecognition ? this.speechRecognition.isSupported() : false; } set isFocused(isFocused) { toggleClass('k-focus', isFocused, this.renderer, this.element); this._focused = isFocused; } get isFocused() { return this._focused; } /** * @hidden */ get hasText() { return isDocumentAvailable() && this.element.textContent.trim().length > 0; } /** * @hidden */ onClick() { if (this.isWebSpeechSupported && this.integrationMode === 'webSpeech') { this.ngZone.run(() => { this.isListening ? this.speechRecognition.stop() : this.speechRecognition.start(); }); } else if (this.integrationMode === 'none') { let asyncFactory = () => of(null); this.ngZone.run(() => { this.isListening ? this.end.emit(fn => asyncFactory = fn) : this.start.emit(fn => asyncFactory = fn); const result = asyncFactory(); const observable = this.toObservable(result); observable.pipe(take(1)).subscribe(() => this.isListening = !this.isListening); }); } } /** * @hidden */ get buttonSvgIcon() { return this.isListening ? this.stopSvgIcon : this.microphoneSvgIcon; } /** * @hidden */ get buttonIcon() { return this.isListening ? 'stop-sm' : 'microphone-outline'; } /** * @hidden */ setAttribute(attribute, value) { this.renderer.setAttribute(this.element, attribute, value); } /** * @hidden */ element; /** * @hidden */ isDisabled = false; /** * @hidden */ subs = new Subscription(); microphoneSvgIcon = microphoneOutlineIcon; stopSvgIcon = stopSmIcon; speechRecognition; _size = DEFAULT_SIZE; _rounded = DEFAULT_ROUNDED; _fillMode = DEFAULT_FILL_MODE; _themeColor = DEFAULT_THEME_COLOR; _focused = false; direction; handleClasses(value, input) { const elem = this.element; const classes = getStylingClasses('button', input, this[input], value); if (input === 'fillMode') { this.handleThemeColor(this.themeColor, this[input], value); } if (classes.toRemove) { this.renderer.removeClass(elem, classes.toRemove); } if (classes.toAdd) { this.renderer.addClass(elem, classes.toAdd); } } handleStart() { this.ngZone.run(() => { this.isListening = true; this.start.emit(); }); } handleEnd() { this.ngZone.run(() => { this.isListening = false; this.end.emit(); }); } handleResult(event) { const results = event.results; const lastResultIndex = results.length - 1; const lastResult = results[lastResultIndex]; const alternatives = []; for (let i = 0; i < lastResult.length; i++) { alternatives.push({ transcript: lastResult[i].transcript, confidence: lastResult[i].confidence }); } const args = { isFinal: lastResult.isFinal, alternatives: alternatives, }; this.ngZone.run(() => { this.result.emit(args); }); } handleError(ev) { const errorMessage = ev.error || ev.message || 'Unknown error'; this.ngZone.run(() => { this.error.emit({ errorMessage }); }); } toObservable(input) { return input instanceof Observable ? input : from(input); } handleThemeColor(value, prevFillMode, fillMode) { const elem = this.element; const removeFillMode = prevFillMode || this.fillMode; const addFillMode = fillMode || this.fillMode; const themeColorClass = getThemeColorClasses('button', removeFillMode, addFillMode, this.themeColor, value); this.renderer.removeClass(elem, themeColorClass.toRemove); if (addFillMode !== 'none' && fillMode !== 'none') { if (themeColorClass.toAdd) { this.renderer.addClass(elem, themeColorClass.toAdd); } } } destroyWebSpeech() { if (this.speechRecognition) { this.speechRecognition.stop(); this.speechRecognition.destroy(); this.speechRecognition = null; this.isListening = false; } } createWebSpeech() { if (!isDocumentAvailable()) { return; } this.speechRecognition = new KendoSpeechRecognition({ lang: this.lang, interimResults: this.interimResults, maxAlternatives: this.maxAlternatives, continuous: this.continuous, events: { start: this.handleStart.bind(this), end: this.handleEnd.bind(this), result: this.handleResult.bind(this), error: this.handleError.bind(this) } }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i1.LocalizationService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SpeechToTextButtonComponent, isStandalone: true, selector: "button[kendoSpeechToTextButton]", inputs: { disabled: "disabled", size: "size", rounded: "rounded", fillMode: "fillMode", themeColor: "themeColor", integrationMode: "integrationMode", lang: "lang", continuous: "continuous", interimResults: "interimResults", maxAlternatives: "maxAlternatives" }, outputs: { start: "start", end: "end", result: "result", error: "error", click: "click" }, host: { listeners: { "focus": "onFocus()", "blur": "onBlur()" }, properties: { "class.k-icon-button": "this.iconButtonClass", "class.k-listening": "this.listeningClass", "class.k-speech-to-text-button": "this.speechToTextButtonClass", "class.k-button": "this.classButton", "class.k-disabled": "this.classDisabled", "attr.dir": "this.getDirection", "attr.aria-pressed": "this.ariaPressed" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.speechtotextbutton' } ], exportAs: ["kendoSpeechToTextButton"], usesOnChanges: true, ngImport: i0, template: ` <kendo-icon-wrapper innerCssClass="k-button-icon" [name]="buttonIcon" [svgIcon]="buttonSvgIcon"> </kendo-icon-wrapper> <span class="k-button-text"><ng-content></ng-content></span> `, isInline: true, dependencies: [{ kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoSpeechToTextButton', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.speechtotextbutton' } ], selector: 'button[kendoSpeechToTextButton]', template: ` <kendo-icon-wrapper innerCssClass="k-button-icon" [name]="buttonIcon" [svgIcon]="buttonSvgIcon"> </kendo-icon-wrapper> <span class="k-button-text"><ng-content></ng-content></span> `, standalone: true, imports: [IconWrapperComponent] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i0.NgZone }]; }, propDecorators: { disabled: [{ type: Input }], size: [{ type: Input }], rounded: [{ type: Input }], fillMode: [{ type: Input }], themeColor: [{ type: Input }], integrationMode: [{ type: Input }], lang: [{ type: Input }], continuous: [{ type: Input }], interimResults: [{ type: Input }], maxAlternatives: [{ type: Input }], start: [{ type: Output }], end: [{ type: Output }], result: [{ type: Output }], error: [{ type: Output }], click: [{ type: Output }], iconButtonClass: [{ type: HostBinding, args: ['class.k-icon-button'] }], listeningClass: [{ type: HostBinding, args: ['class.k-listening'] }], speechToTextButtonClass: [{ type: HostBinding, args: ['class.k-speech-to-text-button'] }], classButton: [{ type: HostBinding, args: ['class.k-button'] }], classDisabled: [{ type: HostBinding, args: ['class.k-disabled'] }], getDirection: [{ type: HostBinding, args: ['attr.dir'] }], ariaPressed: [{ type: HostBinding, args: ['attr.aria-pressed'] }], onFocus: [{ type: HostListener, args: ['focus'] }], onBlur: [{ type: HostListener, args: ['blur'] }] } });