This question already has answers here:
How do I get the (x, y) pixel coordinates of the caret in text boxes?
(2 answers)
Closed 8 years ago.
I was wondering if it is possible to find the exact location of the caret inside a textarea in pixels in relation to the entire page. For instance, if I have typed This is text into my text box, I would like to know the pixels from the top left of the screen to the caret.
It would be in the form X: 200 Y: 100. This is so I can position a floating div. This needs to be done dynamically in javascript.
Thanks guys
This "raw code" works at least in IE.
Using the code you can put your <TEXTAREA> where ever you want in page, and the <DIV id="db"> will follow it. Even despite of the scrolling position of the page. You can fix the position of <DIV> by changing the literal numbers at d.style...6-statements.
<body>
<div id="db" style="position:absolute;left:-20px;top:-20px;border:1px solid red;">V</div>
<br><br><br>
<form id="props">
<textarea id="ta" style="width:200px;height:100px;" onkeyup="moveDiv();"></textarea>
</form>
<script>
<!--
var b=document.body;
var d=document.getElementById('db');
var a=document.getElementById('ta');
function moveDiv(){
var sel=document.selection;
var targ=sel.createRange();
d.style.top=a.offsetTop+a.clientTop-d.clientHeight-6;
d.style.left=targ.offsetLeft+b.scrollLeft-6;
return;
}
// -->
</script>
</body>
Positioning of the <DIV> is not quite exact, but gets better when using fixed-width font in <TEXTAREA>.
Here is a simple mix of ideas from Caret position in pixels in an input type text (not a textarea) , Caret position in textarea, in characters from the start and document.getElementById vs jQuery $() to get the caret position in jQuery for an <input> element. It works when clicking inside it, but not when moving the caret using the arrows or typing.
<input id="someid">
<span id="faux" style="display:none"></span><br/>
<script>
var inputter = $('#someid')[0];
var faux = $('#faux');
function getCaret(el) {
if (el.selectionStart) {
return el.selectionStart;
} else if (document.selection) {
el.focus();
var r = document.selection.createRange();
if (r == null) {
return 0;
}
var re = el.createTextRange(),
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint('EndToStart', re);
return rc.text.length;
}
return 0;
}
$("#someid").click( function( event ) {
caretpos = getCaret(inputter);
calcfaux = faux.text($(this).val().substring(0, caretpos));
fauxpos = calcfaux.outerWidth();
$("#getpx").text(fauxpos + " px");
});
</script>
Instead of var inputter = document.getElementById('someid') we use var inputter = $('#someid')[0];
Here is a FIDDLE.
Related
When you drag a file from your OS filesystem over a textarea or text input, a cursor appears near the mouse pointer (this is different from positionStart), showing the user where the dragged content would be inserted.
UPDATE: here is an image, I'm dragging a file (test.sh) over the text input. You can see the drop cursor if the middle of the "text" word. The selection cursor is at the end of the string (not visible on this picture).
(Chrome's default behavior is to open the dropped file, but I'm overriding this behavior in the drop event. I want to insert the name of the file in the textarea.)
I'm trying to get this position (in terms of index in the textarea value string) when drop occurs. Any idea?
Phew, what you want to do isn't easy, since there is no way to reference this specific caret!
Off the top of my head, you could only implement this through heavy workarounds: What you can obtain upon drop occuring is the mouse cursor position.
You would have to make an invisible div-clone identical to the textarea in shape, text-size, margins, etc that automatically gets filled with the text from your textarea.
Next you'd have to create a span for each possible caret position (i.e. 1 span for every character of text) inside this div and get each span's x and y coordinates and save them into an array.
.txtarea {
width: 300px;
height: 150px;
font-size: 12px;
font-family: Arial;
padding: 5px;
text-align: left;
border: 1px solid red;
}
<textarea class="txtarea">Mytext</textarea>
<!--just an example, auto-fill this content through a JS oninput event -->
<div class="txtarea"><span>M</span><span>y</span><span>t</span><span>e</span><span>x</span><span>t</span></div>
Then, upon drop occuring, get the mouse coordinates of the event, compare them to the array of coordinates, approximate the closest x/y position and set a new selectionStart index in your textarea based on that, insert the file name, and then restore the previous selectionStart.
This is only a partial answer: The following works in IE11, but not in Chrome (didn't test in other browsers).
Just select some letters from the upper input and drag them into the bottom one:
let to = document.getElementById("to");
to.addEventListener("dragover", function (e) {
if (e.target.selectionStart) {
console.log(e.target.selectionStart);
}
});
<input type="text" id="from" value="select some letters from this sentence, and drag them into the other input element"/ style="width: 500px;"><br/>
<input type="text" id="to" value="drag over here"/>
Few notes for future research:
In Chrome, dragover event is fired only after focus is first set inside the input element.
In Chrome, the value of selectionStart is the value of the last selected text position within the target input (put differently: the last position of the cursor within the target input).
Setting type="number" fires with selectionStart assigned to null in Chrome, but not in IE11.
From Your picture, it looks like an input text (I admit, I was too lazy to code this for a textarea...) but - anyway - this is a nice question.
To get the text width, You need to clone the original element and loop over the text char by char. Just to mention an example, see some answers here on SO about the topic: "how to set the ellipsis text using JavaScript".
I also noticed that by moving the mouse pointer over the text, the caret is shifting left and right from the center of each char. At the end, the main problem here is: how to find the x of the middle of each char.
So, my code is splitted in two parts: text input clone and character position. After that, to position the caret, You can use an existing library like this: jQuery Caret Plugin or jQuery Caret - up to You - I would skip this part of the question.
I tested the code below in FF, Safari, IE & Chrome, there are obviously some small and annoying issues in the drag & drop behavior of these browser, but they seems to me irrelevant.
function getCaretPos(target, x) {
var txt = target.value, doc = target.ownerDocument,
clone = doc.createElement('span'), /* Make a clone */
style = doc.defaultView.getComputedStyle(target, null),
mL = style.getPropertyValue('margin-left'),
pL = style.getPropertyValue('padding-left'),
mouseX = x - parseFloat(mL) - parseFloat(pL);
clone.style.fontFamily = style.getPropertyValue('font-family');
clone.style.fontSize = style.getPropertyValue('font-size');
clone.style.fontWeight = style.getPropertyValue('font-weight');
clone.style.position = 'absolute'; /* Keep layout */
clone.style.left = -9999;
target.parentNode.appendChild(clone);
clone.style.width = 'auto';
clone.style.whiteSpace = 'pre'; /* Keep whitespaces */
clone.style.marginLeft = 0;
clone.style.paddingLeft = 0;
clone.innerText = txt; /* Start from full length */
var i = txt.length, pos = -1, xL = 0, xC = 0, xR = clone.clientWidth;
while (i--) { /* Find the caret pos */
clone.innerText = txt.slice(0, i);
xL = clone.clientWidth;
xC = (0.5 * (xR + xL)) | 0; /* We need the center */
if (xC < mouseX) { pos = i; break }
xR = xL;
}
pos++; /* Restore the correct index */
target.parentNode.removeChild(clone); /* Clean up */
clone = null;
return pos;
}
function onFileDragOver(e) {
if(!window.chrome) e.preventDefault(); /* Show the caret in Chromium */
}
function onFileDrop(e) {
e.preventDefault();
var target = e.currentTarget, txt = target.value,
pos = getCaretPos(target, e.offsetX),
tok1 = txt.substr(0, pos), tok2 = txt.substr(pos);
/* Get the drop-action result */
var result = '', files = e.dataTransfer.files,
data = e.dataTransfer.getData('text');
for (var i = 0, f; (f = files[i]); i++) { result += f.name }
target.value = tok1 + result + tok2;
target.focus();
/* Up to You how to position the caret */
if(target.setSelectionRange) target.setSelectionRange(pos, pos);
//$(target).caret(pos);
}
$(document).ready(function () {
var target = document.getElementById('search-input');
target.addEventListener('dragover', onFileDragOver);
target.addEventListener('drop', onFileDrop);
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="stylesheet" href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.2/jquery.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.js"></script>
</head>
<body>
<div data-role="page">
<div data-role="header"><h1>Caret Position</h1></div>
<div data-role="content">
<div class="ui-field-contain">
<label for="search-input">Products</label>
<input type="search" name="search-input" id="search-input" value="target text box"
spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="none"/>
</div>
</div>
</div>
</body>
</html>
Try jQuery. What you have to do is get the text box with a query selector, then bind that to mousemove and get $(this).caret().start.
Example:
let cursorPosition = 0;
$("#textinput").bind("mousemove", function() {
cursorPosition = $(this).caret().start;
});
// Do what you want with cursorPosition when file is dropped
And I think that's all you need to do here.
As long as you're willing to use jQuery you should be good.
I want to know how to change the background color or may be color of the text that was modified in a textarea.
Like suppose, consider a textarea with a pre-defined value as "Hello World".
Now if you try to change the text inside the textarea to "Hello Universe", it should show Universe highlighted (may be background color change, or color change or make it bold, anything.
I just want to get the modified text to be highlighted so it is visible what was changed.
Highlighting is possible if you make the textarea partially transparent and then had a div behind it where you can clone the content and put span tags around the changed values. The hard part is in figuring out how to diff the string. For an example of highlight certain parts of text "in the text area" see this fiddle:
https://jsfiddle.net/mcgraphix/tn0ahcfx/
<div class="container">
<div id="highlighter"></div>
<textarea id="editor" onkeyup="updateHighlight()"
value="This is some text in the editor"></textarea>
</div>
JS
function updateHighlight() {
//calculate index of changes
//assume chars 1-10 are different //see my comment for how to calculate what to highlight
var content = document.getElementById('editor').value;
var highlighted = '';
var i = 0;
while (i < content.length) {
if (i === 1) {
highlighted += '<span class="highlighted">';
} else if (i > 10) {
highlighted += '</span>'
}
highlighted += content.charAt(i);
i++;
}
document.getElementById('highlighter').innerHTML = highlighted;
}
Basically, as you type the text in the text area is parsed and as text is identified as being in need of highlight, a span tag is wrapped around it. After parsing the text, the copy with the spans is put inside the div that is behind the textarea. With the right css you can hide the text in that div and just put a background color such that it looks highlighted. The fiddle gives you the basic idea but you would have to account for the user resizing the text area as you need to make sure the text area and the "highlighter" behind it are aligned.
The hard part is figuring out what to highlight. such that you don't highlight every character after the first change. Take a look at Levenshtein distance algorithm for determining which characters you need to highlight when comparing two strings.
Keep old value in variable.
Split the value using delimiter as space
Check indexOf new value after spitting by space
Use Array#indexOf to listen the change in value!
Most important point, you can not apply style over characters in textarea. Example given below has a demonstration in div element considering value from textarea
var input = $('textarea');
var div = $('div');
var oldVal = input.val();
var oldArr = oldVal.split(' ');
input.on('input', function() {
var newVal = this.value;
var html = [];
newVal.split(' ').forEach(function(el) {
if (oldArr.indexOf(el) === -1) {
html.push('<span style="color:green">' + el + '</span>');
} else {
html.push('<span>' + el + '</span>');
}
});
div.html(html.join(' '));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<textarea>Hello World</textarea>
<div></div>
This question already has an answer here:
ContentEditable div - all HTML after the first 'x' characters
(1 answer)
Closed 6 years ago.
Using a contenteditable div, I'd like to highlight words that meet a certain criteria. One of which is if the content the user has entered has reached the allowed maximum length of that area.
I've kind of got something working - but once it reaches the threshold, the cursor position resets back to the beginning (and the user is effectively typing in reverse!).
I need this to work in IE8+.
Is there some way to get the position and set it back where it was before I replaced the contents of the area, or is my line of thinking all wrong?
Thanks
JS Fiddle:
http://jsfiddle.net/vfgLjf0c/
$(document).on("keypress", "div#editableContent", function(e) {
var element = $(this);
// KEYUP
if (e.type == "keypress") {
var curLen = element.text().length;
var maxLen = element.attr("data-maxlength");
if (curLen > maxLen) {
var overHang = element.text().substr(maxLen);
overHang = "<em style='background: #fcc;'>" + overHang + "</em>";
var newString = element.text().substr(0, maxLen) + overHang;
$(this).html(newString);
}
}
});
UPDATE Answered my own question - see below.
EDIT This issue has been resolved here by user: https://stackoverflow.com/users/2159246/alan0xd7
Solution: https://stackoverflow.com/a/31498234/1266457
The idea is this -
There is a contenteditable element with some text in it. Am trying to build out a tagging mechanism (kind of like twitter's people tagging when you type '#'). Whenever a user types '#', it shows up a popover with suggestions and filters when they continue typing. Until here it's easy and I have got it figured out. The problem comes when I need to show the popover if/only if the caret is over the element containing the tag.
<div contenteditable="">
<p>Some random text before
<a href="javascript:;"
class="name-suggest"
style="color:inherit !important;text-decoration:inherit !important">#samadams</a>
Some random text after</p>
</div>
Now, whenever the user moves the caret over the a tag / clicks on it, I want to trigger an event that shows the popover, and remove it whenever the caret leaves the a tag. (kind of like focus / blur but they don't seem to work). onmousedown works but there is no way to tell if the cursor has been moved into the anchor tag with the keyboard.
Also, am doing this in angularjs, so, any solution targeted towards that would be preferable but not necessary.
Have been trying to get this to work for a day and any help is greatly appreciated.
This will let you know when your caret position is in an anchor node containing an #
$('#content').on('mouseup keydown keyup', function (event) {
var sel = getSelection();
if (sel.type === "Caret") {
var anchorNodeVal = sel.anchorNode.nodeValue;
if ( anchorNodeVal.indexOf('#') >= 0) {
$('#pop').show()
} else {
$('#pop').hide()
}
}
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="content" contenteditable="">
<p>Some random text before
<a href="javascript:;"
class="name-suggest"
style="color:inherit !important;text-decoration:inherit !important">#samadams</a>
Some random text after</p>
</div>
<div id="pop" style="display:none">Twitter node found</div>
You could add some regex to further validate the selection.
There is a weird move with RegExps and offset calculation in the code below, but let me explain why it's a better solution.
I've been building a complicated editor using contenteditable about a year ago. It wasn't just a disaster. It was a fucking disaster. There is no cover-all-the-cases spec. Browsers behave differently in every possible detail and it changes frequently. Put a caret before # char and you will get this is Gecko:
<a href="#">|#name
And this in WebKit:
|<a href="#">#name
Well, unless <a> is paragraph's first child. Then result would be the same as in Gecko. Try to put caret after the nickname and both will tell it's inside the link. Start typing, and caret will pop out the element - a year ago Gecko wasn't doing it.
I've used native Selection & Range APIs in this example, they are IE9+. You may want to use Rangy instead.
$el = $('#content');
var showTip = function (nickname) {
// ...
console.log('Show: ' + nickname);
};
var dismissTip = function () {
// ...
console.log('Hide');
};
// I'm sure there is a better RegExp for this :)
var nicknameRegexp = /(^|\b|\s)\#(\w+)(\s|\b|$)/g;
var trackSelection = function () {
var selection = window.getSelection(),
range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
if (range == null || $el[0].contains(range.commonAncestorContainer) == false) {
return dismissTip();
}
var comparer = range.cloneRange();
comparer.setStart($el[0], 0);
var offset = comparer.toString().length;
var match, from, to;
while (match = nicknameRegexp.exec($el[0].textContent)) {
from = match.index + match[1].length;
to = match.index + match[1].length + match[2].length + 1;
if (offset >= from && offset <= to) {
// Force rewind, otherwise next time result might be incorrect
nicknameRegexp.lastIndex = 0;
return showTip(match[2]);
}
}
return dismissTip();
};
$el.on({
// `mousedown` can happen outside #content
'mousedown': function (e) {
$(document).one('mouseup', function (e) {
// Calling function without a tiny delay will lead to a wrong selection info
setTimeout(trackSelection, 5);
});
},
'keyup': trackSelection
});
Just looked at Fire event when caret enters span element which led me here, pretending your case was quite similar except finding if current word is specifically beginning with # for the modal to show...
The thing you need is a way to get the word we're on at the moment we move or type, then check the first character and hide/show the modal pane accordingly will be pretty easy.
function getSelectedWord(grab=document.getSelection()) {
var i = grab.focusOffset, node = grab.focusNode, // find cursor
text = node.data || node.innerText, // get focus-node text
a = text.substr(0, i), p = text.substr(i); // split on caret
return a.split(/\s/).pop() + p.split(/\s/)[0]} // cut-out at spaces
Now you can listen for keydown or selectionchange events and show your pane knowning what have already been written of the current/selected word.
editor.addEventListener('keydown', ev => {
if (ev.key.substr(0, 5) != 'Arrow') // react when we move caret or
if (ev.key != '#') return; // react when we type an '#' or quit
var word = getSelectedWord(); // <-- checking value
if (word[0] == '#') showModal(word.substr(1)); // pass without '#'
});
Note that social networks and code completion usually stops at caret position while I did check for word tail... You can go usual by removing p off of getSelectedWord function definition if desired.
Hope this still helps; Happy coding ! ;)
I have read a few posts on positioning the caret, but none seem to answer my particular issue.
I have 2 divs (div1 and div2)
div1 = noneditable div
div2 = contenteditable div
both divs contain exact same contents
when user clicks on div1, it gets hidden, and div2 appears in exact location and user can edit
The problem: I want the caret to appear in exact location on div2 as div1
So, I need some way to READ the location where the user clicks on div1, and then when div2 appears place the cursor/caret in that same location, so a getCaretLocation(in_div_id) and setCaretLocation(in_div_id) set of functions.
Any way to do that?
Thanks -
Short answer : You can't
Long answer : The problem you'll face is that you'll be able to get (x,y) coordinates for the click event on div1, but any implementation of the caret position while require you knowing the position of the caret in the content (which is the number of characters preceding the caret).
To convert the (x,y) coordinates to a character position you actually need to know how many characters were before (ie. left on the current line and above, if the text is ltr).
If you use a fixed width font, you can simplify the problem : mapping an (x,y) coordinate to a (line, column) coordinate on a character grid.
However, you still face the problem of not knowing how the text is wrapped. For example :
------------------
|Lorem ipsum |
|dolor sit amet |
|consectetur |
|adipiscing elit |
------------------
If the user clicks on the d in dolor, you know that the character is the 1st on the 2nd line, but without knowing the wrapping algorithm there is no way you'll know that it is the 13th character in "Lorem ipsum dolor sit…". And there is no guarantee that such a wrapping algorithm is identical across browsers and platform.
Now, what I'm wondering is why would you use 2 different synced div in the first place ? Wouldn't it be easier to use only one div and set its content to editable when the user clicks (or hovers) ?
You could insert a tiny span-element at the caret, get its position, and remove it. For a cross-browser range and selection library, see rangy.
you can, basically you need set temporary content editable on your first div to catch caret pos
$('div1').hover(function()
{ $(this).attr('contenteditable','true');
},function()
{ $(this).removeAttr('contenteditable');
}).mouseup(function()
{ var t = $(this);
// get caret position and remove content editable
var caret = t.getCaret();
t.removeAttr('contenteditable');
// do your div switch stuff
...
// and apply saved caret position
$('div2').setCaret(caret);
});
now just need get/set caret method :)
edit > here is my own, (live demo)
getSelection:function($e)
{ if(undefined === window.getSelection) return false;
var range = window.getSelection().getRangeAt(0);
function getTreeOffset($root,$node)
{ if($node.parents($root).length === 0) return false; // is node child of root ?
var tree = [], treesize = 0;
while(1)
{ if($node.is($root)) break;
var index, $parent = $node.parent();
index = $parent.contents().index($node);
if(index !== -1) { tree[treesize++] = index; } $node = $parent;
}; return tree.reverse();
}
var start = getTreeOffset($e,$(range.startContainer));
var end = getTreeOffset($e,$(range.endContainer));
if(start & end === false) return false;
return {start:start,end:end,startOffset:range.startOffset,endOffset:range.endOffset};
}, setSelection:function($e,s,win)
{ $e.focus(); if(s === false) return; var sel = win.getSelection(); sel.removeAllRanges();
function getNode($e,s)
{ var node = $e;
for( var n=0;n<s.length;n++ )
{ var index = s[n]; if(index < 0) break;
node = node.contents(':eq('+index+')');
} return node.get(0);
}
var start = getNode($e,s.start), end = getNode($e,s.end), range = win.document.createRange();
range.setStart(start,s.startOffset); range.setEnd(end,s.endOffset); sel.addRange(range);
}
It sounds like you are trying to do an inline edit... have you looked at the jeditable plugin?
When you click on an element, a Selection object with zero length is created (get it from element.getSelection() , where element is the div in question). The focusOffset of that object will let you know that you clicked on, for example, the 74th character in that div (this is the thing that Adrien said was impossible in a different answer).