Setting marker position inside textarea - javascript

I'm looking for a way to set the text marker to the beginning of a textarea when there's a value set or text between the textarea tags. I couldn't find anything on it when searching. So, does anyone know how to go about doing this?

var el = document.getElementById("myTextArea");
if (typeof el.setSelectionRange != "undefined") {
el.setSelectionRange(0, 0);
} else if (typeof el.createTextRange != "undefined") {
var range = el.createTextRange();
range.collapse(true);
range.select();
}

The following should be something like what you're looking for, although I haven't tested it.
var el = document.getElementById("myTextArea");
// IE
if (document.selection) {
var sel = el.createTextRange();
sel.moveStart("character", 0);
}
// Others
else if ("setSelectionRange" in el) {
el.setSelectionRange(0, 0);
}

Related

How to make range offset to work with HTML elements in a multiple line contenteditable div?

I am having a few issues with my code regarding caret positioning, content editable div and HTML tags in it.
What I am trying to achieve
I'd like to have a content editable div, which allows for line breaks and multiple HTML tags inserted by typing some sort of shortcut - double left bracket '{{' in my case.
What I have achieved so far
The div allows for a single HTML tag and only works in a single line of text.
The issues
1) When I break the line with the return key, the {{ no longer triggers the tag to show up. I assume that you have to somehow make the script to take line breaks (nodes?) into account when creating the range.
2) If you already have one HTML tag visible, you can't insert another one. Instead, you get the following error in browser's console.
Uncaught DOMException: Failed to execute 'setStart' on 'Range': The offset 56 is larger than the node's length (33).
I noticed that range offset goes to 0 (or starts with the end of HTML tag) which is probably at the culprit of the issue here.
Below is the code I have so far...
Everything is triggered on either keyup or mouseclick.
var tw_template_trigger = '{{';
var tw_template_tag = '<span class="tw-template-tag" contenteditable="false"><i class="tw-icon tw-icon-close"></i>Pick a tag</span>';
$('.tw-post-template-content').on( 'keyup mouseup', function() {
// Basically check if someone typed {{
// if yes, attempt to delete those two characters
// then paste tag HTML in that position
if( checkIfTagIsTriggered( this ) && deleteTagTrigger( this ) ) {
pasteTagAtCaret();
}
});
function pasteTagAtCaret(selectPastedContent) {
// Then add the tag
var sel, range;
if (window.getSelection) {
// IE9 and non-IE
sel = window.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 = document.createElement("div");
el.innerHTML = tw_template_tag;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
var firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if ( (sel = document.selection) && sel.type != "Control") {
// IE < 9
var originalRange = sel.createRange();
originalRange.collapse(true);
sel.createRange().pasteHTML( tw_template_tag );
}
}
function checkIfTagIsTriggered(containerEl) {
var precedingChar = "", sel, range, precedingRange;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(containerEl, 0);
precedingChar = range.toString().slice(-2);
}
} else if ( (sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
precedingChar = precedingRange.text.slice(-2);
}
if( tw_template_trigger == precedingChar )
return true;
return false;
}
function deleteTagTrigger(containerEl) {
var preceding = "",
sel,
range,
precedingRange;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(containerEl, 0);
preceding = range.toString();
}
} else if ((sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
preceding = precedingRange.text;
}
// First Remove {{
var words = range.toString().trim().split(' '),
lastWord = words[words.length - 1];
if (lastWord && lastWord == tw_template_trigger ) {
/* Find word start and end */
var wordStart = range.toString().lastIndexOf(lastWord);
var wordEnd = wordStart + lastWord.length;
range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);
range.deleteContents();
range.insertNode(document.createTextNode(' '));
// delete That specific word and replace if with resultValue
return true;
}
return false;
}
I noticed that those two lines are causing the browser error in the second issue
range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);
Theoretically, I know what the issue is. I believe both issues could be solved by making the range-creating script to use parent node rather than children nodes and also to loop through text nodes which line breaks are. However, I don't have a clue how to implement it at this point.
Could you please point me into the right direction?
Edit
I've actually managed to upload a demo with the progress so far to make it more clear.
Demo
I solved the problem myself and merged all functions into one. Neat! Below is the final code. I removed the ability to press enter after further considering it.
Hope it helps someone
var tw_template_trigger = '{{';
var tw_template_tag = '<span class="tw-template-tag" contenteditable="false">Pick a tag</span>';
$(".tw-post-template-content").keypress(function(e){ return e.which != 13; });
$('.tw-post-template-content').on( 'keyup mouseup', function() {
triggerTag( this );
});
function triggerTag(containerEl) {
var sel,
range,
text;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange(); // clone current range into another variable for manipulation#
range.collapse(true);
range.setStart(containerEl, 0);
text = range.toString();
}
}
if( text && text.slice(-2) == tw_template_trigger ) {
range.setStart( range.endContainer, range.endOffset - tw_template_trigger.length);
range.setEnd( range.endContainer, range.endOffset );
range.deleteContents();
range.insertNode(document.createTextNode(' '));
//
var el = document.createElement("div");
el.innerHTML = tw_template_tag;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
var firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
return true;
}
return false;
}

