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