Skip to content

Commit c46f64e

Browse files
author
marker dao ®
committed
SmartTextArea: Research
1 parent 3179e2b commit c46f64e

File tree

4 files changed

+265
-5
lines changed

4 files changed

+265
-5
lines changed

packages/devextreme-scss/scss/widgets/base/textArea/_index.scss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,29 @@
2727
white-space: normal;
2828
}
2929
}
30+
31+
&.dx-textarea-smart {
32+
.dx-texteditor-input-container {
33+
position: relative;
34+
}
35+
36+
.dx-textarea-suggestion {
37+
position: absolute;
38+
top: 0;
39+
left: 0;
40+
right: 0;
41+
bottom: 0;
42+
pointer-events: none;
43+
z-index: 1;
44+
opacity: 0.5;
45+
resize: none;
46+
font-family: inherit;
47+
display: block;
48+
overflow: hidden;
49+
white-space: pre-wrap;
50+
margin: 0;
51+
border: none;
52+
background: transparent;
53+
}
54+
}
3055
}

packages/devextreme/js/__internal/ui/m_text_area.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import eventsEngine from '@js/common/core/events/core/events_engine';
22
import scrollEvents from '@js/common/core/events/gesture/emitter.gesture.scroll';
33
import pointerEvents from '@js/common/core/events/pointer';
4-
import { addNamespace, eventData } from '@js/common/core/events/utils/index';
4+
import { addNamespace, eventData, normalizeKeyName } from '@js/common/core/events/utils/index';
55
import registerComponent from '@js/core/component_registrator';
66
import type { dxElementWrapper } from '@js/core/renderer';
77
import $ from '@js/core/renderer';
@@ -32,12 +32,22 @@ class TextArea<
3232
> extends TextBox<TProperties> {
3333
_eventY!: number;
3434

35+
_$suggestionInput?: dxElementWrapper;
36+
37+
_currentSuggestion?: string;
38+
39+
_isSuggestionVisible = false;
40+
3541
_getDefaultOptions(): TProperties {
3642
return {
3743
...super._getDefaultOptions(),
3844
spellcheck: true,
3945
autoResizeEnabled: false,
4046
_shouldAttachKeyboardEvents: false,
47+
smartSuggestionEnabled: false,
48+
userRole: '',
49+
suggestionLength: 100,
50+
onSuggestionRequest: undefined,
4151
};
4252
}
4353

@@ -55,6 +65,10 @@ class TextArea<
5565
this.$element().addClass(TEXTAREA_CLASS);
5666
super._initMarkup();
5767
this.setAria('multiline', 'true');
68+
69+
if (this.option('smartSuggestionEnabled')) {
70+
this.$element().addClass('dx-textarea-smart');
71+
}
5872
}
5973

6074
_renderContentImpl(): void {
@@ -65,6 +79,10 @@ class TextArea<
6579
_renderInput(): void {
6680
super._renderInput();
6781
this._renderScrollHandler();
82+
83+
if (this.option('smartSuggestionEnabled')) {
84+
this._renderSuggestionTextArea();
85+
}
6886
}
6987

7088
_createInput(): dxElementWrapper {
@@ -90,6 +108,34 @@ class TextArea<
90108
eventsEngine.on($input, addNamespace(pointerEvents.move, this.NAME), this._pointerMoveHandler.bind(this));
91109
}
92110

111+
_renderAdditionalKeyDownHandler(): void {
112+
const $input = this._input();
113+
114+
if (this.option('smartSuggestionEnabled')) {
115+
// @ts-expect-error ts-error
116+
eventsEngine.on($input, addNamespace('scroll', this.NAME), this._syncScroll.bind(this));
117+
// @ts-expect-error ts-error
118+
eventsEngine.on($input, addNamespace('keydown', this.NAME), this._keyDownHandler.bind(this));
119+
}
120+
}
121+
122+
_keyDownHandler(e): void {
123+
super._keyDownHandler(e);
124+
125+
if (!this.option('smartSuggestionEnabled')) {
126+
return;
127+
}
128+
129+
const keyName = normalizeKeyName(e);
130+
131+
if (keyName === 'tab' && this._currentSuggestion && this._isSuggestionVisible) {
132+
e.preventDefault();
133+
this._acceptSuggestion();
134+
} else if (keyName === 'escape' && this._currentSuggestion && this._isSuggestionVisible) {
135+
this._rejectSuggestion();
136+
}
137+
}
138+
93139
_pointerDownHandler(e): void {
94140
this._eventY = eventData(e).y;
95141
}
@@ -138,9 +184,22 @@ class TextArea<
138184
eventsEngine.on(this._input(), addNamespace('input paste', this.NAME), this._updateInputHeight.bind(this));
139185
}
140186