Prevent editable div's behavior when typing certain characters

I have the following code taken from Pranav C Balan's answer to my previous question:
var div = document.getElementById('div');
div.addEventListener('input', function() {
var pos = getCaretCharacterOffsetWithin(this);
// get all red subtring and wrap it with span
this.innerHTML = this.innerText.replace(/red/g, '<span style="color:red">$&</span>')
setCaretPosition(this, pos);
})
// following code is copied from following question
// https://stackoverflow.com/questions/26139475/restore-cursor-position-after-changing-contenteditable
function getCaretCharacterOffsetWithin(element) {
var caretOffset = 0;
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
var sel;
if (typeof win.getSelection != "undefined") {
sel = win.getSelection();
if (sel.rangeCount > 0) {
var range = win.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
}
} else if ((sel = doc.selection) && sel.type != "Control") {
var textRange = sel.createRange();
var preCaretTextRange = doc.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
function setCaretPosition(element, offset) {
var range = document.createRange();
var sel = window.getSelection();
//select appropriate node
var currentNode = null;
var previousNode = null;
for (var i = 0; i < element.childNodes.length; i++) {
//save previous node
previousNode = currentNode;
//get current node
currentNode = element.childNodes[i];
//if we get span or something else then we should get child node
while (currentNode.childNodes.length > 0) {
currentNode = currentNode.childNodes[0];
}
//calc offset in current node
if (previousNode != null) {
offset -= previousNode.length;
}
//check whether current node has enough length
if (offset <= currentNode.length) {
break;
}
}
//move caret to specified offset
if (currentNode != null) {
range.setStart(currentNode, offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
<span contenteditable="true" id="div" style="width:100%;display:block">sss</span>
It has a editable <div> where the user can input and it automatically colors the word red as red just like some code editors color key words like HTML tags, strings, functions, etc.Type "red" and you will understand what I mean.
The issue I'm having is, when I type "<", it deletes all the characters in front of it unless it finds a ">" where it will stop. Another error happens if you type "&#1" (or any other number instead of 1 really).
Any ideia on how to prevent this behavior?
You're running into this problem because you're expecting the user to be able to input HTML-like entities such as <xyz... or { but don't want to parse that input as HTML, but at the same time, you're yourself putting html elements in the same div and you want that to be parsed as HTML. So there are two ways you can go about this:
Keep the input and presentation separate. So user can input anything, which you'll sanitize and display in an output box.
Or... change the addEventListener function:
div.addEventListener('input', function() {
var pos = getCaretCharacterOffsetWithin(this);
var userString = sanitizeHTML(this.innerText);
// get all red subtring and wrap it with span
this.innerHTML = userString.replace(/red/g, '<span style="color:red">$&</span>')
setCaretPosition(this, pos);
})
This would work in most scenarios, but it'd break (badly) if you're expecting user to input HTML too, for example <span class="red" style="color: red">red</span> would become horribly mutilated. Other than that, you're good to go. Get sanitizeHTML from here: https://github.com/punkave/sanitize-html

Delete particular Text before and after the selected text javascript

I want to delete some particular text before and after the selected text.For example if the text is:
<p>This is a <random>sentence</random> that i am writing<p>
If the user selects text,it should remove <random> and </random> from the text and text will be like this.
This is a sentence that i am writing.
If the user selects anything other than 'sentence',nothing will happen.
I know how to select a particular text but i dont know the next step on how to remove text before and after a particular text.Is it possible?
function replaceSelection() {
var sel, range, fragment;
if (typeof window.getSelection != "undefined") {
// IE 9 and other non-IE browsers
sel = window.getSelection();
// Test that the Selection object contains at least one Range
if (sel.getRangeAt && sel.rangeCount) {
// Get the first Range (only Firefox supports more than one)
range = window.getSelection().getRangeAt(0);
var selectedText = range.toString();
var replacementText = selectedText.replace(/<\/?random>/, '');
range.deleteContents();
// Create a DocumentFragment to insert and populate it with HTML
// Need to test for the existence of range.createContextualFragment
// because it's non-standard and IE 9 does not support it
if (range.createContextualFragment) {
fragment = range.createContextualFragment(replacementText);
} else {
// In IE 9 we need to use innerHTML of a temporary element
var div = document.createElement("div"), child;
div.innerHTML = replacementText;
fragment = document.createDocumentFragment();
while ( (child = div.firstChild) ) {
fragment.appendChild(child);
}
}
var firstInsertedNode = fragment.firstChild;
var lastInsertedNode = fragment.lastChild;
range.insertNode(fragment);
if (selectInserted) {
if (firstInsertedNode) {
range.setStartBefore(firstInsertedNode);
range.setEndAfter(lastInsertedNode);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if (document.selection && document.selection.type != "Control") {
// IE 8 and below
range = document.selection.createRange();
var selectedText = range.text;
var replacementText = selectedText.replace(/<\/?random>/, '')
range.pasteHTML(replacementText);
}
}
<div onmouseup="replaceSelection()"><p>This is a <random>sentence</random> that i am writing<p></div>

JavaScript: how to adapt working function (IE) for Firefox and/or Chrome

I am using the following function to get the selected text range within a contenteditable div.
This works fine in IE but not in Firefox and Chrome.
Can anyone tell me how I would need to adjust this so that it works in either both FF and Chrome or at least one of them (besides IE) ? It it would work in current versions there that would be enough.
The idea with this is to replace the selected text through another function that gets the "selTxt" from here.
The function to get the selection (working in IE):
function GetSelection()
{
selTxt = '';
if (typeof window.getSelection != "undefined")
{
var sel = window.getSelection();
if (sel.rangeCount)
{
var container = document.createElement('div');
for (var i = 0, len = sel.rangeCount; i < len; ++i)
{
container.appendChild(sel.getRangeAt(i).cloneContents());
}
selTxt = container.innerHTML;
}
}
else if (typeof document.selection != 'undefined')
{
if (document.selection.type == 'Text')
{
selTxt = document.selection.createRange().htmlText;
}
}
return selTxt;
}
The function to replace the selection (this seems to be the issue):
function EditBold()
{
var newTxt = '';
btnID = 'btnBold';
GetSelection();
if (selTxt.toLowerCase().indexOf('<strong>') == -1)
{
document.selection.createRange().pasteHTML("<strong>" + selTxt + "</strong>");
}
}
Many thanks for any help with this, Tim.
You use a variable selTxt in the EditBold() function, but don't have it declared inside the function. If the value is supposed to be what is returned by GetSelection(), use the following:
var selTxt = GetSelection();

HTML form, make tab key trigger indent?

The default behaviour in browsers is to select the next form element. I want my textbox to indent code by, lets say 4 spaces when tab is pressed. Just like if you were indenting code in an IDE. How would I achieve this behaviour in JavaScript? If I have to use jQuery, or its easier, I'm fine with that.
Thanks!
Tracking the key code and adding 4 spaces to the element should do it. You can prevent the default when the tab key is pressed. Like so?:
Edit after all comments:
Ahh, ok so you're actually asking for several JS functions (get cursor position in text area, change text, set cursor position in text area). A little more looking around would have given you all of these, but since I'm a nice guy I'll put it in there for ya. The other answers can be found in this post about getCursorPosition() and this post about setCursorPosition(). I updated the jsFiddle for ya. Here's the code update
<script>
$('#myarea').on('keydown', function(e) {
var thecode = e.keyCode || e.which;
if (thecode == 9) {
e.preventDefault();
var html = $('#myarea').val();
var pos = $('#myarea').getCursorPosition(); // get cursor position
var prepend = html.substring(0,pos);
var append = html.replace(prepend,'');
var newVal = prepend+' '+append;
$('#myarea').val(newVal);
$('#myarea').setCursorPosition(pos+4);
}
});
new function($) {
$.fn.getCursorPosition = function() {
var pos = 0;
var el = $(this).get(0);
// IE Support
if (document.selection) {
el.focus();
var Sel = document.selection.createRange();
var SelLength = document.selection.createRange().text.length;
Sel.moveStart('character', -el.value.length);
pos = Sel.text.length - SelLength;
}
// Firefox support
else if (el.selectionStart || el.selectionStart == '0')
pos = el.selectionStart;
return pos;
}
} (jQuery);
new function($) {
$.fn.setCursorPosition = function(pos) {
if ($(this).get(0).setSelectionRange) {
$(this).get(0).setSelectionRange(pos, pos);
} else if ($(this).get(0).createTextRange) {
var range = $(this).get(0).createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}
}(jQuery);
​
</script>
<textarea id="myarea"></textarea>

Categories