/
TextControl.ts
291 lines (261 loc) · 8.99 KB
/
TextControl.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Copyright 2016 Erik Neumann. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { LabControl } from './LabControl.js';
import { Observer, Parameter, SubjectEvent, ParameterBoolean, ParameterString }
from '../util/Observe.js';
import { Util } from '../util/Util.js';
/** A user interface control for displaying and editing the text value of an object.
Synchronizes with the target object's string value by executing specified `getter` and
`setter` functions. Creates (or uses an existing) text input element to display and
edit the text.
Because this is an {@link Observer}, you can connect it to a Subject;
when the Subject broadcasts events, this will update the value it displays.
*/
export class TextControlBase implements Observer, LabControl {
/** the name shown in a label next to the textField */
private label_: string;
/** function that returns the current target value */
private getter_: ()=>string;
/** function to change the target value */
private setter_: (value: string)=>void;
/** The value of the target as last seen by this control; */
private value_: string;
/** The number of columns (characters) shown in the text field. */
private columns_: number = 40;
/** the text field showing the double value */
private textField_: HTMLInputElement;
private topElement_: HTMLElement;
/** The last value that the text field was set to, used to detect when user has
intentionally changed the value.
*/
private lastValue_: string = '';
private changeFn_: (e:Event)=>void;
private focusFn_: (e:Event)=>void;
private clickFn_: (e:Event)=>void;
/** True when first click in field after gaining focus. */
private firstClick_: boolean = false;
/**
* @param label the text shown in a label next to the text input area
* @param getter function that returns the target value
* @param setter function to change the target value
* @param textField the text field to use; if not provided, then
* a text input field is created.
*/
constructor(label: string, getter: ()=>string, setter: (value: string)=>void,
textField?: HTMLInputElement) {
this.label_ = label;
this.getter_ = getter;
this.setter_ = setter;
this.value_ = getter();
if (typeof this.value_ !== 'string') {
throw 'not a string '+this.value_;
}
let labelElement: HTMLLabelElement|null = null;
if (textField !== undefined) {
// see if the parent is a label
const parent = textField.parentElement;
if (parent != null && parent.tagName == 'LABEL') {
labelElement = parent as HTMLLabelElement;
}
} else {
// create input text field and label
textField = document.createElement('input');
textField.type = 'text';
textField.size = this.columns_;
labelElement = document.createElement('label');
labelElement.appendChild(document.createTextNode(this.label_));
labelElement.appendChild(textField);
}
this.textField_ = textField;
this.topElement_ = labelElement !== null ? labelElement : this.textField_;
this.textField_.style.textAlign = 'left';
this.changeFn_ = this.validate.bind(this);
this.textField_.addEventListener('change', this.changeFn_, /*capture=*/true);
this.focusFn_ = this.gainFocus.bind(this);
this.textField_.addEventListener('focus', this.focusFn_, /*capture=*/false);
this.clickFn_ = this.doClick.bind(this);
this.textField_.addEventListener('click', this.clickFn_, /*capture=*/false);
this.formatTextField();
};
/** @inheritDoc */
toString() {
return this.toStringShort().slice(0, -1)
+', columns_: '+this.columns_
+'}';
};
/** @inheritDoc */
toStringShort() {
return this.getClassName() + '{label_: "'+this.label_+'"}';
};
/** @inheritDoc */
disconnect() {
this.textField_.removeEventListener('change', this.changeFn_, /*capture=*/true);
this.textField_.removeEventListener('focus', this.focusFn_, /*capture=*/false);
this.textField_.removeEventListener('click', this.clickFn_, /*capture=*/false);
};
/**
* @param event the event that caused this callback to fire
*/
private doClick(_event: Event): void {
if (this.firstClick_) {
// first click after gaining focus should select entire field
this.textField_.select();
this.firstClick_ = false;
}
};
/** Sets the text field to match this.value_.
*/
private formatTextField(): void {
this.lastValue_ = this.value_;
this.textField_.value = this.value_;
this.textField_.size =this.columns_;
};
/**
* @param event the event that caused this callback to fire
*/
private gainFocus(_event: Event): void {
this.firstClick_ = true;
};
/** Returns name of class of this object.
* @return name of class of this object.
*/
getClassName(): string {
return 'TextControlBase';
};
/** Returns width of the text input field (number of characters).
@return the width of the text input field.
*/
getColumns(): number {
return this.columns_;
};
/** @inheritDoc */
getElement(): HTMLElement {
return this.topElement_;
};
/** @inheritDoc */
getParameter(): null|Parameter {
return null;
};
/** Returns the value of this control (which should match the target value if
{@link observe} is being called).
@return the value of this control
*/
getValue(): string {
return this.value_;
};
/** @inheritDoc */
observe(_event: SubjectEvent): void {
// Ensures that the value displayed by the control matches the target value.
this.setValue(this.getter_());
};
/** Sets the width of the text input field (number of characters).
@param value the width of the text input field
@return this object for chaining setters
*/
setColumns(value: number): TextControlBase {
if (this.columns_ != value) {
this.columns_ = value;
this.formatTextField();
}
return this;
};
/** @inheritDoc */
setEnabled(enabled: boolean): void {
this.textField_.disabled = !enabled;
};
/** Changes the value shown by this control, and sets the target to this value.
@throws if value is not a string
@param value the new value
*/
setValue(value: string): void {
if (value != this.value_) {
/*if (Util.DEBUG) {
console.log('TextControlBase.setValue value='+value+' vs '+this.value_);
}
*/
try {
if (typeof value !== 'string') {
throw 'not a string '+value;
}
// set this.value_ first to prevent the observe() coming here twice
this.value_ = value;
// parameter_.setValue() broadcasts which causes observe() to be called here
this.setter_(value);
} catch(ex) {
alert(ex);
this.value_ = this.getter_();
}
this.formatTextField();
}
};
/** Checks that an entered number is a valid number, updates the target value
* if valid; if an exception occurs then shows an alert and restores the old value.
* @param event the event that caused this callback to fire
*/
private validate(_event: Event): void {
// trim whitespace from start and end of string
const nowValue = this.textField_.value.replace(/^\s*|\s*$/g, '');
// Compare the current and previous text value of the field.
// Note that the double value may be different from the text value because
// of rounding.
if (nowValue != this.lastValue_) {
const value = nowValue;
if (typeof value !== 'string') {
alert('not a string: '+nowValue);
this.formatTextField();
} else {
this.setValue(value);
}
}
};
} // end class
Util.defineGlobal('lab$controls$TextControlBase', TextControlBase);
// ***************************** TextControl ****************************
/** A user interface control for displaying and editing the value of a
{@link ParameterString}.
*/
export class TextControl extends TextControlBase {
private parameter_: ParameterString;
/**
* @param parameter the ParameterString to display and edit
* @param textField the text field to use; if not provided, then
* a text field is created.
*/
constructor(parameter: ParameterString, textField?: HTMLInputElement) {
super(parameter.getName(/*localized=*/true),
() => parameter.getValue(), a => parameter.setValue(a), textField);
this.parameter_ = parameter;
this.setColumns(this.parameter_.getSuggestedLength());
this.parameter_.getSubject().addObserver(this);
};
/** @inheritDoc */
override toString() {
return super.toString().slice(0, -1)
+ ', parameter_: '+this.parameter_.toStringShort()+'}';
};
/** @inheritDoc */
override disconnect() {
super.disconnect();
this.parameter_.getSubject().removeObserver(this);
};
/** @inheritDoc */
override getClassName(): string {
return 'TextControl';
};
/** @inheritDoc */
override getParameter(): null|Parameter {
return this.parameter_;
};
} // end class
Util.defineGlobal('lab$controls$TextControl', TextControl);