187+
if (this.option('smartSuggestionEnabled')) {
188+
// @ts-expect-error ts-error
189+
eventsEngine.on(this._input(), addNamespace('input paste', this.NAME), this._inputHandler.bind(this));
190+
}
191+
141192
super._renderEvents();
142193
}
143194

195+
_inputHandler(): void {
196+
if (this.option('smartSuggestionEnabled')) {
197+
const { text = '' } = this.option();
198+
199+
this._requestSuggestion(text);
200+
}
201+
}
202+
144203
_refreshEvents(): void {
145204
// @ts-expect-error ts-error
146205
eventsEngine.off(this._input(), addNamespace('input paste', this.NAME));
@@ -213,6 +272,10 @@ class TextArea<
213272
if (autoHeightResizing) {
214273
this.$element().css('height', 'auto');
215274
}
275+
276+
if (this.option('smartSuggestionEnabled')) {
277+
this._syncDimensions();
278+
}
216279
}
217280

218281
// eslint-disable-next-line class-methods-use-this
@@ -264,6 +327,118 @@ class TextArea<
264327
}
265328
}
266329

330+
// eslint-disable-next-line class-methods-use-this
331+
_createSuggestionInput(): dxElementWrapper {
332+
const $suggestionInput = $('<textarea>');
333+
334+
// @ts-expect-error ts-error
335+
$suggestionInput.attr({
336+
readonly: 'readonly',
337+
tabindex: '-1',
338+
'aria-hidden': 'true',
339+
autocomplete: 'off',
340+
spellcheck: 'false',
341+
'aria-label': 'suggestion textarea',
342+
});
343+
344+
return $suggestionInput;
345+
}
346+
347+
_renderSuggestionTextArea(): void {
348+
this._$suggestionInput = this._createSuggestionInput();
349+
this._$suggestionInput
350+
.addClass('dx-textarea-suggestion')
351+
.addClass('dx-texteditor-input')
352+
.appendTo(this._$textEditorInputContainer);
353+
354+
this._syncDimensions();
355+
this._renderAdditionalKeyDownHandler();
356+
}
357+
358+
_disposeSuggestionTextArea(): void {
359+
if (this._$suggestionInput) {
360+
eventsEngine.off(this._$suggestionInput);
361+
this._$suggestionInput.remove();
362+
this._$suggestionInput = undefined;
363+
this._currentSuggestion = undefined;
364+
this._isSuggestionVisible = false;
365+
}
366+
}
367+
368+
_syncScroll(): void {
369+
// sync scroll on big suggestion or input resize
370+
}
371+
372+
_syncDimensions(): void {
373+
// sync dimentions on big suggestion or input resize
374+
}
375+
376+
_updateSuggestionDisplay(userText: string, suggestion: string): void {
377+
if (!this._$suggestionInput) {
378+
return;
379+
}
380+
381+
this._currentSuggestion = suggestion;
382+
383+
const fullText = userText + suggestion;
384+
this._$suggestionInput.val(fullText);
385+
386+
this._syncScroll();
387+
388+
this._isSuggestionVisible = !!suggestion;
389+
}
390+
391+
_acceptSuggestion(): void {
392+
if (!this._currentSuggestion) {
393+
return;
394+
}
395+
const { text: currentValue = '' } = this.option();
396+
// const currentValue = this.option('value') as unknown as string || '';
397+
const newValue = currentValue + this._currentSuggestion;
398+
399+
this.option('value', newValue);
400+
401+
this._rejectSuggestion();
402+
}
403+
404+
_rejectSuggestion(): void {
405+
if (!this._$suggestionInput) {
406+
return;
407+
}
408+
409+
this._currentSuggestion = undefined;
410+
this._isSuggestionVisible = false;
411+
412+
this._$suggestionInput.val('');
413+
}
414+
415+
_requestSuggestion(text: string): void {
416+
const { userRole, onSuggestionRequest } = this.option();
417+
418+
if (!onSuggestionRequest || typeof onSuggestionRequest !== 'function') {
419+
return;
420+
}
421+
422+
onSuggestionRequest(text, userRole)
423+
.then((suggestion: string) => { this._updateSuggestionDisplay(text, suggestion); })
424+
// @ts-expect-error ts-error
425+
.catch(() => { this._rejectSuggestion(); });
426+
}
427+
428+
// _valueChangeEventHandler(e): void {
429+
// super._valueChangeEventHandler(e);
430+
431+
// if (this.option('smartSuggestionEnabled')) {
432+
// const text = this.option('value') as unknown as string || '';
433+
// this._requestSuggestion(text);
434+
// }
435+
// }
436+
437+
_clean(): void {
438+
this._disposeSuggestionTextArea();
439+
super._clean();
440+
}
441+
267442
_optionChanged(args: OptionChanged<TProperties>): void {
268443
const { name, value } = args;
269444

@@ -290,6 +465,20 @@ class TextArea<
290465
this._updateInputHeight();
291466
}
292467
break;
468+
case 'smartSuggestionEnabled':
469+
if (value) {
470+
this.$element().addClass('dx-textarea-smart');
471+
this._renderSuggestionTextArea();
472+
} else {
473+
this.$element().removeClass('dx-textarea-smart');
474+
this._disposeSuggestionTextArea();
475+
}
476+
this._refreshEvents();
477+
break;
478+
case 'userRole':
479+
case 'suggestionLength':
480+
case 'onSuggestionRequest':
481+
break;
293482
default:
294483
super._optionChanged(args);
295484
}

