@progress/kendo-angular-buttons 19.2.1-develop.3 → 19.3.0-develop.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,519 @@
1
+ /**-----------------------------------------------------------------------------------------
2
+ * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
+ * Licensed under commercial license. See LICENSE.md in the project root for more information
4
+ *-------------------------------------------------------------------------------------------*/
5
+ import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgZone, Output, Renderer2 } from '@angular/core';
6
+ import { from, Observable, of, Subscription } from 'rxjs';
7
+ import { take } from 'rxjs/operators';
8
+ import { microphoneOutlineIcon, stopSmIcon } from '@progress/kendo-svg-icons';
9
+ import { KendoSpeechRecognition } from '@progress/kendo-webspeech-common';
10
+ import { IconWrapperComponent } from '@progress/kendo-angular-icons';
11
+ import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
12
+ import { anyChanged, isChanged, isDocumentAvailable, isFirefox, isSafari } from '@progress/kendo-angular-common';
13
+ import { validatePackage } from '@progress/kendo-licensing';
14
+ import { packageMetadata } from '../package-metadata';
15
+ import { getStylingClasses, getThemeColorClasses, toggleClass } from '../util';
16
+ import * as i0 from "@angular/core";
17
+ import * as i1 from "@progress/kendo-angular-l10n";
18
+ const DEFAULT_ROUNDED = 'medium';
19
+ const DEFAULT_SIZE = 'medium';
20
+ const DEFAULT_THEME_COLOR = 'base';
21
+ const DEFAULT_FILL_MODE = 'solid';
22
+ /**
23
+ * Represents the Kendo UI SpeechToTextButton component for Angular.
24
+ *
25
+ * @example
26
+ * ```html
27
+ * <button kendoSpeechToTextButton></button>
28
+ * ```
29
+ */
30
+ export class SpeechToTextButtonComponent {
31
+ renderer;
32
+ ngZone;
33
+ /**
34
+ * When `true`, disables the SpeechToTextButton and prevents user interaction.
35
+ *
36
+ * @default false
37
+ */
38
+ set disabled(disabled) {
39
+ //Required, because in FF focused buttons are not blurred on disabled
40
+ if (disabled && isDocumentAvailable() && isFirefox(navigator.userAgent)) {
41
+ this.blur();
42
+ }
43
+ this.isDisabled = disabled;
44
+ this.renderer.setProperty(this.element, 'disabled', disabled);
45
+ }
46
+ get disabled() {
47
+ return this.isDisabled;
48
+ }
49
+ /**
50
+ * Sets the padding of the SpeechToTextButton.
51
+ *
52
+ * @default 'medium'
53
+ */
54
+ set size(size) {
55
+ const newSize = size || DEFAULT_SIZE;
56
+ this.handleClasses(newSize, 'size');
57
+ this._size = newSize;
58
+ }
59
+ get size() {
60
+ return this._size;
61
+ }
62
+ /**
63
+ * Sets the border radius of the SpeechToTextButton.
64
+ *
65
+ * @default 'medium'
66
+ */
67
+ set rounded(rounded) {
68
+ const newRounded = rounded || DEFAULT_ROUNDED;
69
+ this.handleClasses(newRounded, 'rounded');
70
+ this._rounded = newRounded;
71
+ }
72
+ get rounded() {
73
+ return this._rounded;
74
+ }
75
+ /**
76
+ * Sets the background and border styles of the SpeechToTextButton.
77
+ *
78
+ * @default 'solid'
79
+ */
80
+ set fillMode(fillMode) {
81
+ const newFillMode = fillMode || DEFAULT_FILL_MODE;
82
+ this.handleClasses(newFillMode, 'fillMode');
83
+ this._fillMode = newFillMode;
84
+ }
85
+ get fillMode() {
86
+ return this._fillMode;
87
+ }
88
+ /**
89
+ * Sets a predefined theme color for the SpeechToTextButton.
90
+ * The theme color applies as a background and border color and adjusts the text color.
91
+ *
92
+ * @default 'base'
93
+ */
94
+ set themeColor(themeColor) {
95
+ const newThemeColor = themeColor || DEFAULT_THEME_COLOR;
96
+ this.handleThemeColor(newThemeColor);
97
+ this._themeColor = newThemeColor;
98
+ }
99
+ get themeColor() {
100
+ return this._themeColor;
101
+ }
102
+ /**
103
+ * Specifies which speech recognition engine or integration the component should use. Allows the component to operate in different environments or use alternative implementations.
104
+ */
105
+ integrationMode = 'webSpeech';
106
+ /**
107
+ * Specifies a `BCP 47` language tag (e.g., 'en-US', 'bg-BG') used for speech recognition.
108
+ *
109
+ * @default 'en-US'
110
+ */
111
+ lang = 'en-US';
112
+ /**
113
+ * Specifies whether continuous results are returned for each recognition, or only a single result once recognition stops.
114
+ *
115
+ * @default false
116
+ */
117
+ continuous = false;
118
+ /**
119
+ * Specifies whether interim results should be returned or not. Interim results are results that are not yet final.
120
+ *
121
+ * @default false
122
+ */
123
+ interimResults = false;
124
+ /**
125
+ * Represents the maximum number of alternative transcriptions to return for each result.
126
+ *
127
+ * @default 1
128
+ */
129
+ maxAlternatives = 1;
130
+ /**
131
+ * Fires when the speech recognition service has begun listening to incoming audio.
132
+ */
133
+ start = new EventEmitter();
134
+ /**
135
+ * Fires when the speech recognition service has disconnected.
136
+ */
137
+ end = new EventEmitter();
138
+ /**
139
+ * Fires when the speech recognition service returns a result - a word or phrase has been positively recognized.
140
+ */
141
+ result = new EventEmitter();
142
+ /**
143
+ * Fires when a speech recognition error occurs. The event argument is a string, containing the error message.
144
+ */
145
+ error = new EventEmitter();
146
+ /**
147
+ * Fires when the user clicks the SpeechToTextButton.
148
+ */
149
+ click = new EventEmitter();
150
+ get iconButtonClass() {
151
+ return !this.hasText;
152
+ }
153
+ get listeningClass() {
154
+ return this.isListening;
155
+ }
156
+ speechToTextButtonClass = true;
157
+ classButton = true;
158
+ get classDisabled() {
159
+ return this.isDisabled;
160
+ }
161
+ get getDirection() {
162
+ return this.direction;
163
+ }
164
+ get ariaPressed() {
165
+ return this.isListening;
166
+ }
167
+ /**
168
+ * @hidden
169
+ */
170
+ onFocus() {
171
+ this.isFocused = true;
172
+ }
173
+ /**
174
+ * @hidden
175
+ */
176
+ onBlur() {
177
+ this.isFocused = false;
178
+ }
179
+ /**
180
+ * Focuses the SpeechToTextButton component.
181
+ */
182
+ focus() {
183
+ if (isDocumentAvailable()) {
184
+ this.element.focus();
185
+ this.isFocused = true;
186
+ }
187
+ }
188
+ /**
189
+ * Removes focus from the SpeechToTextButton component.
190
+ */
191
+ blur() {
192
+ if (isDocumentAvailable()) {
193
+ this.element.blur();
194
+ this.isFocused = false;
195
+ }
196
+ }
197
+ ngOnInit() {
198
+ this.ngZone.runOutsideAngular(() => {
199
+ this.subs.add(this.renderer.listen(this.element, 'click', this.onClick.bind(this)));
200
+ this.subs.add(this.renderer.listen(this.element, 'mousedown', (event) => {
201
+ const isBrowserSafari = isDocumentAvailable() && isSafari(navigator.userAgent);
202
+ if (!this.isDisabled && isBrowserSafari) {
203
+ event.preventDefault();
204
+ this.element.focus();
205
+ }
206
+ }));
207
+ if (this.integrationMode !== 'webSpeech') {
208
+ return;
209
+ }
210
+ this.createWebSpeech();
211
+ });
212
+ }
213
+ ngOnChanges(changes) {
214
+ if (isChanged("integrationMode", changes, false)) {
215
+ if (this.integrationMode === 'webSpeech') {
216
+ if (!this.speechRecognition) {
217
+ this.ngZone.runOutsideAngular(() => {
218
+ this.createWebSpeech();
219
+ });
220
+ }
221
+ }
222
+ else {
223
+ this.destroyWebSpeech();
224
+ }
225
+ }
226
+ if (anyChanged(['lang', 'interimResults', 'maxAlternatives', 'continuous'], changes)) {
227
+ if (this.speechRecognition) {
228
+ this.speechRecognition.setOptions({
229
+ lang: this.lang,
230
+ interimResults: this.interimResults,
231
+ maxAlternatives: this.maxAlternatives,
232
+ continuous: this.continuous
233
+ });
234
+ }
235
+ }
236
+ }
237
+ ngAfterViewInit() {
238
+ const stylingOptions = ['size', 'rounded', 'fillMode'];
239
+ stylingOptions.forEach(input => {
240
+ this.handleClasses(this[input], input);
241
+ });
242
+ }
243
+ ngOnDestroy() {
244
+ this.destroyWebSpeech();
245
+ this.subs.unsubscribe();
246
+ }
247
+ constructor(element, renderer, localization, ngZone) {
248
+ this.renderer = renderer;
249
+ this.ngZone = ngZone;
250
+ validatePackage(packageMetadata);
251
+ this.direction = localization.rtl ? 'rtl' : 'ltr';
252
+ this.subs.add(localization.changes.subscribe(({ rtl }) => (this.direction = rtl ? 'rtl' : 'ltr')));
253
+ this.element = element.nativeElement;
254
+ }
255
+ /**
256
+ * Indicates whether the button is actively listening for incoming audio.
257
+ */
258
+ isListening = false;
259
+ /**
260
+ * Indicates whether web speech functionality is supported.
261
+ */
262
+ get isWebSpeechSupported() {
263
+ return this.speechRecognition ? this.speechRecognition.isSupported() : false;
264
+ }
265
+ set isFocused(isFocused) {
266
+ toggleClass('k-focus', isFocused, this.renderer, this.element);
267
+ this._focused = isFocused;
268
+ }
269
+ get isFocused() {
270
+ return this._focused;
271
+ }
272
+ /**
273
+ * @hidden
274
+ */
275
+ get hasText() {
276
+ return isDocumentAvailable() && this.element.textContent.trim().length > 0;
277
+ }
278
+ /**
279
+ * @hidden
280
+ */
281
+ onClick() {
282
+ if (this.isWebSpeechSupported && this.integrationMode === 'webSpeech') {
283
+ this.ngZone.run(() => {
284
+ this.isListening ? this.speechRecognition.stop() : this.speechRecognition.start();
285
+ });
286
+ }
287
+ else if (this.integrationMode === 'none') {
288
+ let asyncFactory = () => of(null);
289
+ this.ngZone.run(() => {
290
+ this.isListening ? this.end.emit(fn => asyncFactory = fn) : this.start.emit(fn => asyncFactory = fn);
291
+ const result = asyncFactory();
292
+ const observable = this.toObservable(result);
293
+ observable.pipe(take(1)).subscribe(() => this.isListening = !this.isListening);
294
+ });
295
+ }
296
+ }
297
+ /**
298
+ * @hidden
299
+ */
300
+ get buttonSvgIcon() {
301
+ return this.isListening ? this.stopSvgIcon : this.microphoneSvgIcon;
302
+ }
303
+ /**
304
+ * @hidden
305
+ */
306
+ get buttonIcon() {
307
+ return this.isListening ? 'stop-sm' : 'microphone-outline';
308
+ }
309
+ /**
310
+ * @hidden
311
+ */
312
+ setAttribute(attribute, value) {
313
+ this.renderer.setAttribute(this.element, attribute, value);
314
+ }
315
+ /**
316
+ * @hidden
317
+ */
318
+ element;
319
+ /**
320
+ * @hidden
321
+ */
322
+ isDisabled = false;
323
+ /**
324
+ * @hidden
325
+ */
326
+ subs = new Subscription();
327
+ microphoneSvgIcon = microphoneOutlineIcon;
328
+ stopSvgIcon = stopSmIcon;
329
+ speechRecognition;
330
+ _size = DEFAULT_SIZE;
331
+ _rounded = DEFAULT_ROUNDED;
332
+ _fillMode = DEFAULT_FILL_MODE;
333
+ _themeColor = DEFAULT_THEME_COLOR;
334
+ _focused = false;
335
+ direction;
336
+ handleClasses(value, input) {
337
+ const elem = this.element;
338
+ const classes = getStylingClasses('button', input, this[input], value);
339
+ if (input === 'fillMode') {
340
+ this.handleThemeColor(this.themeColor, this[input], value);
341
+ }
342
+ if (classes.toRemove) {
343
+ this.renderer.removeClass(elem, classes.toRemove);
344
+ }
345
+ if (classes.toAdd) {
346
+ this.renderer.addClass(elem, classes.toAdd);
347
+ }
348
+ }
349
+ handleStart() {
350
+ this.ngZone.run(() => {
351
+ this.isListening = true;
352
+ this.start.emit();
353
+ });
354
+ }
355
+ handleEnd() {
356
+ this.ngZone.run(() => {
357
+ this.isListening = false;
358
+ this.end.emit();
359
+ });
360
+ }
361
+ handleResult(event) {
362
+ const results = event.results;
363
+ const lastResultIndex = results.length - 1;
364
+ const lastResult = results[lastResultIndex];
365
+ const alternatives = [];
366
+ for (let i = 0; i < lastResult.length; i++) {
367
+ alternatives.push({
368
+ transcript: lastResult[i].transcript,
369
+ confidence: lastResult[i].confidence
370
+ });
371
+ }
372
+ const args = {
373
+ isFinal: lastResult.isFinal,
374
+ alternatives: alternatives,
375
+ };
376
+ this.ngZone.run(() => {
377
+ this.result.emit(args);
378
+ });
379
+ }
380
+ handleError(ev) {
381
+ const errorMessage = ev.error || ev.message || 'Unknown error';
382
+ this.ngZone.run(() => {
383
+ this.error.emit({ errorMessage });
384
+ });
385
+ }
386
+ toObservable(input) {
387
+ return input instanceof Observable ? input : from(input);
388
+ }
389
+ handleThemeColor(value, prevFillMode, fillMode) {
390
+ const elem = this.element;
391
+ const removeFillMode = prevFillMode || this.fillMode;
392
+ const addFillMode = fillMode || this.fillMode;
393
+ const themeColorClass = getThemeColorClasses('button', removeFillMode, addFillMode, this.themeColor, value);
394
+ this.renderer.removeClass(elem, themeColorClass.toRemove);
395
+ if (addFillMode !== 'none' && fillMode !== 'none') {
396
+ if (themeColorClass.toAdd) {
397
+ this.renderer.addClass(elem, themeColorClass.toAdd);
398
+ }
399
+ }
400
+ }
401
+ destroyWebSpeech() {
402
+ if (this.speechRecognition) {
403
+ this.speechRecognition.stop();
404
+ this.speechRecognition.destroy();
405
+ this.speechRecognition = null;
406
+ this.isListening = false;
407
+ }
408
+ }
409
+ createWebSpeech() {
410
+ this.speechRecognition = new KendoSpeechRecognition({
411
+ lang: this.lang,
412
+ interimResults: this.interimResults,
413
+ maxAlternatives: this.maxAlternatives,
414
+ continuous: this.continuous,
415
+ events: {
416
+ start: this.handleStart.bind(this),
417
+ end: this.handleEnd.bind(this),
418
+ result: this.handleResult.bind(this),
419
+ error: this.handleError.bind(this)
420
+ }
421
+ });
422
+ }
423
+ 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 });
424
+ 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: [
425
+ LocalizationService,
426
+ {
427
+ provide: L10N_PREFIX,
428
+ useValue: 'kendo.speechtotextbutton'
429
+ }
430
+ ], exportAs: ["kendoSpeechToTextButton"], usesOnChanges: true, ngImport: i0, template: `
431
+ <kendo-icon-wrapper
432
+ innerCssClass="k-button-icon"
433
+ [name]="buttonIcon"
434
+ [svgIcon]="buttonSvgIcon">
435
+ </kendo-icon-wrapper>
436
+ <span class="k-button-text"><ng-content></ng-content></span>
437
+ `, isInline: true, dependencies: [{ kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }] });
438
+ }
439
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonComponent, decorators: [{
440
+ type: Component,
441
+ args: [{
442
+ exportAs: 'kendoSpeechToTextButton',
443
+ providers: [
444
+ LocalizationService,
445
+ {
446
+ provide: L10N_PREFIX,
447
+ useValue: 'kendo.speechtotextbutton'
448
+ }
449
+ ],
450
+ selector: 'button[kendoSpeechToTextButton]',
451
+ template: `
452
+ <kendo-icon-wrapper
453
+ innerCssClass="k-button-icon"
454
+ [name]="buttonIcon"
455
+ [svgIcon]="buttonSvgIcon">
456
+ </kendo-icon-wrapper>
457
+ <span class="k-button-text"><ng-content></ng-content></span>
458
+ `,
459
+ standalone: true,
460
+ imports: [IconWrapperComponent]
461
+ }]
462
+ }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i0.NgZone }]; }, propDecorators: { disabled: [{
463
+ type: Input
464
+ }], size: [{
465
+ type: Input
466
+ }], rounded: [{
467
+ type: Input
468
+ }], fillMode: [{
469
+ type: Input
470
+ }], themeColor: [{
471
+ type: Input
472
+ }], integrationMode: [{
473
+ type: Input
474
+ }], lang: [{
475
+ type: Input
476
+ }], continuous: [{
477
+ type: Input
478
+ }], interimResults: [{
479
+ type: Input
480
+ }], maxAlternatives: [{
481
+ type: Input
482
+ }], start: [{
483
+ type: Output
484
+ }], end: [{
485
+ type: Output
486
+ }], result: [{
487
+ type: Output
488
+ }], error: [{
489
+ type: Output
490
+ }], click: [{
491
+ type: Output
492
+ }], iconButtonClass: [{
493
+ type: HostBinding,
494
+ args: ['class.k-icon-button']
495
+ }], listeningClass: [{
496
+ type: HostBinding,
497
+ args: ['class.k-listening']
498
+ }], speechToTextButtonClass: [{
499
+ type: HostBinding,
500
+ args: ['class.k-speech-to-text-button']
501
+ }], classButton: [{
502
+ type: HostBinding,
503
+ args: ['class.k-button']
504
+ }], classDisabled: [{
505
+ type: HostBinding,
506
+ args: ['class.k-disabled']
507
+ }], getDirection: [{
508
+ type: HostBinding,
509
+ args: ['attr.dir']
510
+ }], ariaPressed: [{
511
+ type: HostBinding,
512
+ args: ['attr.aria-pressed']
513
+ }], onFocus: [{
514
+ type: HostListener,
515
+ args: ['focus']
516
+ }], onBlur: [{
517
+ type: HostListener,
518
+ args: ['blur']
519
+ }] } });
@@ -0,0 +1,31 @@
1
+ /**-----------------------------------------------------------------------------------------
2
+ * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
+ * Licensed under commercial license. See LICENSE.md in the project root for more information
4
+ *-------------------------------------------------------------------------------------------*/
5
+ import { NgModule } from '@angular/core';
6
+ import { KENDO_SPEECHTOTEXTBUTTON } from '../directives';
7
+ import { IconsService } from '@progress/kendo-angular-icons';
8
+ import * as i0 from "@angular/core";
9
+ import * as i1 from "./speechtotextbutton.component";
10
+ // IMPORTANT: NgModule export kept for backwards compatibility.
11
+ /**
12
+ * Represents the exported package module.
13
+ *
14
+ * Required for adding SpeechToTextButton features in NgModule-based Angular applications.
15
+ *
16
+ * The package exports:
17
+ * - `SpeechToTextButtonComponent`&mdash;The SpeechToTextButton component class.
18
+ */
19
+ export class SpeechToTextButtonModule {
20
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
21
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonModule, imports: [i1.SpeechToTextButtonComponent], exports: [i1.SpeechToTextButtonComponent] });
22
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonModule, providers: [IconsService], imports: [KENDO_SPEECHTOTEXTBUTTON] });
23
+ }
24
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpeechToTextButtonModule, decorators: [{
25
+ type: NgModule,
26
+ args: [{
27
+ exports: [...KENDO_SPEECHTOTEXTBUTTON],
28
+ imports: [...KENDO_SPEECHTOTEXTBUTTON],
29
+ providers: [IconsService]
30
+ }]
31
+ }] });
package/esm2022/util.mjs CHANGED
@@ -93,6 +93,17 @@ export const getThemeColorClasses = (componentType, prevFillMode, fillMode, prev
93
93
  toAdd: newValue !== 'none' ? `k-${componentType}-${fillMode}-${newValue}` : ''
94
94
  };
95
95
  };
96
+ /**
97
+ * @hidden
98
+ */
99
+ export const toggleClass = (className, add, renderer, element) => {
100
+ if (add) {
101
+ renderer.addClass(element, className);
102
+ }
103
+ else {
104
+ renderer.removeClass(element, className);
105
+ }
106
+ };
96
107
  /**
97
108
  * @hidden
98
109
  *