Set textarea's caret position using x and y position - javascript

I want to implement inline edition, so if user clicks on a div with some text, the textarea will appear at the same position as clicked div's position and will get the text from the div. This is fine for me, but what I want to do next is to set textarea's caret position according to x and y position from div click event. Any ideas?
HTML:
<div id="content" style="display: block; z-index: 10">Some text</div>
<textarea id="editor" style="position: absolute; display: none; z-index: 11"></textarea>
JS:
$('#content').click(function(e) {
var content = $(this);
var editor = $('#editor');
var position = content.offset();
editor.css('left', position.left);
editor.css('top', position.top);
editor.val(content.text());
editor.show();
var mousePosition = { x: e.offsetX, y: e.offsetY };
// here I want to set the #editor caret position at the same place,
// where user clicks the #content (mousePosition variable)
});

Looks like you could do something like this:
createTextArea = function (e) {
var range = window.getSelection().getRangeAt(0),
start = range.startOffset,
target = e.target,
setPoint;
while (target.tagName.toLowerCase() !== 'div') {
target = target.parentElement;
if (!target) return;
}
range.setStart(target, 0);
setPoint = range.toString().length;
// place and show #editor
editor.focus();
editor.setSelectionRange(setPoint, setPoint);
return;
};
An example at jsFiddle. Notice that this works only in modern browsers. Older IE's don't have Input API, and the Selection/Range model is totally different.

I've found a solution:
JS:
function getCaretPosition(editableDiv) {
var caretPos = 0, containerEl = null, sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode == editableDiv) {
caretPos = range.endOffset;
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() == editableDiv) {
var tempEl = document.createElement("span");
editableDiv.insertBefore(tempEl, editableDiv.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint("EndToEnd", range);
caretPos = tempRange.text.length;
}
}
return caretPos;
}
$.fn.selectRange = function (start, end) {
if (!end) end = start;
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();
}
});
};
Usage:
$('#content').click(function (e) {
var content = $(this);
var editor = $('#editor');
var caret = getCaretPosition(this);
var position = content.offset();
editor.css('left', position.left);
editor.css('top', position.top);
editor.val(content.text());
editor.show();
var mousePosition = { x: e.offsetX, y: e.offsetY };
// here I want to set the #editor caret position at the same place,
// where user clicks the #content (mousePosition variable)
editor.selectRange(caret, caret);
});

Related

Insert text at current cursor position on dropdown list changed inside iframe