packages/devextreme/js/ui/text_area.d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ export interface dxTextAreaOptions extends dxTextBoxOptions<dxTextArea> {
167167
* @public
168168
*/
169169
spellcheck?: boolean;
170+
/**
171+
* @docid
172+
* @default false
173+
* @public
174+
*/
175+
smartSuggestionEnabled?: boolean;
176+
/**
177+
* @docid
178+
* @default ""
179+
* @public
180+
*/
181+
userRole?: string;
182+
/**
183+
* @docid
184+
* @default 100
185+
* @public
186+
*/
187+
suggestionLength?: number;
188+
/**
189+
* @docid
190+
* @default undefined
191+
* @public
192+
*/
193+
onSuggestionRequest?: ((text: string, userRole?: string) => PromiseLike<string>);
170194
}
171195
/**
172196
* @docid
@@ -268,5 +292,10 @@ onPaste?: ((e: PasteEvent) => void);
268292
* @type_function_param1 e:{ui/text_area:ValueChangedEvent}
269293
*/
270294
onValueChanged?: ((e: ValueChangedEvent) => void);
295+
/**
296+
* @docid dxTextAreaOptions.onSuggestionRequest
297+
* @type_function_param1 e:{ui/text_area:ValueChangedEvent}
298+
*/
299+
onSuggestionRequest?: ((e: any) => void);
271300
};
272301
/// #ENDDEBUG

packages/devextreme/playground/jquery.html

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,29 @@ <h1 style="position: fixed; left: 0; top: 0; clip: rect(1px, 1px, 1px, 1px);">Te
4949
<select id="theme-selector" style="display: block;">
5050
</select>
5151
<br />
52-
<div id="button"></div>
52+
<div id="textarea"></div>
5353
<script>
5454
$(() => {
55-
$("#button").dxButton({
56-
text: 'Click me!',
57-
onClick: () => { alert("clicked"); }
55+
let debounceTimer;
56+
57+
$("#textarea").dxTextArea({
58+
height: 200,
59+
width: 600,
60+
// placeholder: 'Smart suggestions',
61+
smartSuggestionEnabled: true,
62+
userRole: 'support agent',
63+
suggestionLength: 100,
64+
onSuggestionRequest: (text, userRole) => {
65+
return new Promise((resolve) => {
66+
if (debounceTimer) {
67+
clearTimeout(debounceTimer);
68+
}
69+
70+
debounceTimer = setTimeout(() => {
71+
resolve(' SUGGESTION TEXT ');
72+
}, 700);
73+
});
74+
}
5875
});
5976
});
6077
</script>

0 commit comments

Comments
 (0)