Restore cursor position after .replace() - javascript

My biggest problem is that after it's replaced, the cursor defaults to the end of the textarea. That's no issue if I'm typing, but if I'm going back and editing, it's really annoying. Here's what I tried (the id of the textarea is "area")
var el = e.area;
position = el.selectionStart; // Capture initial position
el.value = el.value.replace('\u0418\u0410', '\u042F');
el.selectionEnd = position; // Set the cursor back to the initial position.

You can try the following code snippet. In its current form, it replaces == with +, but it allows to replace any string with another one, shorter or longer.
In order to maintain the cursor position, you have to save and restore the selectionStart and the selectionEnd. An offset is calculated to account for the difference in length between the two strings, and the number of occurrences before the cursor.
The use of setTimeout ensures that the newly typed character has been inserted in the text before doing the processing.
var area = document.getElementById("area");
var getCount = function (str, search) {
return str.split(search).length - 1;
};
var replaceText = function (search, replaceWith) {
if (area.value.indexOf(search) >= 0) {
var start = area.selectionStart;
var end = area.selectionEnd;
var textBefore = area.value.substr(0, end);
var lengthDiff = (replaceWith.length - search.length) * getCount(textBefore, search);
area.value = area.value.replace(search, replaceWith);
area.selectionStart = start + lengthDiff;
area.selectionEnd = end + lengthDiff;
}
};
area.addEventListener("keypress", function (e) {
setTimeout(function () {
replaceText("==", "+");
}, 0)
});
<textarea id="area" cols="40" rows="8"></textarea>

Related

How to make .replace script "digit-case-sensitive"?

I have a simple side-project here to help me at work. What I have is a script that will replace a specified number with another.
My problem now though is, I cannot find a way to make it "digit-case-sensitive" (I'm not sure what it's called sorry), meaning, if I want to change the following, they replace only the specified and not anything else. For example:
10 = 80
1 = 75
0 = 65
The problem is, if I replace 10, there's a tendency that it will become 865.
It's changing 10 to 80 first and then the 0 to 65.
Now I really need help as to how do I make the replace script to the specified digit only and not cut the digits or take only half and change them.
Also, how can I make it so that it changes only once for 1 click of button? In this case, it's processing it twice with 1 click of button. It changes 10 to 80 first and then the 0 to 65. What I'd like is run the script only once per click. In this flawed script, it should only be 65 and not process the 0 to 65, since doing so should take 2 clicks.
Here's my sample code, there will be thousands of digits to replace once I move on from this obstacle.
function fixTextarea(textarea) {
textarea.value = textarea.value.replace("", "")
.replaceAll("10", "80")
.replaceAll("1", "75")
.replaceAll("0", "65")
};
function fixtext() {
let textarea = document.getElementById("textarea1");
textarea.select();
fixTextarea(textarea);
}
window.addEventListener('DOMContentLoaded', function(e) {
var area = document.getElementById("textarea1");
var getCount = function(str, search) {
return str.split(search).length - 1;
};
var replace = function(search, replaceWith) {
if (typeof(search) == "object") {
area.value = area.value.replace(search, replaceWith);
return;
}
if (area.value.indexOf(search) >= 0) {
var start = area.selectionStart;
var end = area.selectionEnd;
var textBefore = area.value.substr(0, end);
var lengthDiff = (replaceWith.length - search.length) * getCount(textBefore, search);
area.value = area.value.replace(search, replaceWith);
area.selectionStart = start + lengthDiff;
area.selectionEnd = end + lengthDiff;
}
};
});
<textarea id="textarea1" name="textarea1">10</textarea>
<button onclick="fixtext()">Update</button>
I apologize in advance for not being able to make myself too clear.
You could replace with searching for alternative strings and take the longest first.
For prevent changing the value again on a click, store the orginal value to a data-* attribute.
const
replace = s => s.replace(/10|1|0/g, s => ({ 10: '80', 1: '75', 0: '65' }[s]));
console.log(replace('1010'));
console.log(replace('01'));
You'll have to be a little creative, by changing "10" to something that has special meaning so that the match on "0" won't change it, then change it to your end result:
function fixTextarea(textarea) {
textarea.value = textarea.value.replace("", "")
.replaceAll("10", "xx")
.replaceAll("1", "75")
.replaceAll("0", "65")
.replaceAll("xx", "80")
};
You can do the replace in an if else if else block
function fixTextarea(textarea) {
if(textarea.value === "10"){
textarea.value = textarea.value.replace("", "").replaceAll("10", "80")
}else if(textarea.value === "1"){
textarea.value = textarea.value.replace("", "").replaceAll("1", "75")
}else{
textarea.value = textarea.value.replace("", "").replaceAll("0", "65")
}
};
function fixtext() {
let textarea = document.getElementById("textarea1");
textarea.select();
fixTextarea(textarea);
}
window.addEventListener('DOMContentLoaded', function(e) {
var area = document.getElementById("textarea1");
var getCount = function(str, search) {
return str.split(search).length - 1;
};
var replace = function(search, replaceWith) {
if (typeof(search) == "object") {
area.value = area.value.replace(search, replaceWith);
return;
}
if (area.value.indexOf(search) >= 0) {
var start = area.selectionStart;
var end = area.selectionEnd;
var textBefore = area.value.substr(0, end);
var lengthDiff = (replaceWith.length - search.length) * getCount(textBefore, search);
area.value = area.value.replace(search, replaceWith);
area.selectionStart = start + lengthDiff;
area.selectionEnd = end + lengthDiff;
}
};
});
<textarea id="textarea1" name="textarea1">10</textarea>
<button onclick="fixtext()">Update</button>