I am using a text editor provided by Microsoft ajax-toolkit.
It renders iframe on browser. I have added a dropdown in that editor and I want that when user changes the drop-down index the value should be added in the editor current cursor position.
I got a code on SO which gives me the current selected text inside editor is as follows
function getIframeSelectionText(iframe) {
var win = iframe.contentWindow;
var doc = iframe.contentDocument || win.document;
if (win.getSelection) {
return win.getSelection().toString();
} else if (doc.selection && doc.selection.createRange) {
return doc.selection.createRange().text;
}
}
But I want to add some text at the current position. The html is rendering as below
<td class="ajax__htmleditor_editor_editpanel"><div id="Editor1_ctl02" style="height:100%;width:100%;">
<iframe id="Editor1_ctl02_ctl00" name="Editor1_ctl02_ctl00" marginheight="0" marginwidth="0" frameborder="0" style="height:100%;width:100%;display:none;border-width:0px;">
</iframe><textarea id="Editor1_ctl02_ctl01" class="ajax__htmleditor_htmlpanel_default" style="height:100%;width:100%;display:none;"></textarea><iframe id="Editor1_ctl02_ctl02" name="Editor1_ctl02_ctl02" marginheight="0" marginwidth="0" frameborder="0" style="height:100%;width:100%;display:none;border-width:0px;">
</iframe>
</div></td>
I am trying as follow
$("#imgDropdown").change(function () {
//var iframeBody = $(window.Editor1_ctl02_ctl00.document.getElementsByTagName("body")[0]);
var iframe = document.getElementById("Editor1_ctl02_ctl00");
$("#Editor1_ctl02_ctl00").find("body").insertAtCaret("value");
//alert(getIframeSelectionText(iframe));
});
the function for inserting text is not working with iframe is as follow
$.fn.extend({
insertAtCaret: function (myValue) {
if (document.selection) {
this.focus();
sel = document.selection.createRange();
sel.text = myValue;
this.focus();
}
else if (this.selectionStart || this.selectionStart == '0') {
var startPos = this.selectionStart;
var endPos = this.selectionEnd;
var scrollTop = this.scrollTop;
this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos, this.value.length);
this.focus();
this.selectionStart = startPos + myValue.length;
this.selectionEnd = startPos + myValue.length;
this.scrollTop = scrollTop;
} else {
this.value += myValue;
this.focus();
}
}
})
Easy, you just have to use.
$("#Editor1_ctl02_ctl00").contents().find('textarea').insertAtCaret('value');
Updated
Sorry, I thought the insertAtCaret function is working for you, you just needed to work inside iFrame. You can use this version of insertAtCaret:
jQuery.fn.extend({
insertAtCaret: function (html) {
var winObject = function (el){
var doc = el.ownerDocument;
return doc.defaultView || doc.parentWindow
};
return this.each(function (i) {
var sel, range, w = this;
w = winObject(w);
if (w.getSelection) {
// IE9 and non-IE
sel = w.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
// Range.createContextualFragment() would be useful here but is
// only relatively recently standardized and is not supported in
// some browsers (IE9, for one)
var el = w.document.createElement("div");
el.innerHTML = html;
var frag = w.document.createDocumentFragment(), node, lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if (w.document.selection && w.document.selection.type != "Control") {
// IE < 9
w.document.selection.createRange().pasteHTML(html);
}
}
)
}
});
and call it like:
$("#Editor1_ctl02_ctl00").contents().find('body').insertAtCaret($val);
Function adapted from here
Happy coding!
There seem to be a few issues here.
The Microsoft ajax-toolkit editor creates an iframe where the designMode property is turned on, and that's why it's editable, it has no value, and textNodes are added straight to the body, which makes it a little more difficult.
When you're selecting something from a dropdown, the focus is on the dropdown, and there is no caret position, as the focus is shifted away from the iFrame.
I'm assuming that the dropdown is in the top menubar for the editor or anywhere else that is outside the iFrame.
Also, the Microsoft ajax-toolkit editor has a recommended update, the HTMLEditorExtender.
The code you have to capture the caret position seems to be for a regular input / textarea, and you'd have to adapt that code to work with any Node inside an iframe that is in designMode, with it's own window and document etc.
Given the above considerations, this is what I came up with to do this
var frameID = 'Editor1_ctl02_ctl00',
selectID = 'imgDropdown',
iframe = document.getElementById(frameID),
iWin = iframe.contentWindow ? iframe.contentWindow : window.frames[frameID];
$(iWin).on('blur', function() {
$(iframe).data('range', getRange(iWin));
});
$('#' + selectID).on('change', function() {
var range = $(iframe).data('range');
addText(iWin, range, this.value);
});
function getRange(win) {
var sel, range, html;
if (win.getSelection) {
sel = win.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
}
} else if (win.document.selection && win.document.selection.createRange) {
range = win.document.selection.createRange();
}
return range;
}
function addText(win, range, text) {
if (win.getSelection) {
range.insertNode(win.document.createTextNode(text));
} else if (win.document.selection && win.document.selection.createRange) {
range.text = text;
}
}
FIDDLE

How blur (focus lost) the image after insertion with execCommand in IE10?

