How would you move the cursor to the next or previous word in a textarea using Javascript? I'm trying to replicate the Emacs commands "forward one word" and "back one word" in an HTML textarea.
I can get the current caret/cursor position using rangyinputs, but I'm not yet sure how to efficiently move to the next word without using various splits that could be slow on very long pieces of text.
I used setCaretToTextEnd() from here and .selectRange() from here. The following functions use Emacs style caret positions, and is more efficient than looping through words.
function nextWord(input) {
let currentCaretPosition = input.selectionStart;
// -1 Because Emacs goes to end of next word.
let nextWordPosition = input.value.indexOf(' ', currentCaretPosition) - 1;
if (nextWordPosition < 0) {
input.setCaretToTextEnd();
} else {
input.selectRange(nextWordPosition);
}
}
function previousWord(input) {
let currentCaretPosition = input.selectionStart;
// +1 Because Emacs goes to start of previous word.
let previousWordPosition = input.value.lastIndexOf(' ', currentCaretPosition) + 1;
if (previousWordPosition < 0) {
input.selectRange(0);
} else {
input.selectRange(previousWordPosition);
}
}
See this fiddle. I used functions from jQuery Set Cursor Position in Text Area to change position of cursor.
function nextWord(input) {
var words = input.value.split(" "),
index = 0;
for (var i in words) {
var word = words[i];
if (index+word.length >= input.selectionStart) {
setCaretToPos(input, index+word.length+1);
break;
}
index += word.length+1;
}
}
function previousWord(input) {
var words = input.value.split(" ").reverse(),
index = input.value.length;
for (var i in words) {
var word = words[i];
if (index+1 <= input.selectionStart) {
setCaretToPos(input, index-word.length);
break;
}
index -= word.length+1;
}
}
Related
How do I change the color of the letter that is currently "selected" when looping through text like M00110100N00000000P?
For example, in a for loop, when i = 1 I need to change first letter in a word to yellow color. Then when i becomes 2, the first letter becomes black again and second letter becomes yellow, etc. I want to use setTimeout to make a delay so the change is visible.
I tried to do it using this method but as my "word / line" is made out of numbers like 0 and 1 it doesn't work.
function myFunction() {
var letters = document.getElementById('text');
for (var i = 0; i < letters.innerHTML.length; i++) {
//only change the one you want to
letters.innerHTML = letters.innerHTML.replace(letters[i], '<span style="color: yellow;">'+letters[i]+'</span>');
}
}
There are a few problems with what you are doing. You can get it working like this - see how it works in the comments, and an explanation of what was going wrong below:
var textElement = document.getElementById('text');
colourCycle(textElement);
/* function: changeLetter
takes the whole word and the position of the current letter
and wraps that letter with a span to change the colour */
function changeLetter(textToChange, pos){
// if the pos is the same as the length, we're at the end so return the plain word
if (textToChange.length == pos) return textToChange;
// split the text at the letter and insert the span around the letter
return textToChange.substring(0, pos)
+ '<span style="color:yellow;">' + textToChange[pos] + '</span>'
+ textToChange.substring(pos+1);
}
function colourCycle(textElement) {
// make a copy of the text to work from
var letters = textElement.innerHTML;
// for each letter, call our function to change the colour of the letter after 1 sec delay
// NOTE: we loop 1 extra time so we can remove the colour on the last loop
for (var i = 0; i <= textElement.innerHTML.length; i++) {
(function (i) {
setTimeout(function () {
textElement.innerHTML = changeLetter(letters, i);
}, 1000 * i);
})(i);
}
}
<div id="text">HELLO</div>
How This Works
1. We create a function that will wrap the letter at the specified position with a span with the CSS to change the colour.
Note that instead of using replace (where we can't choose the position of the letter to replace), we use substring to split the string at the position of the letter, then rebuild the string with the span tags around the letter:
function changeLetter(textToChange, pos){
return textToChange.substring(0, pos)
+ '<span style="color:yellow;">' + textToChange[pos] + '</span>'
+ textToChange.substring(pos+1);
}
2. In our main function, create You need to use a copy of the innerHTML Otherwise you are changing the position of the letters - when you add the <span... html, the letter position has changed.
var letters = textElement.innerHTML;
3. Set the timeout to change each letter for a second - when loop through each letter, set a timeout delay of 1 second before we call the changeLetter function on the next letter:
for (var i = 0; i <= textElement.innerHTML.length; i++) {
(function (i) {
setTimeout(function () {
textElement.innerHTML = changeLetter(letters, i);
}, 1000 * i);
})(i);
}
4. Reset the color to the default at the end To do this we need to loop one extra time to replace the text with the last yellow letter.
In the loop, we loop up to i <= length:
for (var i = 0; i <= textElement.innerHTML.length; i++)
And add this line to the start of in the changeLetter function:
if (textToChange.length == pos) return textToChange;
you want to add if and else loop show below.
function myFunction() {
var letters = document.getElementById('text');
for (var i = 0; i < letters.innerHTML.length; i++) {
//only change the one you want to
if (i==1){letters.innerHTML = letters.innerHTML.replace(letters[i], '<span style="color: yellow;">'+letters[i]+'</span>');}
else { letters.innerHTML = letters.innerHTML.replace(letters[i],+letters[i]+'</span>');
}
}
This question already has an answer here:
Correct substring position after replacement
(1 answer)
Closed 5 years ago.
TL;DR
I have function that replace text, a string and cursor position (a number) and I need to get corrected position (a number) for new string that is created with replace function if the length of the string changes:
input and cursor position: foo ba|r text
replacement: foo -> baz_text, bar -> quux_text
result: baz_text qu|ux_text text
input and cursor position: foo bar| text
replacement: foo -> baz_text, bar -> quux_text
result: baz_text quux_text| text
input and cursor position: foo bar| text
replacement: foo -> f, bar -> b
result: f b| text
input and cursor position: foo b|ar text
replacement: foo -> f, bar -> b
result: f b| text
the problem is that I can use substring on original text but then the replacement will not match whole word so it need to be done for whole text but then substring will not match the replacement.
I'm also fine with solution that cursor is always at the end of the word when original cursor is in the middle of the replaced word.
and now my implementation, in jQuery Terminal I have a array of formatters functions in:
$.terminal.defaults.formatters
they accept a string and it should return new string it work fine except this case:
when I have formatter that change length if break the command line, for instance this formatter:
$.terminal.defaults.formatters.push(function(string) {
return string.replace(/:smile:/g, 'a')
.replace(/(foo|bar|baz)/g, 'text_$1');
});
then the cursor position was wrong when command line get new string.
I've try to fix this but it don't work as expected, the internal of the terminal look like this,
when I change position I'm crating another variable formatted_position that's use in command line to display the cursor. to get that value I use this:
formatted_position = position;
var string = formatting(command);
var len = $.terminal.length(string);
var command_len = $.terminal.length(command);
if (len !== command_len) {
var orig_sub = $.terminal.substring(command, 0, position);
var orig_len = $.terminal.length(orig_sub);
var formatted = formatting(orig_sub);
var formatted_len = $.terminal.length(formatted);
if (orig_len > formatted_len) {
// if formatting make substring - (text before cursor)
// shorter then subtract the difference
formatted_position -= orig_len - formatted_len;
} else if (orig_len < formatted_len) {
// if the formatted string is longer add difference
formatted_position += formatted_len - orig_len;
}
}
if (formatted_position > len) {
formatted_position = len;
} else if (formatted_position < 0) {
formatted_position = 0;
}
$.terminal.substring and $.terminal.length are helper functions that are terminal formatting aware (text that look like this [[b;#fff;]hello]) if you will write solution you can use normal text and use string methods.
the problem is that when I move the cursor in the middle of the word that is changed
it kind of work when text is longer, but for shorter string the cursor jump to the right when text is in the middle of the word that got replaced.
I've try to fix this as well using this code:
function find_diff(callback) {
var start = position === 0 ? 0 : position - 1;
for (var i = start; i < command_len; ++i) {
var substr = $.terminal.substring(command, 0, i);
var next_substr = $.terminal.substring(command, 0, i + 1);
var formatted = formatting(next_substr);
var substr_len = $.terminal.length(substr);
var formatted_len = $.terminal.length(formatted);
var diff = Math.abs(substr_len - formatted_len);
if (diff > 1) {
return diff;
}
}
return 0;
}
...
} else if (len < command_len) {
formatted_position -= find_diff();
} else if (len > command_len) {
formatted_position += find_diff();
}
but this I think make it even worse becuase it find diff when cursor is before or in the middle of replaced word and it should find diff only when cursor is in the middle of replaced word.
You can see the result of my attempts in this codepen https://codepen.io/jcubic/pen/qPVMPg?editors=0110 (that allow to type emoji and foo bar baz get replaced by text_$1)
UPDATE:
I've make it kind of work with this code:
// ---------------------------------------------------------------------
// :: functions used to calculate position of cursor when formatting
// :: change length of output text like with emoji demo
// ---------------------------------------------------------------------
function split(formatted, normal) {
function longer(str) {
return found && length(str) > length(found) || !found;
}
var formatted_len = $.terminal.length(formatted);
var normal_len = $.terminal.length(normal);
var found;
for (var i = normal_len; i > 1; i--) {
var test_normal = $.terminal.substring(normal, 0, i);
var formatted_normal = formatting(test_normal);
for (var j = formatted_len; j > 1; j--) {
var test_formatted = $.terminal.substring(formatted, 0, j);
if (test_formatted === formatted_normal &&
longer(test_normal)) {
found = test_normal;
}
}
}
return found || '';
}
// ---------------------------------------------------------------------
// :: return index after next word that got replaced by formatting
// :: and change length of text
// ---------------------------------------------------------------------
function index_after_formatting(position) {
var start = position === 0 ? 0 : position - 1;
var command_len = $.terminal.length(command);
for (var i = start; i < command_len; ++i) {
var substr = $.terminal.substring(command, 0, i);
var next_substr = $.terminal.substring(command, 0, i + 1);
var formatted_substr = formatting(substr);
var formatted_next = formatting(next_substr);
var substr_len = length(formatted_substr);
var next_len = length(formatted_next);
var test_diff = Math.abs(next_len - substr_len);
if (test_diff > 1) {
return i;
}
}
}
// ---------------------------------------------------------------------
// :: main function that return corrected cursor position on display
// :: if cursor is in the middle of the word that is shorter the before
// :: applying formatting then the corrected position is after the word
// :: so it stay in place when you move real cursor in the middle
// :: of the word
// ---------------------------------------------------------------------
function get_formatted_position(position) {
var formatted_position = position;
var string = formatting(command);
var len = $.terminal.length(string);
var command_len = $.terminal.length(command);
if (len !== command_len) {
var orig_sub = $.terminal.substring(command, 0, position);
var orig_len = $.terminal.length(orig_sub);
var sub = formatting(orig_sub);
var sub_len = $.terminal.length(sub);
var diff = Math.abs(orig_len - sub_len);
if (false && orig_len > sub_len) {
formatted_position -= diff;
} else if (false && orig_len < sub_len) {
formatted_position += diff;
} else {
var index = index_after_formatting(position);
var to_end = $.terminal.substring(command, 0, index + 1);
//formatted_position -= length(to_end) - orig_len;
formatted_position -= orig_len - sub_len;
if (orig_sub && orig_sub !== to_end) {
var formatted_to_end = formatting(to_end);
var common = split(formatted_to_end, orig_sub);
var re = new RegExp('^' + $.terminal.escape_regex(common));
var to_end_rest = to_end.replace(re, '');
var to_end_rest_len = length(formatting(to_end_rest));
if (common orig_sub !== common) {
var commnon_len = length(formatting(common));
formatted_position = commnon_len + to_end_rest_len;
}
}
}
if (formatted_position > len) {
formatted_position = len;
} else if (formatted_position < 0) {
formatted_position = 0;
}
}
return formatted_position;
}
it don't work for one case when you type emoji as first character and the cursor is in the middle of :smile: word. How to fix get_formatted_position function to have correct fixed position after replace?
UPDATE: I've ask different and simple question and got the solution using trackingReplace function that accept regex and string, so I've change the API for formatters to accept array with regex and string along the function Correct substring position after replacement
So I was able to accomplish the given task, however I wasn't able to implement it into the library as I am not sure how to implements many things there.
I made it in vanilla javascript so there shouldn't be any hiccups while implementing into the library. The script is mostly dependant on the selectionStart and selectionEnd properties available on textarea, input or similar elements. After all replacement is done, the new selection is set to the textarea using setSelectionRange method.
// sel = [selectionStart, selectionEnd]
function updateSelection(sel, replaceStart, oldLength, newLength){
var orig = sel.map(a => a)
var diff = newLength - oldLength
var replaceEnd = replaceStart + oldLength
if(replaceEnd <= sel[0]){
// Replacement occurs before selection
sel[0] += diff
sel[1] += diff
console.log('Replacement occurs before selection', orig, sel)
}else if(replaceStart <= sel[0]){
// Replacement starts before selection
if(replaceEnd >= sel[1]){
// and ends after selection
sel[1] += diff
}else{
// and ends in selection
}
console.log('Replacement starts before selection', orig, sel)
}else if(replaceStart <= sel[1]){
// Replacement starts in selection
if(replaceEnd < sel[1]){
// and ends in seledtion
}else{
// and ends after selection
sel[1] += diff
}
console.log('Replacement starts in selection', orig, sel)
}
}
Here is whole demo: codepen.
PS: From my observations the format script runs way to often.
I have a game of hangman where if someone guesses an incorrect letter it adds the image changes. to accomplish this I created a variable that when an incorrect letter is guessed is incremented and I use Jquery .html to change the picture. The problem I have is that the first wrong guess changes the picture but subsequent guesses do not. Is this because Jquery's .html just adds another div? what is a good solution to this? Below is the code
var word;
var wrongGuess = 0;
var usedLetters = [];
$(document).ready(function () {
SetGameBoard();
});
//When guess button is clicked
$('#BtnGuess').click(function () {
CheckGuess();
});
function GetPhrase() {
word = ReturnWord();
alert(word);
}
function SetGameBoard() {
$('#WinLose').hide();
$('#controls').show();
GetPhrase();
wordToGuess = new Array(word.length);
// Place underscore for each letter in the answer word in the DivWord div
for (var i = 0; i < word.length; i++){
wordToGuess[i] = "_ ";
}
$('#DivWord').html(wordToGuess);
}
function CheckGuess() {
var pos = 0;
var posArray = [];
var guessLetter = $('#tbGuessLetter').val();
var picNum = 0;
//check to see if letter has been used
if(usedLetters.indexOf(guessLetter) != -1){
alert("You've already used this Letter!");
}
//populate array with indices of occurrences of guessed letter
while ((pos = word.indexOf(guessLetter, pos)) > -1) {
posArray.push(++pos);
}
//if the guessed letter is not in the word...
if (posArray.length == 0) {
picNum++;
alert(guessLetter + " is not in the word.");
$('#DivPic').html('<img src="images/h'+picNum+'.png" />');
}else{
//iterate over array of underscores (wordToGuess[]) and splice in guessed letter
for(i=0; i < posArray.length; i++){
wordToGuess.splice(posArray[i] - 1, 1, guessLetter);
}
//update DivWord
$('#DivWord').html(wordToGuess);
usedLetters.push(guessLetter);
}
$('#tbGuessLetter').val("");
}
You should move picNum to the top, to declare it as global:
var picNum = 0;
var word;
var wrongGuess = 0;
var usedLetters = [];
Because each time it goes to the CheckGuess() function it resets to zero, then if the user fails it will only increment to 1. That's why you are seeing the same image (first time)
I have a script which is almost complete but I can't figure out the last bit here. The script is meant to limit the amount of words that can be entered into a text area and if they go over the word limit these extra words are removed. I have the amount of words beyond the max labeled as overage. For instance, if you were to enter in 102 words, then the overage would be 2. How would I remove those two words from the text area?
jQuery(document).ready(function($) {
var max = 100;
$('#text').keyup(function(e) {
if (e.which < 0x20) {
return;
}
var value = $('#text').val();
var regex = /\s+/gi;
var wordCount = value.trim().replace(regex, ' ').split(' ').length;
if (wordCount == max) {
// Reached max, prevent additional.
e.preventDefault();
} else if (wordCount > max) {
<!--Edited to show code from user3003216-->
<!--Isn't working like this, textarea doesn't update.-->
var overage = wordCount - max;
var words = value.split(' ');
for(var i = 0; i<overage; i++){
words.pop();
}
}
});
});
The easiest way to approach this is just to count the number of words on keypress and go from there. Check whether there are more words than the amount allowed. If so, remove all the excess words: while (text.length > maxWords). Then just replace the value of the text box with the updated text.
fiddle
JavaScript
var maxWords = 10;
$("#myText").keypress(function (event) {
var text = $(this).val().split(" "); // grabs the text and splits it
while (text.length > maxWords) { // while more words than maxWords
event.preventDefault();
text.pop(); // remove the last word
// event.preventDefault() isn't absolutely necessary,
// it just slightly alters the typing;
// remove it to see the difference
}
$(this).val(text.join(" ")); // replace the text with the updated text
})
HTML
<p>Enter no more than 10 words:</p>
<textarea id="myText"></textarea>
CSS
textarea {
width: 300px;
height: 100px;
}
You can easily test whether it works by pasting more than maxWords—in this case, 10—words into the textarea and pressing space. All the extra words will be removed.
You can put below code into your else if statement..
else if (wordCount > max) {
var overage = wordCount - max;
var words = value.split(' ');
for(var i = 0; i<overage; i++){
words.pop();
}
}
And if you want to get your string back from that words, you can use join like below:
str = words.join(' ');
well it would be better to use java script so here you go:
var maxWords = 20;
event.rc = true;
var words = event.value.split(" ");
if (words.length>maxWords) {
app.alert("You may not enter more than " + maxWords + " words in this field.");
event.rc = false;
}
jsFiddle Demo
You can use val to re-value the text-box. The array slice method will allow you to pull the first 100 words out of the array. Then just join them with a space and stick them back in the text-box.
$(document).ready(function($) {
var max = 100;
$('#text').keyup(function(e) {
if (e.which < 0x20) {
return;
}
var value = $('#text').val();
var words = value.trim().split(/\s+/gi);
var wordCount = words.length;
if (wordCount == max) {
// Reached max, prevent additional.
e.preventDefault();
} else if (wordCount > max) {
var substring = words.slice(0, max).join(' ');
$("#text").val(substring + ' ');
}
});
});
While you've already accepted an answer I thought I might be able to offer a slightly more refined version:
function limitWords(max){
// setting the value of the textarea:
$(this).val(function(i,v){
// i: the index of the current element in the collection,
// v: the current (pre-manipulation) value of the element.
// splitting the value by sequences of white-space characters,
// turning it into an Array. Slicing that array taking the first 10 elements,
// joining these words back together with a single space between them:
return v.split(/\s+/).slice(0,10).join(' ');
});
}
$('#demo').on('keyup paste input', limitWords);
JS Fiddle demo.
References:
JavaScript:
Array.prototype.join().
Array.prototype.slice().
String.prototype.split().
jQuery:
on().
val().
I am working on a simple text screen / terminal emulator (similar to the JQuery terminal plugin, but without RPC stuff and with window functionality).
Each line of the screen is a table row (a HTML string) and a print command can insert text with some attributes (e.g. foreground and background color). Each printed text
is enclosed by a span with style attributes, for example:
<span style="color:#000000;background-color:#111111">A</span><span style="color:#222222;background-color:#333333>BC</span>
This works fine. Now I want to add a function which gives me all attributes of a character at a given screen position,
in the above line the character at position 0 (A) has the color #000000.
So I have to count characters which don't belong to the span tag and get the last preceding styles. My first rather error prone solution is:
function getAttr(line, position) {
var result = {foreground:'', background:''},
ch = '', i, j = -1, tag = false;
// Count characters
for (i = 0; i < line.length && j < position; i++) {
ch = line.charAt(i);
if (ch == '<') {
tag = true;
}
if (ch == '>') {
tag = false;
}
else if (!tag) {
j++;
}
}
i--;
// Find styles
while (i > 0 && line.charAt(i) != '<') {
if (line.substr(i, 6) == 'color:') {
result.foreground = line.substr(i + 6, 7);
}
if (line.substr(i, 17) == 'background-color:') {
result.background = line.substr(i + 17, 7);
}
i--;
}
return result;
}
Is there a simpler solution without counting characters (maybe JQuery or a regex)?
This is similar to
Get parent element of a selected text
but I don't need a selection, just a character index.
A possible way to handle building a data structure that allows you to index each line and get at the character and it's associated styles could be done using the following snippet for each line. This assumes the markup you're generating for the HTML shown above is fairly stable as well (you could account for variations in the regex if needed):
var tagre = /\<span style="([^"]+)"\>([A-Za-z]+)\<\/span\>/ig,
s = '<span style="color:#000000;background-color:#111111">A</span><span style="color:#222222;background-color:#333333">BC</span>';
var matches,
positions = [];
while (matches = tagre.exec(s)) {
var len = matches[2].length,
chars = matches[2],
styles = {};
matches[1].split(';').forEach(function(o) {
var _s = o.split(':'),
key = _s[0],
val = _s[1];
styles[key] = val;
});
for (var i=0; i < len; i++) {
var char = chars[i];
positions.push({ 'char': char, 'styles': styles });
}
}
console.log("positions=%o", positions);
This would give you an array for each line that looks like the following:
[
{ char: 'A',
styles: { 'background-color': '#111111', 'color': '#000000' }
},
{ char: 'B',
styles: { 'background-color': '#333333', 'color': '#222222' }
},
{ char: 'C',
styles: { 'background-color': '#333333', 'color': '#222222' }
}
]
That would let you index into each line by integer character position and get the character at that position along with the associated styles as an object.
I'd leave the task of parsing HTML to browser and just use the resulting DOM tree. Here's some pseudo-code you could use based on the idea of using the DOM tree:
function getAttr(lineNumber, position) {
var lineDom = getDOMContainerForLineNumber(lineNumber);
var current = 0; // the current character position
function getAttrRec(elems, foreground, background) {
for(elem in elems) {
if(elem is <span>) {
var res = getAttrRec(elem.children, elem.foregroundColor, elem.backgroundColor);
if(res != null)
return res;
} else if(elem is TEXT) {
current += elem.textLength;
if(current >= position)
return {foreground: foreground, background: background};
}
}
return null;
}
return getAttrRec(lineDom.children, black, black);
}
This is just a very rough sketch though. Especially you'll have to watch out for whitespaces - they are stripped pretty intensively by browsers. So directly relying on the text length might not work in your case. Also you might want to handle the case that a span tag does not contain foreground or background color information.