Get occurence between # and textarea current position

I'd like to get the word after # depending on the current writing position of a textarea. More precisely:
if the current cursor position is on any letter of #<user>, the answer should be <user>
if the current cursor position is on any other word, the answer should be empty ''
I'm struggling with this, but don't find any "nice" way to do it.
$('#hey').on('click', function() { alert(); });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<textarea id="chat">hello #user it's me this is a long text, here is another #username cheers!</textarea>
<span id="hey">CLICK ME</span>
Having updated the code from the assumed duplicate Get current word on caret position, the result is as follows
function getCaretPosition(ctrl) {
var start, end;
if (ctrl.setSelectionRange) {
start = ctrl.selectionStart;
end = ctrl.selectionEnd;
} else if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
start = 0 - range.duplicate().moveStart('character', -100000);
end = start + range.text.length;
}
return {
start: start,
end: end
}
}
$("textarea").on("click keyup", function () {
var caret = getCaretPosition(this);
var endPos = this.value.indexOf(' ',caret.end);
if (endPos ==-1) endPos = this.value.length;
var result = /\S+$/.exec(this.value.slice(0, endPos));
var lastWord = result ? result[0] : null;
if (lastWord) lastWord = lastWord.replace(/['";:,.\/?\\-]$/, ''); // remove punctuation
$("#atID").html((lastWord && lastWord.indexOf("#") == 0)?lastWord:"");
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<textarea>Follow me on twitter #mplungjan if you want</textarea><br/>
<span id="atID"></span>

add value to textarea using jquery (simple texteditor)

im trying to make a simple texteditor. its work fine, but i want to add the text on the selected text at textarea.
in my code, when i click bold button it will show [b][/b] with cursor focused inside b tags, and when i click the button again, it will show inside b tags again. (this is what i want dont mind this code)
function typeInTextarea(el, newText)
{
var start = el.prop("selectionStart")
var end = el.prop("selectionEnd")
var text = el.val()
var before = text.substring(0, start)
var after = text.substring(end, text.length)
el.val(before + newText + after)
el[0].selectionStart = el[0].selectionEnd = start - 4 + newText.length
el.focus()
return false
}
$("#button-bold").on("click", function()
{
typeInTextarea($("#textareapost"), "[b][/b]")
return false
});
what im looking right now is, when I select "abcd" then i click the button, it will show [b]abcd[/b], i success with this code:
function typeInTextarea(el, newText)
{
var start = el.prop("selectionStart")
var end = el.prop("selectionEnd")
var text = el.val()
var before = text.substring(0, start)
var after = text.substring(end, text.length)
el.val(before + newText.substring(0,3) + after + newText.substring(3,7)) //i making change in this one
el[0].selectionStart = el[0].selectionEnd = newText.length + end
el.focus()
return false
}
but when i didnt select the "abcd" text, it will show [b]abcd[/b][b][abcd/b]. just like copying the value of the text.
what im asking is, how to add value on selected text (not replace it)
and making if function when text is selected will add [b]abcd[/b] else will add [b] [/b] while the [b]abcd[/b] still there.
basicly it will be like stackoverflow editor, but without live view. thanks for advance, hope I found the answer. been looking this for a weeks.
found 1 answer
JQUERY Set Textarea Cursor to End of Text
and modify to
var b = { pos: 3, txt: function(s){ return '[b]' + s +'[/b]'; }}
$.fn.selectRange = function(start, end) {
return this.each(function() {
if (this.setSelectionRange) {
this.focus();
this.setSelectionRange(start, end);
} else if (this.createTextRange) {
var range = this.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
});
};
$('#this-b').on('click',function(e){
var cc = $('#this-txta')[0].selectionStart;
var str = $('#this-txta').val();
var en = $('#this-txta')[0].selectionEnd;
var sstr = str.substring(cc,en);
$('#this-txta').val( str.substring(0, cc) + b.txt(sstr) + str.substring(en) ).focus();
var pst = cc + b.pos
var pen = en + b.pos
$('#this-txta').selectRange(pst,pen);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="this-b"><b>B</b></button>
<br/>
<textarea id="this-txta">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</textarea>
Sceditor has similar functionality and is open source.
Check how it was done here
http://www.sceditor.com/

Traversing contenteditable paragraphs with arrow keys

I'm try to traverse between contenteditable paragraphs using the arrow keys. I can't put a containing div around all paragraphs as the may be divided by other non-editable elements.
I need to be able to determine the character length of the first line so that when the up arrow key is pressed when the cursor is on the line then it will jump up to the previous paragraph - hopefully keeping the cursor position relative to the line.
I can get the cursor index with:
function cursorIndex() {
return window.getSelection().getRangeAt(0).startOffset;
}
and set it with: as found here - Javascript Contenteditable - set Cursor / Caret to index
var setSelectionRange = function(element, start, end) {
var rng = document.createRange(),
sel = getSelection(),
n, o = 0,
tw = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, null);
while (n = tw.nextNode()) {
o += n.nodeValue.length;
if (o > start) {
rng.setStart(n, n.nodeValue.length + start - o);
start = Infinity;
}
if (o >= end) {
rng.setEnd(n, n.nodeValue.length + end - o);
break;
}
}
sel.removeAllRanges();
sel.addRange(rng);
};
var setCaret = function(element, index) {
setSelectionRange(element, index, index);
};
Say the cursor is at the top row of the third paragraph and the up arrow is pressed, I would like it to jump to the bottom row of the second paragraph
http://jsfiddle.net/Pd52U/2/
Looks like there's no easy way to do this, I have the following working example. There's a bit of processing so it's a little slow and it can be out by the odd character when moving up and down between paragraph.
Please inform me of any improvements that can be made.
http://jsfiddle.net/zQUhV/47/
What I've done is split the paragraph by each work, insert them into a new element one by one, checking for a height change - when it does change a new line was added.
This function returns an array of line objects containing the line text, starting index and end index:
(function($) {
$.fn.lines = function(){
words = this.text().split(" "); //split text into each word
lines = [];
hiddenElement = this.clone(); //copies font settings and width
hiddenElement.empty();//clear text
hiddenElement.css("visibility", "hidden");
jQuery('body').append(hiddenElement); // height doesn't exist until inserted into document
hiddenElement.text('i'); //add character to get height
height = hiddenElement.height();
hiddenElement.empty();
startIndex = -1; // quick fix for now - offset by one to get the line indexes working
jQuery.each(words, function() {
lineText = hiddenElement.text(); // get text before new word appended
hiddenElement.text(lineText + " " + this);
if(hiddenElement.height() > height) { // if new line
lines.push({text: lineText, startIndex: startIndex, endIndex: (lineText.length + startIndex)}); // push lineText not hiddenElement.text() other wise each line will have 1 word too many
startIndex = startIndex + lineText.length +1;
hiddenElement.text(this); //first word of the next line
}
});
lines.push({text: hiddenElement.text(), startIndex: startIndex, endIndex: (hiddenElement.text().length + startIndex)}); // push last line
hiddenElement.remove();
lines[0].startIndex = 0; //quick fix for now - adjust first line index
return lines;
}
})(jQuery);
Now you could use that to measure the number of character up until the point of the cursor and apply that when traversing paragraph to keep the cursor position relative to the start of the line. However that can produce wildly inaccurate results when considering the width of an 'i' to the width of an 'm'.
Instead it would be better to find the width of the line up to the point of the cursor:
function distanceToCaret(textElement,caretIndex){
line = findLineViaCaret(textElement,caretIndex);
if(line.startIndex == 0) {
// +1 needed for substring to be correct but only first line?
relativeIndex = caretIndex - line.startIndex +1;
} else {
relativeIndex = caretIndex - line.startIndex;
}
textToCaret = line.text.substring(0, relativeIndex);
hiddenElement = textElement.clone(); //copies font settings and width
hiddenElement.empty();//clear text
hiddenElement.css("visibility", "hidden");
hiddenElement.css("width", "auto"); //so width can be measured
hiddenElement.css("display", "inline-block"); //so width can be measured
jQuery('body').append(hiddenElement); // doesn't exist until inserted into document
hiddenElement.text(textToCaret); //add to get width
width = hiddenElement.width();
hiddenElement.remove();
return width;
}
function findLineViaCaret(textElement,caretIndex){
jQuery.each(textElement.lines(), function() {
if(this.startIndex <= caretIndex && this.endIndex >= caretIndex) {
r = this;
return false; // exits loop
}
});
return r;
}
Then split the target line up into characters and find the point that closest matches the width above by adding characters one by one until the point is reached:
function getCaretViaWidth(textElement, lineNo, width) {
line = textElement.lines()[lineNo-1];
lineCharacters = line.text.replace(/^\s+|\s+$/g, '').split("");
hiddenElement = textElement.clone(); //copies font settings and width
hiddenElement.empty();//clear text
hiddenElement.css("visibility", "hidden");
hiddenElement.css("width", "auto"); //so width can be measured
hiddenElement.css("display", "inline-block"); //so width can be measured
jQuery('body').append(hiddenElement); // doesn't exist until inserted into document
if(width == 0) { //if width is 0 index is at start
caretIndex = line.startIndex;
} else {// else loop through each character until width is reached
hiddenElement.empty();
jQuery.each(lineCharacters, function() {
text = hiddenElement.text();
prevWidth = hiddenElement.width();
hiddenElement.text(text + this);
elWidth = hiddenElement.width();
caretIndex = hiddenElement.text().length + line.startIndex;
if(hiddenElement.width() > width) {
// check whether character after width or before width is closest
if(Math.abs(width - prevWidth) < Math.abs(width - elWidth)) {
caretIndex = caretIndex -1; // move index back one if previous is closes
}
return false;
}
});
}
hiddenElement.remove();
return caretIndex;
}
That with the following keydown function is enough to traverse pretty accurately between contenteditable paragraphs:
$(document).on('keydown', 'p[contenteditable="true"]', function(e) {
//if cursor on first line & up arrow key
if(e.which == 38 && (cursorIndex() < $(this).lines()[0].text.length)) {
e.preventDefault();
if ($(this).prev().is('p')) {
prev = $(this).prev('p');
getDistanceToCaret = distanceToCaret($(this), cursorIndex());
lineNumber = prev.lines().length;
caretPosition = getCaretViaWidth(prev, lineNumber, getDistanceToCaret);
prev.focus();
setCaret(prev.get(0), caretPosition);
}
// if cursor on last line & down arrow
} else if(e.which == 40 && cursorIndex() >= $(this).lastLine().startIndex && cursorIndex() <= ($(this).lastLine().startIndex + $(this).lastLine().text.length)) {
e.preventDefault();
if ($(this).next().is('p')) {
next = $(this).next('p');
getDistanceToCaret = distanceToCaret($(this), cursorIndex());
caretPosition = getCaretViaWidth(next, 1, getDistanceToCaret);
next.focus();
setCaret(next.get(0), caretPosition);
}
//if start of paragraph and left arrow
} else if(e.which == 37 && cursorIndex() == 0) {
e.preventDefault();
if ($(this).prev().is('p')) {
prev = $(this).prev('p');
prev.focus();
setCaret(prev.get(0), prev.text().length);
}
// if end of paragraph and right arrow
} else if(e.which == 39 && cursorIndex() == $(this).text().length) {
e.preventDefault();
if ($(this).next().is('p')) {
$(this).next('p').focus();
}
};

Find out the 'line' (row) number of the cursor in a textarea

I would like to find out and keep track of the 'line number' (rows) of the cursor in a textarea. (The 'bigger picture' is to parse the text on the line every time a new line is created/modified/selected, if of course the text was not pasted in. This saves parsing the whole text un-necessarily at set intervals.)
There are a couple of posts on StackOverflow however none of them specifically answer my question, most questions are for cursor position in pixels or displaying lines numbers besides the textarea.
My attempt is below, it works fine when starting at line 1 and not leaving the textarea. It fails when clicking out of the textarea and back onto it on a different line. It also fails when pasting text into it because the starting line is not 1.
My JavaScript knowledge is pretty limited.
<html>
<head>
<title>DEVBug</title>
<script type="text/javascript">
var total_lines = 1; // total lines
var current_line = 1; // current line
var old_line_count;
// main editor function
function code(e) {
// declare some needed vars
var keypress_code = e.keyCode; // key press
var editor = document.getElementById('editor'); // the editor textarea
var source_code = editor.value; // contents of the editor
// work out how many lines we have used in total
var lines = source_code.split("\n");
var total_lines = lines.length;
// do stuff on key presses
if (keypress_code == '13') { // Enter
current_line += 1;
} else if (keypress_code == '8') { // Backspace
if (old_line_count > total_lines) { current_line -= 1; }
} else if (keypress_code == '38') { // Up
if (total_lines > 1 && current_line > 1) { current_line -= 1; }
} else if (keypress_code == '40') { // Down
if (total_lines > 1 && current_line < total_lines) { current_line += 1; }
} else {
//document.getElementById('keycodes').innerHTML += keypress_code;
}
// for some reason chrome doesn't enter a newline char on enter
// you have to press enter and then an additional key for \n to appear
// making the total_lines counter lag.
if (total_lines < current_line) { total_lines += 1 };
// putput the data
document.getElementById('total_lines').innerHTML = "Total lines: " + total_lines;
document.getElementById('current_line').innerHTML = "Current line: " + current_line;
// save the old line count for comparison on next run
old_line_count = total_lines;
}
</script>
</head>
<body>
<textarea id="editor" rows="30" cols="100" value="" onkeydown="code(event)"></textarea>
<div id="total_lines"></div>
<div id="current_line"></div>
</body>
</html>
You would want to use selectionStart to do this.
<textarea onkeyup="getLineNumber(this, document.getElementById('lineNo'));" onmouseup="this.onkeyup();"></textarea>
<div id="lineNo"></div>
<script>
function getLineNumber(textarea, indicator) {
indicator.innerHTML = textarea.value.substr(0, textarea.selectionStart).split("\n").length;
}
</script>
This works when you change the cursor position using the mouse as well.
This is tough because of word wrap. It's a very easy thing to count the number of line breaks present, but what happens when the new row is because of word wrap? To solve this problem, it's useful to create a mirror (credit: github.com/jevin). Here's the idea:
Create a mirror of the textarea
Send the content from the beginning of the textarea to the cursor to the mirror
Use the height of the mirror to extract the current row
On JSFiddle
jQuery.fn.trackRows = function() {
return this.each(function() {
var ininitalHeight, currentRow, firstIteration = true;
var createMirror = function(textarea) {
jQuery(textarea).after('<div class="autogrow-textarea-mirror"></div>');
return jQuery(textarea).next('.autogrow-textarea-mirror')[0];
}
var sendContentToMirror = function (textarea) {
mirror.innerHTML = String(textarea.value.substring(0,textarea.selectionStart-1)).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br />') + '.<br/>.';
calculateRowNumber();
}
var growTextarea = function () {
sendContentToMirror(this);
}
var calculateRowNumber = function () {
if(firstIteration){
ininitalHeight = $(mirror).height();
currentHeight = ininitalHeight;
firstIteration = false;
} else {
currentHeight = $(mirror).height();
}
// Assume that textarea.rows = 2 initially
currentRow = currentHeight/(ininitalHeight/2) - 1;
//remove tracker in production
$('.tracker').html('Current row: ' + currentRow);
}
// Create a mirror
var mirror = createMirror(this);
// Style the mirror
mirror.style.display = 'none';
mirror.style.wordWrap = 'break-word';
mirror.style.whiteSpace = 'normal';
mirror.style.padding = jQuery(this).css('padding');
mirror.style.width = jQuery(this).css('width');
mirror.style.fontFamily = jQuery(this).css('font-family');
mirror.style.fontSize = jQuery(this).css('font-size');
mirror.style.lineHeight = jQuery(this).css('line-height');
// Style the textarea
this.style.overflow = "hidden";
this.style.minHeight = this.rows+"em";
var ininitalHeight = $(mirror).height();
// Bind the textarea's event
this.onkeyup = growTextarea;
// Fire the event for text already present
// sendContentToMirror(this);
});
};
$(function(){
$('textarea').trackRows();
});
This worked for me:
function getLineNumber(textarea) {
return textarea.value.substr(0, textarea.selectionStart) // get the substring of the textarea's value up to the cursor position
.split("\n") // split on explicit line breaks
.map((line) => 1 + Math.floor(line.length / textarea.cols)) // count the number of line wraps for each split and add 1 for the explicit line break
.reduce((a, b) => a + b, 0); // add all of these together
};
Inspired by colab's answer as a starting point, this includes the number of word wraps without having to introduce a mirror (as in bradbarbin's answer).
The trick is simply counting how many times the number of columns textarea.cols can divide the length of each segment between explicit line breaks \n.
Note: this starts counting at 1.

Categories