Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/inputbox/inputBox.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../dom.js';
7
import * as cssJs from '../../cssValue.js';
8
import { DomEmitter } from '../../event.js';
9
import { renderFormattedText, renderText } from '../../formattedTextRenderer.js';
10
import { IHistoryNavigationWidget } from '../../history.js';
11
import { ActionBar } from '../actionbar/actionbar.js';
12
import * as aria from '../aria/aria.js';
13
import { AnchorAlignment, IContextViewProvider } from '../contextview/contextview.js';
14
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
15
import { ScrollableElement } from '../scrollbar/scrollableElement.js';
16
import { Widget } from '../widget.js';
17
import { IAction } from '../../../common/actions.js';
18
import { Emitter, Event } from '../../../common/event.js';
19
import { HistoryNavigator, IHistory } from '../../../common/history.js';
20
import { equals } from '../../../common/objects.js';
21
import { ScrollbarVisibility } from '../../../common/scrollable.js';
22
import './inputBox.css';
23
import * as nls from '../../../../nls.js';
24
import { MutableDisposable, type IDisposable } from '../../../common/lifecycle.js';
25
26
27
const $ = dom.$;
28
29
export interface IInputOptions {
30
readonly placeholder?: string;
31
readonly showPlaceholderOnFocus?: boolean;
32
readonly tooltip?: string;
33
readonly ariaLabel?: string;
34
readonly type?: string;
35
readonly validationOptions?: IInputValidationOptions;
36
readonly flexibleHeight?: boolean;
37
readonly flexibleWidth?: boolean;
38
readonly flexibleMaxHeight?: number;
39
readonly actions?: ReadonlyArray<IAction>;
40
readonly inputBoxStyles: IInputBoxStyles;
41
readonly history?: IHistory<string>;
42
}
43
44
export interface IInputBoxStyles {
45
readonly inputBackground: string | undefined;
46
readonly inputForeground: string | undefined;
47
readonly inputBorder: string | undefined;
48
readonly inputValidationInfoBorder: string | undefined;
49
readonly inputValidationInfoBackground: string | undefined;
50
readonly inputValidationInfoForeground: string | undefined;
51
readonly inputValidationWarningBorder: string | undefined;
52
readonly inputValidationWarningBackground: string | undefined;
53
readonly inputValidationWarningForeground: string | undefined;
54
readonly inputValidationErrorBorder: string | undefined;
55
readonly inputValidationErrorBackground: string | undefined;
56
readonly inputValidationErrorForeground: string | undefined;
57
}
58
59
export interface IInputValidator {
60
(value: string): IMessage | null;
61
}
62
63
export interface IMessage {
64
readonly content?: string;
65
readonly formatContent?: boolean; // defaults to false
66
readonly type?: MessageType;
67
}
68
69
export interface IInputValidationOptions {
70
validation?: IInputValidator;
71
}
72
73
export const enum MessageType {
74
INFO = 1,
75
WARNING = 2,
76
ERROR = 3
77
}
78
79
export interface IRange {
80
start: number;
81
end: number;
82
}
83
84
export const unthemedInboxStyles: IInputBoxStyles = {
85
inputBackground: '#3C3C3C',
86
inputForeground: '#CCCCCC',
87
inputValidationInfoBorder: '#55AAFF',
88
inputValidationInfoBackground: '#063B49',
89
inputValidationWarningBorder: '#B89500',
90
inputValidationWarningBackground: '#352A05',
91
inputValidationErrorBorder: '#BE1100',
92
inputValidationErrorBackground: '#5A1D1D',
93
inputBorder: undefined,
94
inputValidationErrorForeground: undefined,
95
inputValidationInfoForeground: undefined,
96
inputValidationWarningForeground: undefined
97
};
98
99
export class InputBox extends Widget {
100
private contextViewProvider?: IContextViewProvider;
101
element: HTMLElement;
102
protected input: HTMLInputElement;
103
private actionbar?: ActionBar;
104
private readonly options: IInputOptions;
105
private message: IMessage | null;
106
protected placeholder: string;
107
private tooltip: string;
108
private ariaLabel: string;
109
private validation?: IInputValidator;
110
private state: 'idle' | 'open' | 'closed' = 'idle';
111
112
private mirror: HTMLElement | undefined;
113
private cachedHeight: number | undefined;
114
private cachedContentHeight: number | undefined;
115
private maxHeight: number = Number.POSITIVE_INFINITY;
116
private scrollableElement: ScrollableElement | undefined;
117
private readonly hover: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
118
119
private _onDidChange = this._register(new Emitter<string>());
120
public get onDidChange(): Event<string> { return this._onDidChange.event; }
121
122
private _onDidHeightChange = this._register(new Emitter<number>());
123
public get onDidHeightChange(): Event<number> { return this._onDidHeightChange.event; }
124
125
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IInputOptions) {
126
super();
127
128
this.contextViewProvider = contextViewProvider;
129
this.options = options;
130
131
this.message = null;
132
this.placeholder = this.options.placeholder || '';
133
this.tooltip = this.options.tooltip ?? (this.placeholder || '');
134
this.ariaLabel = this.options.ariaLabel || '';
135
136
if (this.options.validationOptions) {
137
this.validation = this.options.validationOptions.validation;
138
}
139
140
this.element = dom.append(container, $('.monaco-inputbox.idle'));
141
142
const tagName = this.options.flexibleHeight ? 'textarea' : 'input';
143
144
const wrapper = dom.append(this.element, $('.ibwrapper'));
145
this.input = dom.append(wrapper, $(tagName + '.input.empty'));
146
this.input.setAttribute('autocorrect', 'off');
147
this.input.setAttribute('autocapitalize', 'off');
148
this.input.setAttribute('spellcheck', 'false');
149
150
this.onfocus(this.input, () => this.element.classList.add('synthetic-focus'));
151
this.onblur(this.input, () => this.element.classList.remove('synthetic-focus'));
152
153
if (this.options.flexibleHeight) {
154
this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
155
156
this.mirror = dom.append(wrapper, $('div.mirror'));
157
this.mirror.innerText = '\u00a0';
158
159
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
160
161
if (this.options.flexibleWidth) {
162
this.input.setAttribute('wrap', 'off');
163
this.mirror.style.whiteSpace = 'pre';
164
this.mirror.style.wordWrap = 'initial';
165
}
166
167
dom.append(container, this.scrollableElement.getDomNode());
168
this._register(this.scrollableElement);
169
170
// from ScrollableElement to DOM
171
this._register(this.scrollableElement.onScroll(e => this.input.scrollTop = e.scrollTop));
172
173
const onSelectionChange = this._register(new DomEmitter(container.ownerDocument, 'selectionchange'));
174
const onAnchoredSelectionChange = Event.filter(onSelectionChange.event, () => {
175
const selection = container.ownerDocument.getSelection();
176
return selection?.anchorNode === wrapper;
177
});
178
179
// from DOM to ScrollableElement
180
this._register(onAnchoredSelectionChange(this.updateScrollDimensions, this));
181
this._register(this.onDidHeightChange(this.updateScrollDimensions, this));
182
} else {
183
this.input.type = this.options.type || 'text';
184
this.input.setAttribute('wrap', 'off');
185
}
186
187
if (this.ariaLabel) {
188
this.input.setAttribute('aria-label', this.ariaLabel);
189
}
190
191
if (this.placeholder && !this.options.showPlaceholderOnFocus) {
192
this.setPlaceHolder(this.placeholder);
193
}
194
195
if (this.tooltip) {
196
this.setTooltip(this.tooltip);
197
}
198
199
this.oninput(this.input, () => this.onValueChange());
200
this.onblur(this.input, () => this.onBlur());
201
this.onfocus(this.input, () => this.onFocus());
202
203
this._register(this.ignoreGesture(this.input));
204
205
setTimeout(() => this.updateMirror(), 0);
206
207
// Support actions
208
if (this.options.actions) {
209
this.actionbar = this._register(new ActionBar(this.element));
210
this.actionbar.push(this.options.actions, { icon: true, label: false });
211
}
212
213
this.applyStyles();
214
}
215
216
protected onBlur(): void {
217
this._hideMessage();
218
if (this.options.showPlaceholderOnFocus) {
219
this.input.setAttribute('placeholder', '');
220
}
221
}
222
223
protected onFocus(): void {
224
this._showMessage();
225
if (this.options.showPlaceholderOnFocus) {
226
this.input.setAttribute('placeholder', this.placeholder || '');
227
}
228
}
229
230
public setPlaceHolder(placeHolder: string): void {
231
this.placeholder = placeHolder;
232
this.input.setAttribute('placeholder', placeHolder);
233
}
234
235
public setTooltip(tooltip: string): void {
236
this.tooltip = tooltip;
237
if (!this.hover.value) {
238
this.hover.value = this._register(getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.input, () => ({
239
content: this.tooltip,
240
appearance: {
241
compact: true,
242
}
243
})));
244
}
245
}
246
247
public setAriaLabel(label: string): void {
248
this.ariaLabel = label;
249
250
if (label) {
251
this.input.setAttribute('aria-label', this.ariaLabel);
252
} else {
253
this.input.removeAttribute('aria-label');
254
}
255
}
256
257
public getAriaLabel(): string {
258
return this.ariaLabel;
259
}
260
261
public get mirrorElement(): HTMLElement | undefined {
262
return this.mirror;
263
}
264
265
public get inputElement(): HTMLInputElement {
266
return this.input;
267
}
268
269
public get value(): string {
270
return this.input.value;
271
}
272
273
public set value(newValue: string) {
274
if (this.input.value !== newValue) {
275
this.input.value = newValue;
276
this.onValueChange();
277
}
278
}
279
280
public get step(): string {
281
return this.input.step;
282
}
283
284
public set step(newValue: string) {
285
this.input.step = newValue;
286
}
287
288
public get height(): number {
289
return typeof this.cachedHeight === 'number' ? this.cachedHeight : dom.getTotalHeight(this.element);
290
}
291
292
public focus(): void {
293
this.input.focus();
294
}
295
296
public blur(): void {
297
this.input.blur();
298
}
299
300
public hasFocus(): boolean {
301
return dom.isActiveElement(this.input);
302
}
303
304
public select(range: IRange | null = null): void {
305
this.input.select();
306
307
if (range) {
308
this.input.setSelectionRange(range.start, range.end);
309
if (range.end === this.input.value.length) {
310
this.input.scrollLeft = this.input.scrollWidth;
311
}
312
}
313
}
314
315
public isSelectionAtEnd(): boolean {
316
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
317
}
318
319
public getSelection(): IRange | null {
320
const selectionStart = this.input.selectionStart;
321
if (selectionStart === null) {
322
return null;
323
}
324
const selectionEnd = this.input.selectionEnd ?? selectionStart;
325
return {
326
start: selectionStart,
327
end: selectionEnd,
328
};
329
}
330
331
public enable(): void {
332
this.input.removeAttribute('disabled');
333
}
334
335
public disable(): void {
336
this.blur();
337
this.input.disabled = true;
338
this._hideMessage();
339
}
340
341
public setEnabled(enabled: boolean): void {
342
if (enabled) {
343
this.enable();
344
} else {
345
this.disable();
346
}
347
}
348
349
public get width(): number {
350
return dom.getTotalWidth(this.input);
351
}
352
353
public set width(width: number) {
354
if (this.options.flexibleHeight && this.options.flexibleWidth) {
355
// textarea with horizontal scrolling
356
let horizontalPadding = 0;
357
if (this.mirror) {
358
const paddingLeft = parseFloat(this.mirror.style.paddingLeft || '') || 0;
359
const paddingRight = parseFloat(this.mirror.style.paddingRight || '') || 0;
360
horizontalPadding = paddingLeft + paddingRight;
361
}
362
this.input.style.width = (width - horizontalPadding) + 'px';
363
} else {
364
this.input.style.width = width + 'px';
365
}
366
367
if (this.mirror) {
368
this.mirror.style.width = width + 'px';
369
}
370
}
371
372
public set paddingRight(paddingRight: number) {
373
// Set width to avoid hint text overlapping buttons
374
this.input.style.width = `calc(100% - ${paddingRight}px)`;
375
376
if (this.mirror) {
377
this.mirror.style.paddingRight = paddingRight + 'px';
378
}
379
}
380
381
private updateScrollDimensions(): void {
382
if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number' || !this.scrollableElement) {
383
return;
384
}
385
386
const scrollHeight = this.cachedContentHeight;
387
const height = this.cachedHeight;
388
const scrollTop = this.input.scrollTop;
389
390
this.scrollableElement.setScrollDimensions({ scrollHeight, height });
391
this.scrollableElement.setScrollPosition({ scrollTop });
392
}
393
394
public showMessage(message: IMessage, force?: boolean): void {
395
if (this.state === 'open' && equals(this.message, message)) {
396
// Already showing
397
return;
398
}
399
400
this.message = message;
401
402
this.element.classList.remove('idle');
403
this.element.classList.remove('info');
404
this.element.classList.remove('warning');
405
this.element.classList.remove('error');
406
this.element.classList.add(this.classForType(message.type));
407
408
const styles = this.stylesForType(this.message.type);
409
this.element.style.border = `1px solid ${cssJs.asCssValueWithDefault(styles.border, 'transparent')}`;
410
411
if (this.message.content && (this.hasFocus() || force)) {
412
this._showMessage();
413
}
414
}
415
416
public hideMessage(): void {
417
this.message = null;
418
419
this.element.classList.remove('info');
420
this.element.classList.remove('warning');
421
this.element.classList.remove('error');
422
this.element.classList.add('idle');
423
424
this._hideMessage();
425
this.applyStyles();
426
}
427
428
public isInputValid(): boolean {
429
return !!this.validation && !this.validation(this.value);
430
}
431
432
public validate(): MessageType | undefined {
433
let errorMsg: IMessage | null = null;
434
435
if (this.validation) {
436
errorMsg = this.validation(this.value);
437
438
if (errorMsg) {
439
this.inputElement.setAttribute('aria-invalid', 'true');
440
this.showMessage(errorMsg);
441
}
442
else if (this.inputElement.hasAttribute('aria-invalid')) {
443
this.inputElement.removeAttribute('aria-invalid');
444
this.hideMessage();
445
}
446
}
447
448
return errorMsg?.type;
449
}
450
451
public stylesForType(type: MessageType | undefined): { border: string | undefined; background: string | undefined; foreground: string | undefined } {
452
const styles = this.options.inputBoxStyles;
453
switch (type) {
454
case MessageType.INFO: return { border: styles.inputValidationInfoBorder, background: styles.inputValidationInfoBackground, foreground: styles.inputValidationInfoForeground };
455
case MessageType.WARNING: return { border: styles.inputValidationWarningBorder, background: styles.inputValidationWarningBackground, foreground: styles.inputValidationWarningForeground };
456
default: return { border: styles.inputValidationErrorBorder, background: styles.inputValidationErrorBackground, foreground: styles.inputValidationErrorForeground };
457
}
458
}
459
460
private classForType(type: MessageType | undefined): string {
461
switch (type) {
462
case MessageType.INFO: return 'info';
463
case MessageType.WARNING: return 'warning';
464
default: return 'error';
465
}
466
}
467
468
private _showMessage(): void {
469
if (!this.contextViewProvider || !this.message) {
470
return;
471
}
472
473
let div: HTMLElement;
474
const layout = () => div.style.width = dom.getTotalWidth(this.element) + 'px';
475
476
this.contextViewProvider.showContextView({
477
getAnchor: () => this.element,
478
anchorAlignment: AnchorAlignment.RIGHT,
479
render: (container: HTMLElement) => {
480
if (!this.message) {
481
return null;
482
}
483
484
div = dom.append(container, $('.monaco-inputbox-container'));
485
layout();
486
487
488
const spanElement = $('span.monaco-inputbox-message');
489
if (this.message.formatContent) {
490
renderFormattedText(this.message.content!, undefined, spanElement);
491
} else {
492
renderText(this.message.content!, undefined, spanElement);
493
}
494
495
spanElement.classList.add(this.classForType(this.message.type));
496
497
const styles = this.stylesForType(this.message.type);
498
spanElement.style.backgroundColor = styles.background ?? '';
499
spanElement.style.color = styles.foreground ?? '';
500
spanElement.style.border = styles.border ? `1px solid ${styles.border}` : '';
501
502
dom.append(div, spanElement);
503
504
return null;
505
},
506
onHide: () => {
507
this.state = 'closed';
508
},
509
layout: layout
510
});
511
512
// ARIA Support
513
let alertText: string;
514
if (this.message.type === MessageType.ERROR) {
515
alertText = nls.localize('alertErrorMessage', "Error: {0}", this.message.content);
516
} else if (this.message.type === MessageType.WARNING) {
517
alertText = nls.localize('alertWarningMessage', "Warning: {0}", this.message.content);
518
} else {
519
alertText = nls.localize('alertInfoMessage', "Info: {0}", this.message.content);
520
}
521
522
aria.alert(alertText);
523
524
this.state = 'open';
525
}
526
527
private _hideMessage(): void {
528
if (!this.contextViewProvider) {
529
return;
530
}
531
532
if (this.state === 'open') {
533
this.contextViewProvider.hideContextView();
534
}
535
536
this.state = 'idle';
537
}
538
539
private onValueChange(): void {
540
this._onDidChange.fire(this.value);
541
542
this.validate();
543
this.updateMirror();
544
this.input.classList.toggle('empty', !this.value);
545
546
if (this.state === 'open' && this.contextViewProvider) {
547
this.contextViewProvider.layout();
548
}
549
}
550
551
private updateMirror(): void {
552
if (!this.mirror) {
553
return;
554
}
555
556
const value = this.value;
557
const lastCharCode = value.charCodeAt(value.length - 1);
558
const suffix = lastCharCode === 10 ? ' ' : '';
559
const mirrorTextContent = (value + suffix)
560
.replace(/\u000c/g, ''); // Don't measure with the form feed character, which messes up sizing
561
562
if (mirrorTextContent) {
563
this.mirror.textContent = value + suffix;
564
} else {
565
this.mirror.innerText = '\u00a0';
566
}
567
568
this.layout();
569
}
570
571
protected applyStyles(): void {
572
const styles = this.options.inputBoxStyles;
573
574
const background = styles.inputBackground ?? '';
575
const foreground = styles.inputForeground ?? '';
576
const border = styles.inputBorder ?? '';
577
578
this.element.style.backgroundColor = background;
579
this.element.style.color = foreground;
580
this.input.style.backgroundColor = 'inherit';
581
this.input.style.color = foreground;
582
583
// there's always a border, even if the color is not set.
584
this.element.style.border = `1px solid ${cssJs.asCssValueWithDefault(border, 'transparent')}`;
585
}
586
587
public layout(): void {
588
if (!this.mirror) {
589
return;
590
}
591
592
const previousHeight = this.cachedContentHeight;
593
this.cachedContentHeight = dom.getTotalHeight(this.mirror);
594
595
if (previousHeight !== this.cachedContentHeight) {
596
this.cachedHeight = Math.min(this.cachedContentHeight, this.maxHeight);
597
this.input.style.height = this.cachedHeight + 'px';
598
this._onDidHeightChange.fire(this.cachedContentHeight);
599
}
600
}
601
602
public insertAtCursor(text: string): void {
603
const inputElement = this.inputElement;
604
const start = inputElement.selectionStart;
605
const end = inputElement.selectionEnd;
606
const content = inputElement.value;
607
608
if (start !== null && end !== null) {
609
this.value = content.substr(0, start) + text + content.substr(end);
610
inputElement.setSelectionRange(start + 1, start + 1);
611
this.layout();
612
}
613
}
614
615
public override dispose(): void {
616
this._hideMessage();
617
618
this.message = null;
619
620
this.actionbar?.dispose();
621
622
super.dispose();
623
}
624
}
625
626
export interface IHistoryInputOptions extends IInputOptions {
627
readonly showHistoryHint?: () => boolean;
628
}
629
630
export class HistoryInputBox extends InputBox implements IHistoryNavigationWidget {
631
632
private readonly history: HistoryNavigator<string>;
633
private observer: MutationObserver | undefined;
634
635
private readonly _onDidFocus = this._register(new Emitter<void>());
636
readonly onDidFocus = this._onDidFocus.event;
637
638
private readonly _onDidBlur = this._register(new Emitter<void>());
639
readonly onDidBlur = this._onDidBlur.event;
640
641
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IHistoryInputOptions) {
642
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS = nls.localize({
643
key: 'history.inputbox.hint.suffix.noparens',
644
comment: ['Text is the suffix of an input field placeholder coming after the action the input field performs, this will be used when the input field ends in a closing parenthesis ")", for example "Filter (e.g. text, !exclude)". The character inserted into the final string is \u21C5 to represent the up and down arrow keys.']
645
}, ' or {0} for history', `\u21C5`);
646
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS = nls.localize({
647
key: 'history.inputbox.hint.suffix.inparens',
648
comment: ['Text is the suffix of an input field placeholder coming after the action the input field performs, this will be used when the input field does NOT end in a closing parenthesis (eg. "Find"). The character inserted into the final string is \u21C5 to represent the up and down arrow keys.']
649
}, ' ({0} for history)', `\u21C5`);
650
651
super(container, contextViewProvider, options);
652
this.history = this._register(new HistoryNavigator<string>(options.history, 100));
653
654
// Function to append the history suffix to the placeholder if necessary
655
const addSuffix = () => {
656
if (options.showHistoryHint && options.showHistoryHint() && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS) && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS) && this.history.getHistory().length) {
657
const suffix = this.placeholder.endsWith(')') ? NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS : NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS;
658
const suffixedPlaceholder = this.placeholder + suffix;
659
if (options.showPlaceholderOnFocus && !dom.isActiveElement(this.input)) {
660
this.placeholder = suffixedPlaceholder;
661
}
662
else {
663
this.setPlaceHolder(suffixedPlaceholder);
664
}
665
}
666
};
667
668
// Spot the change to the textarea class attribute which occurs when it changes between non-empty and empty,
669
// and add the history suffix to the placeholder if not yet present
670
this.observer = new MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
671
mutationList.forEach((mutation: MutationRecord) => {
672
if (!mutation.target.textContent) {
673
addSuffix();
674
}
675
});
676
});
677
this.observer.observe(this.input, { attributeFilter: ['class'] });
678
679
this.onfocus(this.input, () => addSuffix());
680
this.onblur(this.input, () => {
681
const resetPlaceholder = (historyHint: string) => {
682
if (!this.placeholder.endsWith(historyHint)) {
683
return false;
684
}
685
else {
686
const revertedPlaceholder = this.placeholder.slice(0, this.placeholder.length - historyHint.length);
687
if (options.showPlaceholderOnFocus) {
688
this.placeholder = revertedPlaceholder;
689
}
690
else {
691
this.setPlaceHolder(revertedPlaceholder);
692
}
693
return true;
694
}
695
};
696
if (!resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS)) {
697
resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS);
698
}
699
});
700
}
701
702
override dispose() {
703
super.dispose();
704
if (this.observer) {
705
this.observer.disconnect();
706
this.observer = undefined;
707
}
708
}
709
710
public addToHistory(always?: boolean): void {
711
if (this.value && (always || this.value !== this.getCurrentValue())) {
712
this.history.add(this.value);
713
}
714
}
715
716
public prependHistory(restoredHistory: string[]): void {
717
const newHistory = this.getHistory();
718
this.clearHistory();
719
720
restoredHistory.forEach((item) => {
721
this.history.add(item);
722
});
723
724
newHistory.forEach(item => {
725
this.history.add(item);
726
});
727
}
728
729
public getHistory(): string[] {
730
return this.history.getHistory();
731
}
732
733
public isAtFirstInHistory(): boolean {
734
return this.history.isFirst();
735
}
736
737
public isAtLastInHistory(): boolean {
738
return this.history.isLast();
739
}
740
741
public isNowhereInHistory(): boolean {
742
return this.history.isNowhere();
743
}
744
745
public showNextValue(): void {
746
if (!this.history.has(this.value)) {
747
this.addToHistory();
748
}
749
750
let next = this.getNextValue();
751
if (next) {
752
next = next === this.value ? this.getNextValue() : next;
753
}
754
755
this.value = next ?? '';
756
aria.status(this.value ? this.value : nls.localize('clearedInput', "Cleared Input"));
757
}
758
759
public showPreviousValue(): void {
760
if (!this.history.has(this.value)) {
761
this.addToHistory();
762
}
763
764
let previous = this.getPreviousValue();
765
if (previous) {
766
previous = previous === this.value ? this.getPreviousValue() : previous;
767
}
768
769
if (previous) {
770
this.value = previous;
771
aria.status(this.value);
772
}
773
}
774
775
public clearHistory(): void {
776
this.history.clear();
777
}
778
779
public override setPlaceHolder(placeHolder: string): void {
780
super.setPlaceHolder(placeHolder);
781
this.setTooltip(placeHolder);
782
}
783
784
protected override onBlur(): void {
785
super.onBlur();
786
this._onDidBlur.fire();
787
}
788
789
protected override onFocus(): void {
790
super.onFocus();
791
this._onDidFocus.fire();
792
}
793
794
private getCurrentValue(): string | null {
795
let currentValue = this.history.current();
796
if (!currentValue) {
797
currentValue = this.history.last();
798
this.history.next();
799
}
800
return currentValue;
801
}
802
803
private getPreviousValue(): string | null {
804
return this.history.previous() || this.history.first();
805
}
806
807
private getNextValue(): string | null {
808
return this.history.next();
809
}
810
}
811
812