I have a content editable DIV such as:
<div contenteditable='true' id='myRichTextBox'></div>
In addition, I have a button that inserts an image into the mentioned DIV. Once the image inserted it has focused with resizable handlers.
How I can lose it's focus and bring back the focus to the content editable DIV ?
<button type='button' onclick='insertImage()'>Insert an image</button>
Javascript code:
function insertImage()
{
document.execCommand('insertImage',false,'myImage.png');
}
Thanks for any help
You can work round this in several ways, but a simple one would be to use document.execCommand("InsertHTML") in most browsers, falling back to pasteHTML() in IE. However, this will not work in IE 11 because it does not support document.selection or the InsertHTML command.
function insertImageWithInsertHtml(imgSrc) {
var imgHtml = "<img src='" + imgSrc + "'>";
var sel;
if (document.queryCommandSupported("InsertHTML")) {
document.execCommand("InsertHTML", false, imgHtml);
} else if ( (sel = document.selection) && sel.type != "Control") {
var range = sel.createRange();
range.pasteHTML(imgHtml);
range.collapse(false);
range.select();
}
}
Another way which would work in all browsers except IE <= 8 (for which you can use the same fallback as above) would be to insert the image manually using the insertNode() method of a Range obtained from the selection. This is the most future-proof and standards-compliant method so is what I'd recommend:
function insertImageWithInsertNode(imgSrc) {
var sel;
if (window.getSelection) {
var sel = window.getSelection();
if (sel.rangeCount > 0) {
var range = sel.getRangeAt(0).cloneRange();
range.deleteContents();
var img = document.createElement("img");
img.src = imgSrc;
range.insertNode(img);
// Place the caret immediately after the image
range.setStartAfter(img);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
} else if ( (sel = document.selection) && sel.type != "Control") {
var range = sel.createRange();
range.pasteHTML("<img src='" + imgSrc + "'>");
range.collapse(false);
range.select();
}
}
Finally, here's a live demo showing all three techniques:
http://jsfiddle.net/timdown/9ScLA/3/
I used the following solution and worked!
function insertImage(imgSrc)
{
var ua = navigator.userAgent.toLowerCase();
var tr;
if (ua.indexOf("msie") > 0)
tr = document.selection.createRange();
document.execCommand("insertImage", false, imgSrc);
if(ua.indexOf("firefox") > 0)
document.execCommand("enableObjectResizing", false, false);
if (ua.indexOf("msie") > 0)
{
tr.collapse(false);
tr.select();
}
this.textbox.focus();
}

JavaScript: Scroll to selection after using textarea.setSelectionRange in Chrome

A JavaScript function selects a certain word in a textarea using .setSelectionRange().
In Firefox, the textarea automatically scrolls down to show the selected text. In Chrome (v14), it does not. Is there a way to get Chrome to scroll the textarea down to the newly selected text?
jQuery solutions are welcome.
Here is a simple and efficient solution in pure JavaScript:
// Get the textarea
var textArea = document.getElementById('myTextArea');
// Define your selection
var selectionStart = 50;
var selectionEnd = 60;
textArea.setSelectionRange(selectionStart, selectionEnd);
// Mow let’s do some math.
// We need the number of characters in a row
var charsPerRow = textArea.cols;
// We need to know at which row our selection starts
var selectionRow = (selectionStart - (selectionStart % charsPerRow)) / charsPerRow;
// We need to scroll to this row but scrolls are in pixels,
// so we need to know a row's height, in pixels
var lineHeight = textArea.clientHeight / textArea.rows;
// Scroll!!
textArea.scrollTop = lineHeight * selectionRow;
Put this in a function, extend the prototype of JavaScript's Element object with it, and you're good.
A lot of answers, but the accepted one doesn't consider line breaks, Matthew Flaschen didn't add the solution code, and naXa answer has a mistake. The simplest solution code is:
textArea.focus();
const fullText = textArea.value;
textArea.value = fullText.substring(0, selectionEnd);
textArea.scrollTop = textArea.scrollHeight;
textArea.value = fullText;
textArea.setSelectionRange(selectionStart, selectionEnd);
You can see how we solved the problem in ProveIt (see highlightLengthAtIndex). Basically, the trick is to truncate the textarea, scroll to the end, then restore the second part of the text. We also used the textSelection plugin for consistent cross-browser behavior.
Valeriy Katkov's elegant solution works great but has two problems:
It does not work for long strings
Selected contents are scrolled to the bottom of the textarea, making it hard to see the context which surrounds the selection
Here's my improved version that works for long strings (tested with at least 50,000 words) and scroll selection to the center of the textarea:
function setSelectionRange(textarea, selectionStart, selectionEnd) {
// First scroll selection region to view
const fullText = textarea.value;
textarea.value = fullText.substring(0, selectionEnd);
// For some unknown reason, you must store the scollHeight to a variable
// before setting the textarea value. Otherwise it won't work for long strings
const scrollHeight = textarea.scrollHeight
textarea.value = fullText;
let scrollTop = scrollHeight;
const textareaHeight = textarea.clientHeight;
if (scrollTop > textareaHeight){
// scroll selection to center of textarea
scrollTop -= textareaHeight / 2;
} else{
scrollTop = 0;
}
textarea.scrollTop = scrollTop;
// Continue to set selection range
textarea.setSelectionRange(selectionStart, selectionEnd);
}
It works in Chrome 72, Firefox 65, Opera 58, and Edge 42.
For an example of using this function, see my GitHub project SmartTextarea.
This is a code inspired by the Matthew Flaschen's answer.
/**
* Scroll textarea to position.
*
* #param {HTMLInputElement} textarea
* #param {Number} position
*/
function scrollTo(textarea, position) {
if (!textarea) { return; }
if (position < 0) { return; }
var body = textarea.value;
if (body) {
textarea.value = body.substring(0, position);
textarea.scrollTop = position;
textarea.value = body;
}
}
Basically, the trick is to truncate the textarea, scroll to the end, then restore the second part of the text.
Use it as follows
var textarea, start, end;
/* ... */
scrollTo(textarea, end);
textarea.focus();
textarea.setSelectionRange(start, end);
Based on the idea from naXa and Valeriy Katkov, I refined the function with fewer bugs. It should work out of the box (It's written with TypeScript. For JavaScript, just remove the type declaration):
function scrollTo(textarea: HTMLTextAreaElement, offset: number) {
const txt = textarea.value;
if (offset >= txt.length || offset < 0)
return;
// Important, so that scrollHeight will be adjusted
textarea.scrollTop = 0;
textarea.value = txt.substring(0, offset);
const height = textarea.scrollHeight;
textarea.value = txt;
// Margin between selection and top of viewport
textarea.scrollTop = height - 40;
}
Usage:
let textarea, start, end;
/* ... */
scrollTo(textarea, start);
textarea.focus();
textarea.setSelectionRange(start, end);
Complete code for Chrome:
<script type="text/javascript">
var SAR = {};
SAR.find = function () {
debugger;
var parola_cercata = $("#text_box_1").val(); // The searched word
// Make text lowercase if search is
// supposed to be case insensitive
var txt = $('#remarks').val().toLowerCase();
parola_cercata = parola_cercata.toLowerCase();
// Take the position of the word in the text
var posi = jQuery('#remarks').getCursorPosEnd();
var termPos = txt.indexOf(parola_cercata, posi);
if (termPos !== -1) {
debugger;
var target = document.getElementById("remarks");
var parola_cercata2 = $("#text_box_1").val();
// Select the textarea and the word
if (target.setSelectionRange) {
if ('selectionStart' in target) {
target.selectionStart = termPos;
target.selectionEnd = termPos;
this.selectionStart = this.selectionEnd = target.value.indexOf(parola_cercata2);
target.blur();
target.focus();
target.setSelectionRange(termPos, termPos + parola_cercata.length);
}
} else {
var r = target.createTextRange();
r.collapse(true);
r.moveEnd('character', termPos + parola_cercata);
r.moveStart('character', termPos);
r.select();
}
} else {
// Not found from cursor pos, so start from beginning
termPos = txt.indexOf(parola_cercata);
if (termPos !== -1) {
var target = document.getElementById("remarks");
var parola_cercata2 = $("#text_box_1").val();
// Select the textarea and the word
if (target.setSelectionRange) {
if ('selectionStart' in target) {
target.selectionStart = termPos;
target.selectionEnd = termPos;
this.selectionStart = this.selectionEnd = target.value.indexOf(parola_cercata2);
target.blur();
target.focus();
target.setSelectionRange(termPos, termPos + parola_cercata.length);
}
} else {
var r = target.createTextRange();
r.collapse(true);
r.moveEnd('character', termPos + parola_cercata);
r.moveStart('character', termPos);
r.select();
}
} else {
alert("not found");
}
}
};
$.fn.getCursorPosEnd = function () {
var pos = 0;
var input = this.get(0);
// IE support
if (document.selection) {
input.focus();
var sel = document.selection.createRange();
pos = sel.text.length;
}
// Firefox support
else if (input.selectionStart || input.selectionStart === '0')
pos = input.selectionEnd;
return pos;
};
</script>
I published an answer here:
http://blog.blupixelit.eu/scroll-textarea-to-selected-word-using-javascript-jquery/
It works perfectly with just one needed rule: Set a line-height in the CSS content of the textarea!
It calculate the position of the word to scroll to just by doing some simple mathematical calculation and it worked perfectly in all my experiments!

Get caret position in HTML input?

How do I get the index of the text caret in an input?
-> selectionStart
<!doctype html>
<html>
<head>
<meta charset = "utf-8">
<script type = "text/javascript">
window.addEventListener ("load", function () {
var input = document.getElementsByTagName ("input");
input[0].addEventListener ("keydown", function () {
alert ("Caret position: " + this.selectionStart);
// You can also set the caret: this.selectionStart = 2;
});
});
</script>
<title>Test</title>
</head>
<body>
<input type = "text">
</body>
</html>
The following will get you the start and end of the selection as character indices. It works for text inputs and textareas, and is slightly complicated because of IE's strange handling of line breaks.
function getInputSelection(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 textBox = document.getElementById("textBoxId");
textBox.focus();
alert( getInputSelection(textBox).start );
There is now a nice jQuery plugin for this: Caret plugin
Then you can just call $("#myTextBox").caret();
We had used something like this for an old javascript application, but I haven't tested it in a couple years:
function getCaretPos(input) {
// Internet Explorer Caret Position (TextArea)
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
var bookmark = range.getBookmark();
var caret_pos = bookmark.charCodeAt(2) - 2;
} else {
// Firefox Caret Position (TextArea)
if (input.setSelectionRange)
var caret_pos = input.selectionStart;
}
return caret_pos;
}
Get coordinates (css: left:x , top:y) of the current caret position in order to position an element (e.g. show tooltip at caret position)
function getCaretCoordinates() {
let x = 0,
y = 0;
const isSupported = typeof window.getSelection !== "undefined";
if (isSupported) {
const selection = window.getSelection();
// Check if there is a selection (i.e. cursor in place)
if (selection.rangeCount !== 0) {
// Clone the range
const range = selection.getRangeAt(0).cloneRange();
// Collapse the range to the start, so there are not multiple chars selected
range.collapse(true);
// getCientRects returns all the positioning information we need
const rect = range.getClientRects()[0];
if (rect) {
x = rect.left; // since the caret is only 1px wide, left == right
y = rect.top; // top edge of the caret
}
}
}
return { x, y };
}
demo: https://codesandbox.io/s/caret-coordinates-index-contenteditable-9tq3o?from-embed
ref: https://javascript.plainenglish.io/how-to-find-the-caret-inside-a-contenteditable-element-955a5ad9bf81
Working example of getting cursor point in text box:
function textbox()
{
var ctl = document.getElementById('Javascript_example');
var startPos = ctl.selectionStart;
var endPos = ctl.selectionEnd;
alert(startPos + ", " + endPos);
}

How to insert text at the current caret position in a textarea

On a function call from an image, I am trying to insert the alt tag value from the image into the textarea at the position where the caret currently is.
This is the code that I currently have which inserts the alt tag value to the end of the text area.
$("#emoticons").children().children().click(function () {
var ch = $(this).attr("alt");
$("#txtPost").append(ch);
});
The 2 things I have been having a problem with is determining the position of the caret, and creating a new string with the value of the textarea before the carets positon + the code I'm inserting + the value of the textarea after the carets position.
i've currently got this extension in place:
$.fn.insertAtCaret = function(text) {
return this.each(function() {
if (document.selection && this.tagName == 'TEXTAREA') {
//IE textarea support
this.focus();
sel = document.selection.createRange();
sel.text = text;
this.focus();
} else if (this.selectionStart || this.selectionStart == '0') {
//MOZILLA/NETSCAPE support
startPos = this.selectionStart;
endPos = this.selectionEnd;
scrollTop = this.scrollTop;
this.value = this.value.substring(0, startPos) + text + this.value.substring(endPos, this.value.length);
this.focus();
this.selectionStart = startPos + text.length;
this.selectionEnd = startPos + text.length;
this.scrollTop = scrollTop;
} else {
// IE input[type=text] and other browsers
this.value += text;
this.focus();
this.value = this.value; // forces cursor to end
}
});
};
and you can use it like so:
$("#txtPost").insertAtCaret(ch);

Categories

Resources