@progress/kendo-angular-buttons
Version:
Buttons Package for Angular
523 lines (522 loc) • 18.8 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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']
}] } });