The new predictive type feature Smart Compose of Gmail is quite interesting.
Let's say we want to implement such a functionality ourselves:
How
and in gray behind it appears are you?
.Example:
Can a textarea
with Javascript be used to achieve this?
And if not, how could this be implemented otherwise?
My previous answer got deleted, so here's a better attempt at explaining how I've somewhat replicated Smart Compose. My answer only focuses on the pertinent aspects. See https://github.com/jkhaui/predictable for the code.
We are using vanilla js and contenteditable in our solution (just like Gmail does). I bootstrap my example with create-react-app and Medium-Editor, but neither React nor Medium-Editor are necessary.
We have a database of "suggestions" which can be an array of words or phrases. For our purposes, in my example, I use a static array containing 50,000+ common English phrases. But you can easily see how this could be substituted for a dynamic data-source - such as how Gmail uses its neural network API to offer suggestions based on the current context of users' emails: https://ai.googleblog.com/2018/05/smart-compose-using-neural-networks-to.html
Smart Compose uses JavaScript to insert a <span></span>
element immediately after the word you are writing when it detects a phrase to suggest. The span element contains only the characters of the suggestion that have not been typed.
E.g. Say you've written "Hi, how a" and a suggestion appears. Let's say the entire suggestion is "how are you going today". In this case, the suggestion is rendered as "re you going today" within the span. If you continue typing the characters in the placeholder - such as "Hi, how are you goi" - then the text content of the span changes dynamically - such that "ng today" is now the text within the span.
My solution works slightly differently but achieves the same visual effect. The difference is I can't figure out how to insert an inline span adjacent to the user's current text and dynamically mutate the span's content in response to the user's input. So, Instead, I've opted for an overlay element containing the suggestion. The trick is now to position the overlay container exactly over the last word being typed (where the suggestion will be rendered). This provides the same visual effect of an inline typeahead suggestion.
We achieve correct positioning of the overlay by calculating the top + left coordinates for the last word being typed. Then, using JavaScript, we couple the top + left CSS attributes of the overlay container so that they always match the coordinates of the last word. The tricky part is getting these coordinates in the first place. The general steps are:
window.getSelection().anchorNode.data.length
which retrieves the current text node the user is writing in and returns its length, which is necessary to calculate the offset of the last word within its parent element (explained in the following steps).The parent node's text length - the current text node's (i.e the last word's) text length = the offset position of the last text node within its contenteditable parent.
Now we have the offset of the last word, we can use the various range
methods to insert a span element immediately preceding the last word: https://developer.mozilla.org/en-US/docs/Web/API/Range
shadowNode
. Mentally, you can now picture the DOM as follows: we have the user's text content, and we have a shadowNode placed at the position of the last word.getBoundingClientRect
on the shadowNode which returns specific metadata, including the top + left coordinates we're after.Visit this link for documentation https://linkkaro.com/autocomplete.html .
May be you need to make few adjustment in CSS ( padding and width ).
I hope it will help.[![
$(document).ready(function(){
//dummy random output. You can use api
var example = {
1:"dummy text 1",
2:"dummy text 2"
};
function randomobj(obj) {
var objkeys = Object.keys(obj)
return objkeys[Math.floor(Math.random() * objkeys.length)]
}
var autocomplete = document.querySelectorAll("#autocomplete");
var mainInput = document.querySelectorAll("#mainInput");
var foundName = '';
var predicted = '';
var apibusy= false;
var mlresponsebusy = false;
$('#mainInput').keyup(function(e) {
//check if null value send
if (mainInput[0].value == '') {
autocomplete[0].textContent = '';
return;
}
//check if space key press
if (e.keyCode == 32) {
CallMLDataSetAPI(e);
scrolltobototm();
return;
}
//check if Backspace key press
if (e.key == 'Backspace'){
autocomplete[0].textContent = '';
predicted = '';
apibusy = true;
return;
}
//check if ArrowRight or Tab key press
if(e.key != 'ArrowRight'){
if (autocomplete[0].textContent != '' && predicted){
var first_character = predicted.charAt(0);
if(e.key == first_character){
var s1 = predicted;
var s2 = s1.substr(1);
predicted = s2;
apibusy = true;
}else{
autocomplete[0].textContent = '';
apibusy= false;
}
}else{
autocomplete[0].textContent = '';
apibusy= false;
}
return;
}else{
if(predicted){
if (apibusy == true){
apibusy= false;
}
if (apibusy== false){
mainInput[0].value = foundName;
autocomplete[0].textContent = '';
}
}else{
return;
}
}
function CallMLDataSetAPI(event) {
//call api and get response
var response = {
"predicted": example[randomobj(example)]
};
if(response.predicted != ''){
predicted = response.predicted;
var new_text = event.target.value + response.predicted;
autocomplete[0].textContent = new_text;
foundName = new_text
}else{
predicted = '';
var new_text1 = event.target.value + predicted;
autocomplete[0].textContent = new_text1;
foundName = new_text1
}
};
});
$('#mainInput').keypress(function(e) {
var sc = 0;
$('#mainInput').each(function () {
this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
this.setAttribute('style', 'height:' + (this.scrollHeight+3) + 'px;overflow-y:hidden;');
sc = this.scrollHeight;
});
$('#autocomplete').each(function () {
if (sc <=400){
this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
this.setAttribute('style', 'height:' + (sc+2) + 'px;overflow-y:hidden;');
}
}).on('input', function () {
this.style.height = 0;
this.style.height = (sc+2) + 'px';
});
});
function scrolltobototm() {
var target = document.getElementById('autocomplete');
var target1 = document.getElementById('mainInput');
setInterval(function(){
target.scrollTop = target1.scrollHeight;
}, 1000);
};
$( "#mainInput" ).keydown(function(e) {
if (e.keyCode === 9) {
e.preventDefault();
presstabkey();
}
});
function presstabkey() {
if(predicted){
if (apibusy == true){
apibusy= false;
}
if (apibusy== false){
mainInput[0].value = foundName;
autocomplete[0].textContent = '';
}
}else{
return;
}
};
});
#autocomplete { opacity: 0.6; background: transparent; position: absolute; box-sizing: border-box; cursor: text; pointer-events: none; color: black; width: 421px;border:none;} .vc_textarea{ padding: 10px; min-height: 100px; resize: none; } #mainInput{ background: transparent; color: black; opacity: 1; width: 400px; } #autocomplete{ opacity: 0.6; background: transparent;padding: 11px 11px 11px 11px; }
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<textarea id="autocomplete" type="text" class="vc_textarea"></textarea>
<textarea id="mainInput" type="text" name="comments" placeholder="Write some text" class="vc_textarea"></textarea>
]1]1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With