Javascript move text between brackets in textarea - javascript

I have a simple text area with text like this:
Lorem Ipsum [text] dolar, rock n'[more] roller.
I am trying to detect when my cursor is between the brackets and if so, allow for a Ctrl+ Right or left arrow to move the text and brackets right or left by one position with each key press without going past start or end of the line and without moving past an adjacent bracketed block. I guess this is the same as the Ctrl+Right arrow copying the first character (or space) on the right side, to the left side and vice versa.
I have some basic jquery skills but all of my attempts at this have failed miserably. Otherwise, I would have pasted in a partial block of code showing what I have. Thanks,

The solution will require a few different pieces:
Finding the cursor location:
function getCaretPosition(element)
{
var CaretPos = 0;
//Old IE way
if ( document.selection )
{
element.focus();
var textSelection = document.selection.createRange();
textSelection.moveStart( 'character', -element.value.length );
CaretPos = textSelection.text.length;
}
//DOM way
else if ( element.selectionStart || element.selectionStart == '0' )
CaretPos = element.selectionStart;
return (CaretPos);
}
Find the location of the string (to see if caret is inside it). You can use a Regular expression like:
var search = /(\[[^\[\]]+\])/g;
//Then loop and find all the matches and check if
//the caret is between them
var matches = myString.match( search );
You will also need to listen to keypress events on the textarea and in that event listener:
If it's a left arrow or right arrow (with ctrl button held down) then:
find caret position, see if it's inside a bracket piece of text and if so:
Move the text left or right (you'd grab substrings of the text before/after the bracketed text and create a new concatenated string)
Those pieces should make this work. (I'm not going to write it for you as that won't really help you)

Here is a fiddle that does this, and here is the code:
$('textarea').on('keydown', function(e) {
// Is Ctrl-left and Ctrl+right pressed?
if (e.ctrlKey && (e.which === 37 || e.which === 39)) {
var pos = this.selectionStart;
var val = this.value;
var openBracketOnLeft = val.substr(0, pos).lastIndexOf('[');
var closeBracketOnLeft = val.substr(0, pos).lastIndexOf(']');
var closeBracketOnRight = val.substr(pos).indexOf(']');
// Is start of selection within two brackets?
if (openBracketOnLeft > closeBracketOnLeft && closeBracketOnRight !== -1) {
closeBracketOnRight += pos + 1;
var tagText = val.substr(openBracketOnLeft, closeBracketOnRight - openBracketOnLeft);
var level = 0;
// Repeat moving the tag until we do not break another tag in two.
do {
// Is Ctrl-left pressed, and is tag not yet on far left?
if (e.which === 37 && openBracketOnLeft) {
ch = val.substr(openBracketOnLeft - 1, 1);
val = val.substr(0, openBracketOnLeft - 1)
+ tagText
+ ch
+ val.substr(closeBracketOnRight);
openBracketOnLeft--;
closeBracketOnRight--;
// Is Ctrl-right pressed, and is tag not yet on far right?
} else if (e.which === 39 && closeBracketOnRight < val.length) {
ch = val.substr(closeBracketOnRight, 1);
val = val.substr(0, openBracketOnLeft)
+ ch
+ tagText
+ val.substr(closeBracketOnRight + 1);
openBracketOnLeft++;
closeBracketOnRight++;
} else {
break;
}
level += ch == '[' ? 1 : ch == ']' ? -1 : 0;
} while (level);
// Select the tag, without the brackets
this.value = val;
this.selectionStart = openBracketOnLeft + 1;
this.selectionEnd = closeBracketOnRight - 1;
e.preventDefault();
}
};
});

Here is a way to do it without evil regex strings. Instead I wanted to try and do it with jQuery 'keydown' event which was inline with what the questioner mentioned (see: newb at jQuery). Also note that 'keydown' is better for this methodology as 'keyup' will fire multiple times, though I guess this will too... Anyways, here is what I came up with:
$('#inputFieldInQuestion').on('keydown', function (event) {
// if both the control key and left key are pushed
if (event.keyCode == 37 && event.ctrlKey) {
// grab the text from the input and caret position in the input box
var inputBoxText = $(this).val(),
currentCaretPosition = this.selectionStart
// loop through all the characters in the input box text
for (var i = 0; i < inputBoxText.length; i++) {
// if the current character is an open bracket start testing for the end
if (inputBoxText[i] === "[") {
for (var j = i + 1; j < inputBoxText.length; j++) {
// this means that there is another bracketed string in between the
// beginning and the current bracketed string
if (inputBoxText[j] === "[") { break }
// if instead we come to the end of the bracketed string will determine
// if the bounds make sense
else if (inputBoxText[j] === "]") {
// if the caret position is in the bounds that you have just created
// we continue the shift
if (currentCaretPosition > i && currentCaretPosition < j) {
// test as per the question if the bracketed string is adjascent
// to another bracketed string
if (inputBoxText[i - 1] !== "]") {
// if the bracketed text is all the way to the left of the
// input box
if (i > 0) {
// slice and dice the string and move things left by one
// character
var frontString = inputBoxText.substring(0, i),
stringToMove = inputBoxText.substring(i, j + 1),
endString = inputBoxText.substring(j + 1)
$(this).val(frontString.slice(0, i - 1) + stringToMove + frontString.slice(i - 1) + endString)
this.setSelectionRange(currentCaretPosition - 1, currentCaretPosition - 1); break
}
}
else { break }
}
}
}
}
}
// important so that the ctrl-left doesn't shift the cursor to the end of the word
return false;
}
// if both the control key and right key are pushed
else if (event.keyCode == 39 && event.ctrlKey) {
var inputBoxText = $(this).val(),
currentCaretPosition = this.selectionStart
for (var i = 0; i < inputBoxText.length; i++) {
if (inputBoxText[i] === "[") {
for (var j = i; j < inputBoxText.length; j++) {
if (inputBoxText[j] === "]") {
if (currentCaretPosition > i && currentCaretPosition < j) {
// test as per the question if the bracketed string is adjascent
// to another bracketed string
if (inputBoxText[j + 1] !== "[") {
// bracketed text is all the way to the right of the input box
if (inputBoxText.length - j > 1) {
var frontString = inputBoxText.substring(0, i),
stringToMove = inputBoxText.substring(i, j + 1),
endString = inputBoxText.substring(j + 1)
$(this).val(frontString + endString.slice(0, 1) + stringToMove + endString.slice(1))
this.setSelectionRange(currentCaretPosition + 1, currentCaretPosition + 1); break
}
}
else { break }
}
}
}
}
}
return false;
}
})
This might be the most complicated way to do this ever but it does seem to work and satisfy all the constraints posed. Since I just noticed that this was flagged regex, this might be a terrible solution. Let the evisceration begin!
Super Bonus: This will work if you have any number of "[]" pairs in the string.

Related

JavaScript function is always returning true where it should be false

My JavaScript function is always returning true whatever I write. The problem given wants me to validate that:
space at beginning or at end are Not allowed.
every character in the field must be a letter or a space (at most 1 space!) to seperate the words in the field (it's about palindrome words, i already wrote its function estPalindrome(word)).
the button "chercher" must be enabled once 1 word is valid.
I have also tried to replace phrase and len inside and outside the function. The results are terrible:
Inside it only alerts "true" when I blur out with empty field
outside it alerts "true" every time, whatever I write.
Note: I am NOT allowed to change anything in HTML code.
var phrase = document.getElementById('phrase').value;
var len = phrase.length;
function estPhrase() {
var verify = true;
for (i = 0; i < len; i++) {
let ch = phrase.charAt(i);
let isLetter = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == ' ';
if (!isLetter || (ch == ' ' && phrase.charAt(i + 1) == ' ') || phrase.charAt(0) == ' ' || phrase.charAt(len - 1) == ' ')
verify = false;
}
if (len > 0)
butt.removeAttribute('disabled');
var words = new Array();
for (i = 0, j = 0; i < len; i++, j++) {
ch = phrase.charAt(i);
words[j] = "";
while (ch != ' ') {
words[j] += ch;
i++;
}
if (estPalindrome(words[j]) == true)
count++;
}
console.log(verify);
}
<div> Phrase: <input type="text" id="phrase" onblur="estPhrase()" size="50" /></div>
As the html can't be changed and the function estPhrase() is called directly from the <input> element, then the phrase and len variables should be moved into the function body.
It was quite hard to refactor and debug your current code based on your current requirements. Certain segments of code were in need of simplification. Whilst I would not normally do this, it was just easier to re-write the majority of the function. I know this may not be desirable but doing so may introduce you to new techniques and Javascript functions whilst also improving code readability at the same time.
Not knowing the inner workings of your estPalindrome() function, for testing purposes I have return true in all cases. Returning false would only prevent the counter from incrementing.
One thing of note is what 'should' happen if verify becomes false. Should the verify flag be tested prior to splitting the phrase into individual words? If so, then it would abort the remainder of the function and not cause an error in your estPalindrone() function.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<div> Phrase: <input type="text" id="phrase" onblur="estPhrase()" size="50"/></div>
<button id="chercher" disabled>Chercher Button</button>
</body>
<script>
function estPhrase() {
let phrase = document.getElementById('phrase').value;
let chercher = document.getElementById('chercher');
let verify = true;
// Check phrase does not contain a leading or trailing edge space.
if (phrase.length !== phrase.trim().length) {
verify = false;
}
// Check phrase does not contain more than one sequential space.
if (phrase.includes(' ')) {
verify = false;
}
// Check phrase only contains valid characters (a-z, A-Z, space).
if (/[^a-zA-Z ]/.test(phrase)) {
verify = false;
}
// Bail early on failure. It could also be performed within the above 'if' statements.
// if (verify === false) {
// return;
// }
// Now the phrase has been validated, splitting the phrase
// into words for individual consumption should be error free.
let words = phrase.split(' ');
let count = 0;
// Pass the words one by one into the 'estPalindrone' function.
for (let word in words) {
if (estPalindrome(word) === true) {
count++;
}
}
// Disable button if count is 0.
if (count === 0) {
chercher.setAttribute('disabled', '');
} else {
chercher.removeAttribute('disabled');
}
// Testing.
console.log('Phrase: ' + phrase);
console.log('Verify: ' + verify);
console.log('Words: ' + words);
console.log('Count: ' + count);
}
function estPalindrome(word) {
// Do stuff with word...
return true;
}
</script>
</html>
Modified a little and found it is working fine. working code
let phrase = 'abcd lpp';
// let phrase = ' abcdlpp';
// let phrase = 'abcdlpp ';
// let phrase = 'abc,dlpp';
let len = phrase.length;
function estPhrase() {
let verify = true;
for (let i = 0; i < len; i++) {
let ch = phrase.charAt(i);
let isLetter = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === ' ';
if (!isLetter || (ch === ' ' && phrase.charAt(i + 1) === ' ') || phrase.charAt(0) === ' ' || phrase.charAt(len - 1) === ' ')
verify = false;
}
console.log(verify);
}
estPhrase(phrase)

Match cursor position for substring after text replace [duplicate]

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.

textRange MoveToPoint() IE

Have a problem with moveToPoint() method of textRange IE11;
Seems like it dosen't work if pointed node wasn't in first screen;
document.addEventListener( "click", function(e) {
var x = e.clientX;
var y = e.clientY;
var range = document.body.createTextRange();
range.moveToPoint(x, y);
range.expand('word');
console.log(range);
console.log(range.text);
});
This code grab words from click point, but it wokrs normal only if we clicking in node's that were on first scroll.
If we scroll little bit down to the node that wasnt in first scroll, we will catch the exeception.
Does anybody know how to handle this situation correctly?
You can use offsetX, offsetY properties.
Or you can add scroll position to x and y vars, using scrollLeft and scrollTop properties of parent element.
I can confirm that such a bug exists in IE11. You can find details here in comments: http://generatedcontent.org/post/69213745095/ie11review-part1
Possible solution (after you create a range):
range.moveToElementText(e.target);
range.collapse(true)
range.expand("word")
Now you have the first word selected. You have to check now if the selected word fits your mouse click position using TextRange properties boundingHeight, boundingWidth, boundingLeft and boundingTop. If it doesn't, you move in cycle to the next word:
range.collapse(false);
range.expand("word");
Well, inspired by #dinalt and #JAYBEkster, I came up this solution. Maybe someone will need this after all.
Code below(for IE, didn't check all versons, but it works fine 11+) grab word in complex nested html.
Step bys step how it works:
Firstable we create range from e.target
We collapse it to the begining.
That will be awesome if we just could expand("word") and iterate every each word, but unfortunantly we cant. So we loop through characters split them to words and compare their bounding's with e.clientX, and Y, in the other hand by iterating charakters insted of words we get more control.
We get correct one and split target.innerText with it, to get left and right part of node's text.
After that we split each part again but this our separators (regexp with all white spaces and word separaters i could imagine) and we get two arr's.
Why we do steps 4 and 5? Cause we find some text that end's in that node, but it may not be the end or start of ther word, for some styling reasons, acent part of ther word my bein it's own node and target.node my just be the middle part of the word. So we have to jump up throug node's and find word's ending.
If u sure that ur html dosen't complain that u can skip all other steps.
Then if arr is empty we run our recursive function that runs throug node's and grab's text until find separator.
Yeah it really look like roket since for such, that we mayed think simple task.
But I actualy couldn't find a better solution, There are was a couple of options, bu all of them wasnt's as universal as I need it to be.
Benefits of this code that it really dosen't care about complexivity of html at all.
I will post here the link to my github repositiry where u can find the full code of wordGraber, that will work this Chrome, Safari, FF and IE of course.
https://github.com/6graNik/wordGrabber
Below is just a part for IE.
function getWordFromEventIE(e) {
const x = e.clientX;
const y = e.clientY;
const innerText = e.target && e.target.innerText;
const separators = /([\s&^:;,!?(){}])+/;
const IErange = global.document.body.createTextRange();
try {
IErange.moveToElementText(e.target);
IErange.collapse(true);
let wholeSentenceLength = 0;
const reqIEcharTest = () => {
do {
IErange.expand("character");
wholeSentenceLength += 1;
}
while (!separators.test(IErange.text.slice(-1)) && wholeSentenceLength <= innerText.length);
const {boundingLeft, boundingTop, boundingWidth, boundingHeight, text} = IErange;
if (boundingLeft <= x && x <= (boundingLeft + boundingWidth)
&& boundingTop <= y && y <= (boundingTop + boundingHeight)) {
if (wholeSentenceLength <= innerText.length && !separators.test(text.slice(-1)) ) {
return text;
}
return text.substr(0, text.length - 1);
}
IErange.collapse(false);
return reqIEcharTest();
};
const text = reqIEcharTest().trim();
const innerTextArr = innerText.split(text);
const innerTextLeft = innerTextArr[0].split(separators);
const innerTextRight = innerTextArr[1].split(separators);
let leftPart;
if (innerTextLeft <= 1) {
leftPart = recursionWordGet(e.target, 'left') + innerTextLeft.slice(-1)[0];
} else {
leftPart = innerTextLeft.slice(-1)[0];
}
let rightPart;
if (innerTextRight <= 1) {
rightPart = innerTextRight[0] + recursionWordGet(e.target, 'right');
} else {
rightPart = innerTextRight[0];
}
return leftPart + text + rightPart;
} catch (err) {
console.log('>>>>>>>>>>>>>>>>>> text', err);
}
}
function recursionWordGet(target, option) {
const separators = /([\s&^:;,!?(){}])+/;
const uniqString = Date.now();
target.setAttribute("data-target", uniqString);
const {parentNode} = target;
const copyNode = parentNode.cloneNode(true);
copyNode.querySelector(`[data-target="${uniqString}"]`).innerText = uniqString;
const tagName = copyNode.tagName;
const text = copyNode.innerText;
const textArr = text.split(uniqString);
const textLeftPartArr = textArr[0].split(separators);
const textRightPartArr = textArr[1].split(separators);
if (option === 'right') {
let returnText;
if (textRightPartArr.length <= 1 && tagName === 'span') {
returnText = textRightPartArr[0] + recursionWordGet(parentNode, 'right');
} else {
returnText = textRightPartArr[0];
}
return returnText;
}
if (option === 'left') {
let returnText;
if (textLeftPartArr <= 1 && tagName === 'span') {
returnText = recursionWordGet(parentNode, 'left') + textLeftPartArr.slice(-1)[0];
} else {
returnText = textLeftPartArr.slice(-1)[0];
}
return returnText;
}
return '';
}

textarea with limited lines and char limits

i need functionaliy which will have TextArea with
1) maximum total lines- 6 and
2) in each line there must be maximum of 16 chars
3) if user enters 17th character the cursor should go to the next line
and user will type in there (the line will be counted)
4) if user reaches to the 7th line it will not allow user to write
5) if user type e.g "Hello, I Love StackOverflow and its features" (counting
from 1st Char 'H', the 16th char is 't' but it is whole word 'StackOverflow',
it shouldn't break and continue to next line e.g.
Hello, I Love St
ackOverflow
now the whole word should come to next line like:
Hello, I Love
StackOverflow
and its features
here is the link what i have done so far
http://jsfiddle.net/nqjQ2/2/
sometimes some of the functionality work, some times not, and facing browser issues for onKeyUp and onKeyDown
can anyone help me with it ?
I think this is mostly what you want:
<textarea id="splitLines"></textarea>
JavaScript:
var textarea = document.getElementById("splitLines");
textarea.onkeyup = function() {
var lines = textarea.value.split("\n");
for (var i = 0; i < lines.length; i++) {
if (lines[i].length <= 16) continue;
var j = 0; space = 16;
while (j++ <= 16) {
if (lines[i].charAt(j) === " ") space = j;
}
lines[i + 1] = lines[i].substring(space + 1) + (lines[i + 1] || "");
lines[i] = lines[i].substring(0, space);
}
textarea.value = lines.slice(0, 6).join("\n");
};
See the fiddle in action.
In Jquery
$(function () {
var limit = function (event) {
var linha = $(this).attr("limit").split(",")[0];
var coluna = $(this).attr("limit").split(",")[1];
var array = $(this)
.val()
.split("\n");
$.each(array, function (i, value) {
array[i] = value.slice(0, linha);
});
if (array.length >= coluna) {
array = array.slice(0, coluna);
}
$(this).val(array.join("\n"))
}
$("textarea[limit]")
.keydown(limit)
.keyup(limit);
})
<textarea limit='10,5' cols=10 rows=5 ></textarea>
http://jsfiddle.net/PVv6c/

IE's document.selection.createRange doesn't include leading or trailing blank lines

I'm trying to extract the exact selection and cursor location from a textarea. As usual, what's easy in most browsers is not in IE.
I'm using this:
var sel=document.selection.createRange();
var temp=sel.duplicate();
temp.moveToElementText(textarea);
temp.setEndPoint("EndToEnd", sel);
selectionEnd = temp.text.length;
selectionStart = selectionEnd - sel.text.length;
Which works 99% of the time. The problem is that TextRange.text doesn't return leading or trailing new line characters. So when the cursor is a couple of blank lines after a paragraph it yields a position at the end of the preceeding paragraph - rather than the actual cursor position.
eg:
the quick brown fox| <- above code thinks the cursor is here
| <- when really it's here
The only fix I can think of is to temporarily insert a character before and after the selection, grab the actual selection and then remove those temp characters again. It's a hack but in a quick experiment looks like it will work.
But first I'd like to be sure there's not an easier way.
I'm adding another answer since my previous one is already getting somewhat epic.
This is what I consider the best version yet: it takes bobince's approach (mentioned in the comments to my first answer) and fixes the two things I didn't like about it, which were first that it relies on TextRanges that stray outside the textarea (thus harming performance), and second the dirtiness of having to pick a giant number for the number of characters to move the range boundary.
function getSelection(el) {
var start = 0, end = 0, normalizedValue, range,
textInputRange, len, endRange;
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = el.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return {
start: start,
end: end
};
}
var el = document.getElementById("your_textarea");
var sel = getSelection(el);
alert(sel.start + ", " + sel.end);
The move by negative bazillion seems to work perfectly.
Here's what I ended up with:
var sel=document.selection.createRange();
var temp=sel.duplicate();
temp.moveToElementText(textarea);
var basepos=-temp.moveStart('character', -10000000);
this.m_selectionStart = -sel.moveStart('character', -10000000)-basepos;
this.m_selectionEnd = -sel.moveEnd('character', -10000000)-basepos;
this.m_text=textarea.value.replace(/\r\n/gm,"\n");
Thanks bobince - how can I vote up your answer when it's just a comment :(
A jquery plugin to get selection index start and end in text area. The above javascript codes didnt work for IE7 and IE8 and gave very inconsistent results, so I have written this small jquery plugin. Allows to temporarily save start and end index of the selection and hightlight the selection at a later time.
A working example and brief version is here: http://jsfiddle.net/hYuzk/3/
A more details version with comments etc. is here: http://jsfiddle.net/hYuzk/4/
// Cross browser plugins to set or get selection/caret position in textarea, input fields etc for IE7,IE8,IE9, FF, Chrome, Safari etc
$.fn.extend({
// Gets or sets a selection or caret position in textarea, input field etc.
// Usage Example: select text from index 2 to 5 --> $('#myTextArea').caretSelection({start: 2, end: 5});
// get selected text or caret position --> $('#myTextArea').caretSelection();
// if start and end positions are the same, caret position will be set instead o fmaking a selection
caretSelection : function(options)
{
if(options && !isNaN(options.start) && !isNaN(options.end))
{
this.setCaretSelection(options);
}
else
{
return this.getCaretSelection();
}
},
setCaretSelection : function(options)
{
var inp = this[0];
if(inp.createTextRange)
{
var selRange = inp.createTextRange();
selRange.collapse(true);
selRange.moveStart('character', options.start);
selRange.moveEnd('character',options.end - options.start);
selRange.select();
}
else if(inp.setSelectionRange)
{
inp.focus();
inp.setSelectionRange(options.start, options.end);
}
},
getCaretSelection: function()
{
var inp = this[0], start = 0, end = 0;
if(!isNaN(inp.selectionStart))
{
start = inp.selectionStart;
end = inp.selectionEnd;
}
else if( inp.createTextRange )
{
var inpTxtLen = inp.value.length, jqueryTxtLen = this.val().length;
var inpRange = inp.createTextRange(), collapsedRange = inp.createTextRange();
inpRange.moveToBookmark(document.selection.createRange().getBookmark());
collapsedRange.collapse(false);
start = inpRange.compareEndPoints('StartToEnd', collapsedRange) > -1 ? jqueryTxtLen : inpRange.moveStart('character', -inpTxtLen);
end = inpRange.compareEndPoints('EndToEnd', collapsedRange) > -1 ? jqueryTxtLen : inpRange.moveEnd('character', -inpTxtLen);
}
return {start: Math.abs(start), end: Math.abs(end)};
},
// Usage: $('#txtArea').replaceCaretSelection({start: startIndex, end: endIndex, text: 'text to replace with', insPos: 'before|after|select'})
// Options start: start index of the text to be replaced
// end: end index of the text to be replaced
// text: text to replace the selection with
// insPos: indicates whether to place the caret 'before' or 'after' the replacement text, 'select' will select the replacement text
replaceCaretSelection: function(options)
{
var pos = this.caretSelection();
this.val( this.val().substring(0,pos.start) + options.text + this.val().substring(pos.end) );
if(options.insPos == 'before')
{
this.caretSelection({start: pos.start, end: pos.start});
}
else if( options.insPos == 'after' )
{
this.caretSelection({start: pos.start + options.text.length, end: pos.start + options.text.length});
}
else if( options.insPos == 'select' )
{
this.caretSelection({start: pos.start, end: pos.start + options.text.length});
}
}
});
N.B. Please refer to my other answer for the best solution I can offer. I'm leaving this here for background.
I've come across this problem and written the following that works in all cases. In IE it does use the method you suggested of temporarily inserting a character at the selection boundary, and then uses document.execCommand("undo") to remove the inserted character and prevent the insertion from remaining on the undo stack. I'm pretty sure there's no easier way. Happily, IE 9 will support the selectionStart and selectionEnd properties.
function getSelectionBoundary(el, isStart) {
var property = isStart ? "selectionStart" : "selectionEnd";
var originalValue, textInputRange, precedingRange, pos, bookmark;
if (typeof el[property] == "number") {
return el[property];
} else if (document.selection && document.selection.createRange) {
el.focus();
var range = document.selection.createRange();
if (range) {
range.collapse(!!isStart);
originalValue = el.value;
textInputRange = el.createTextRange();
precedingRange = textInputRange.duplicate();
pos = 0;
if (originalValue.indexOf("\r\n") > -1) {
// Trickier case where input value contains line breaks
// Insert a character in the text input range and use that as
// a marker
range.text = " ";
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length - 1;
// Executing an undo command to delete the character inserted
// prevents this method adding to the undo stack. This trick
// came from a user called Trenda on MSDN:
// http://msdn.microsoft.com/en-us/library/ms534676%28VS.85%29.aspx
document.execCommand("undo");
} else {
// Easier case where input value contains no line breaks
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length;
}
return pos;
}
}
return 0;
}
var el = document.getElementById("your_textarea");
var startPos = getSelectionBoundary(el, true);
var endPos = getSelectionBoundary(el, false);
alert(startPos + ", " + endPos);
UPDATE
Based on bobince's suggested approach in the comments, I've created the following, which seems to work well. Some notes:
bobince's approach is simpler and shorter.
My approach is intrusive: it makes changes to the input's value before reverting those changes, although there is no visible effect of this.
My approach has the advantage of keeping all operations within the input. bobince's approach relies on creating ranges that span from the start of the body to the current selection.
A consequence of 3. is that the performance of bobince's varies with the position of the input within the document whereas mine does not. My simple tests suggest that when the input is close to the start of the document, bobince's approach is significantly faster. When the input is after a significant chunk of HTML, my approach is faster.
function getSelection(el) {
var start = 0, end = 0, normalizedValue, textInputRange, elStart;
var range = document.selection.createRange();
var bigNum = -1e8;
if (range && range.parentElement() == el) {
normalizedValue = el.value.replace(/\r\n/g, "\n");
start = -range.moveStart("character", bigNum);
end = -range.moveEnd("character", bigNum);
textInputRange = el.createTextRange();
range.moveToBookmark(textInputRange.getBookmark());
elStart = range.moveStart("character", bigNum);
// Adjust the position to be relative to the start of the input
start += elStart;
end += elStart;
// Correct for line breaks so that offsets are relative to the
// actual value of the input
start += normalizedValue.slice(0, start).split("\n").length - 1;
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
return {
start: start,
end: end
};
}
var el = document.getElementById("your_textarea");
var sel = getSelection(el);
alert(sel.start + ", " + sel.end);

Categories