javascript text editor - how to capture selection - javascript

I'm trying to write a simple web text editor.
what I want to do is let the user type on a textarea (or a textarea-like input) and select text with the mouse or with shift key and click a button to apply a color based effects.
my questions are such:
how do I capture the selection in a way it's not lost when the
button is clicked
how to save the text and the selection
how to make it WYSIWYG
p.s. I don't want a full rich text editor. I need only one effect
thanks

how do I capture the selection in a way it's not lost when the button is clicked
how to save the text and the selection
I credit this completely to #TimDown's answer to a different question here. It replace the current selection with another string.
We create the string by taking the value of the color picker and putting it in a span tag and inserting the current selected text.
how to make it WYSIWYG
Use a contenteditable div and funnel the output to a hidden textarea when the form submits.
Here it is all together. Again, the first function replaceSelection() is not my code.
document.getElementById('color').onchange = function() {
var replace = document.createElement('span');
replace.style.color = this.value;
replace.textContent = window.getSelection().toString();
replaceSelection(replace.outerHTML, true);
}
document.getElementById('wysiwyg').onsubmit = function() {
document.getElementById('result').textContent = document.getElementById('#input').innerHTML;
}
// #TimDown
function replaceSelection(html, selectInserted) {
var sel, range, fragment;
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);
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(html);
} else {
// In IE 9 we need to use innerHTML of a temporary element
var div = document.createElement("div"), child;
div.innerHTML = html;
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);
}
}
}
#input {
min-height: 100px;
border-radius: 5px;
border: 1px solid #ccc;
padding: 5px;
margin-bottom: 1px;
}
#result {
display: none;
}
#wysiwyg input[type="submit"] {
height: 2em;
float: right;
}
<form action="" method="post" id="wysiwyg">
<div id="input" contenteditable="true"></div>
<textarea name="result" id="result"></textarea>
<input type="color" id="color" />
<input type="submit" value="submit" />
</form>

how do I capture the selection in a way it's not lost when the button is clicked
You can get the selected text by using window.getSelection method.
how to save the text and the selection
Save it in a variable.
Ex: var selection = window.getSelection().toString();
how to make it WYSIWYG
You can perform different actions like bolding, underlining or italizing the text (and many other actions) by using window.execCommand method.
I created a simple wysiwyg editor which does bold, underline and italic the selected text.
Check it here.

Related

How do I access the user selected text within an element ui input?

I want to access the text within an ElInput component via Javascript in Electron. According to mozilla it is impossible to access information within an html input or textfield via window.getSelection.
I unsuccessfully tried to access the selection with the proposed selectionStart
const input = document.getElementById("input")
alert(input.selectionStart.toString)
Given that this doesn't work, what do I have to do, to get the text in my selection within my el-input?
<input value="try to select some of the text" onclick="console.log(this.value.substring(this.selectionStart, this.selectionEnd))"/>
You can use .native event handlers as described in Vue.js + Element UI: Get "event.target" at change, and then selectionStart/End of the underlying HTML element will work:
var Main = {
data() {
return {
input: 'Select something.'
}
},
methods: {
dothing(event) {
let elem=event.target;
console.log(elem.value.substring(elem.selectionStart,elem.selectionEnd));
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui#2.13.0/lib/index.js"></script>
<div id="app">
<el-input v-model="input" #mouseup.native="dothing"></el-input>
</div>
Checkout this short video I have made from the ELInput link you provided, the explanation in general is:
Without seeing your html is a bit hard, but I am guessing you don't even have an input with an id of 'input' which is what your javascript code is querying for:
const input = document.getElementById("input")
Looking at the html on the ElInput link you have provided, those inputs have a class of el-input__inner, you can select the input by class with
const input = document.getElementsByClass("el-input__inner")
but this will return an array of elements, you need to make sure you are selecting the one you need (you can select by Id if you have actually added an id tag to the input element, also this is the reason you see a [1] in the video, it is selecting the element in that position of the array).
from there you can select your text inside the input element, and from javascript get the range of the selection with: input.selectionStart and input.selectionEnd
Having those you can now get the substring with input.value.substr(input.selectionStart, input.selectionEnd) and from there do whatever you need with the text.
Based on this answer: How to get selected text from textbox control with javascript you can use Document.querySelectorAll() to get all elements you need. You can use class names, ids or tag names and so on. Then iterate over them with forEach and add the EventListener you need. Inside the forEach loop you can do whatever you like with any given element
UPDATE
Unfortunately the first solution did not work in Firefox. (see further down) This solution should work in more browsers.
var mySpecialSelect = function(element){
element.addEventListener('mouseup', function () {
let startPos = element.selectionStart;
let endPos = element.selectionEnd;
let field_value = element.value;
let selectedText = field_value.substring(startPos,endPos);
if(selectedText.length <= 0) {
return; // stop here if selection length is <= 0
}
// log the selection
console.log(selectedText);
// you can use "element" or "this" to do whatever like toggle a class name
element.classList.toggle('used-this-element');
});
};
var textAreaElements = document.querySelectorAll('textarea');
[...textAreaElements].forEach(mySpecialSelect);
var inputElements = document.querySelectorAll('input');
[...inputElements].forEach(mySpecialSelect);
textarea,
input {
border: solid 2px gray;
}
input {
display: block;
width: 100%;
}
.used-this-element {
border: solid 2px orange;
}
<textarea rows="4" cols="50">Select some text from here and check the console</textarea>
<textarea rows="4" cols="50">Another text Box, Select some text from here and check the console</textarea>
<input type="text" value="Select or change this value">
First solution (hidden in the "Show code snippet") unfortunately window.getSelection() did not work in Firefox. I'll keep this solution here just because maybe someday it will work and then this would be the nicer solution.
var mySpecialSelect = function(element){
element.addEventListener('mouseup', function () {
if(window.getSelection().toString().length <= 0) {
return; // stop here if selection length is <= 0
}
// log the selection
console.log(window.getSelection().toString());
// you can use "element" or "this" to do whatever like toggle a class name
element.classList.toggle('used-this-element');
});
};
var textAreaElements = document.querySelectorAll('textarea');
[...textAreaElements].forEach(mySpecialSelect);
var inputElements = document.querySelectorAll('input');
[...inputElements].forEach(mySpecialSelect);
textarea,
input {
border: solid 2px gray;
}
input {
display: block;
width: 100%;
}
.used-this-element {
border: solid 2px orange;
}
<textarea rows="4" cols="50">Select some text from here and check the console</textarea>
<textarea rows="4" cols="50">Another text Box, Select some text from here and check the console</textarea>
<input type="text" value="Select or change this value">

Place cursor(caret) in specific position in <pre> contenteditable element

I'm making a web application to test regular expressions. I have an input where I enter the regexp and a contenteditable pre element where I enter the text where the matches are found and highlighted.
Example: asuming the regexp is ab, if the user types abcab in the pre element, both regexp and text are sent to an api I implemented which returns
<span style='background-color: lightgreen'>ab</span>c<span style='background-color: lightgreen'>ab</span>
and this string is set as the innerHTML of the pre element
This operation is made each time the user edites the content of the pre element (keyup event to be exact). The problem I have (and I hope you can solve) is that each time the innterHTML is set, the caret is placed at the beginning, and I want it to be placed right after the last character input by de user. Any suggestions on how to know where the caret is placed and how to place it in a desired position?
Thanks.
UPDATE For better understanding...A clear case:
Regexp is ab and in the contenteditable element we have:
<span style='background-color: lightgreen'>ab</span>c<span style='background-color: lightgreen'>ab</span>
Then I type a c between the first a and the first b, so now we have:
acbc<span style='background-color: lightgreen'>ab</span>
At this moment the caret has returned to the beginning of the contenteditable element, and it should be placed right after the c I typed. That's what I want to achieve, hope now it's more clear.
UPDATE2
function refreshInnerHtml() {
document.getElementById('textInput').innerHTML = "<span style='background-color: lightgreen'>ab</span>c<span style='background-color: lightgreen'>ab</span>";
}
<pre contenteditable onkeyup="refreshInnerHtml()" id="textInput" style="border: 1px solid black;" ></pre>
With some help from these functions from here ->
Add element before/after text selection
I've created something I think your after.
I basically place some temporary tags into the html where the current cursor is. I then render the new HTML, I then replace the tags with the span with a data-cpos attribute. Using this I then re-select the cursor.
var insertHtmlBeforeSelection, insertHtmlAfterSelection;
(function() {
function createInserter(isBefore) {
return function(html) {
var sel, range, node;
if (window.getSelection) {
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = window.getSelection().getRangeAt(0);
range.collapse(isBefore);
// Range.createContextualFragment() would be useful here but is
// non-standard and not supported in all browsers (IE9, for one)
var el = document.createElement("div");
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
}
} else if (document.selection && document.selection.createRange) {
// IE < 9
range = document.selection.createRange();
range.collapse(isBefore);
range.pasteHTML(html);
}
}
}
insertHtmlBeforeSelection = createInserter(true);
insertHtmlAfterSelection = createInserter(false);
})();
function refreshInnerHtml() {
var
tag_start = '⇷', //lets use some unicode chars unlikely to ever use..
tag_end = '⇸',
sel = document.getSelection(),
input = document.getElementById('textInput');
//remove old data-cpos
[].forEach.call(
input.querySelectorAll('[data-cpos]'),
function(e) { e.remove() });
//insert the tags at current cursor position
insertHtmlBeforeSelection(tag_start);
insertHtmlAfterSelection(tag_end);
//now do our replace
let html = input.innerText.replace(/(ab)/g, '<span style="background-color: lightgreen">$1</span>');
input.innerHTML = html.replace(tag_start,'<span data-cpos>').replace(tag_end,'</span>');
//now put cursor back
var e = input.querySelector('[data-cpos]');
if (e) {
var range = document.createRange();
range.setStart(e, 0);
range.setEnd(e, 0);
sel.removeAllRanges();
sel.addRange(range);
}
}
refreshInnerHtml();
Type some text below, with the letters 'ab' somewhere within it. <br>
<pre contenteditable onkeyup="refreshInnerHtml()" id="textInput" style="border: 1px solid black;" >It's about time.. above and beyond</pre>
<html>
<head>
<script>
function test(inp){
document.getElementById(inp).value = document.getElementById(inp).value;
}
</script>
</head>
<body>
<input id="search" type="text" value="mycurrtext" size="30"
onfocus="test(this.id);" onclick="test(this.id);" name="search"/>
</body>
</html>
I quickly made this and it places the cursor at the end of the string in the input box. The onclick is for when the user manually clicks on the input and the onfocus is for when the user tabs on the input.
<input id="search" type="text" value="mycurrtext" size="30"
onfocus="test(this.id);" onclick="test(this.id);" name="search"/>
function test(inp){
document.getElementById(inp).value = document.getElementById(inp).value;
}
Here to make selection easy, I've added another span tag with nothing in it, and given it a data-end attribute to make it easy to select.
I then simply create a range from this and use window.getSelection addRange to apply it.
Update: modified to place caret after the first ab
var e = document.querySelector('[data-end]');
var range = document.createRange();
range.setStart(e, 0);
range.setEnd(e, 0);
document.querySelector('[contenteditable]').focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
<div contenteditable="true">
<span style='background-color: lightgreen'>ab</span><span data-end></span>c<span style='background-color: lightgreen'>ab</span>
</div>

How to change the color of the text above a threshold length using js

Making a Twitter tweet box like div which is contenteditable is my task.
I want to change the text color of the div after 60 characters to be red. Something similar to what Twitter does after 120 characters.
I have so far been able to work out how to change the color of the extra text, but I am stuck up at a problem with cursor position.
Here is a fiddle to my attempted code: https://jsfiddle.net/Bharadwajdaya/36sn0dqn/1/
And a snippet below:
var red_string;
$('#div_editable').on("keyup", function(e) {
//console.log
var content = this.innerText;
var siz = this.innerText.length;
if (siz > 60) {
var contentEditableElement = document.getElementById("div_editable");
var con = this.innerText.substring(0, 60);
red_string = this.innerText.substring(60, siz);
var newString = "<span class='highlight'>" + red_string + "</span>";
console.log(newString + con);
$("#div_editable").html(con + newString);
document.getElementById("div_editable").focus();
setSelectionRange(document.getElementById("div_editable"), siz, siz);
setCaretToPos(siz - 61)
}
});
function setCaretToPos(siz) {
console.log(siz)
var node = document.getElementById("div_editable");
node.lastChild.focus();
var textNode = node.lastChild;
var caret = siz;
var range = document.createRange();
range.setStart(textNode, node.lastChild.innerText.length);
range.setEnd(textNode, node.lastChild.innerText.length);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
.highlight {
background-color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div id="div_editable" style="min-width: 320px; min-height: 120px" contenteditable="true">Use this place</div>
As you can see, applying the background color to extra text is working!
My problem is that once it reaches 60 characters the cursor or caret is moving back to start and the caret remains there and never expands.
Want to change the text color of the div after 60 to be red.Similar
like twitter
This is tricky, of course. As long as you just want to change the color of the text after a specified length (60 in your case), whatever you have attempted is good enough. Just wrap the exceeding characters in another element have a suitable class applied to it.
There is no problem with your code as such, and you are definitely on the right track.
Your Problem:
However, the trickiness starts compounding when you want it to be dynamically adjusted. i.e. to keep typing anywhere and get the extra text highlighted on the go.
The problem, which you then face is of keeping the cursor position intact!
Reason:
The contenteditable selection ranges and offsets restart for every element node in the contenteditable itself. And those work with text nodes only. This means, if in your contenteditable div there is another element then the cursor position would be offset from within that element.
This manifests in your case when you wrap the exceeding text into a span to highlight those. Once you overwrite the content of your div after wrapping the extra text in span, you lose the cursor position with the context of its text node.
Solution:
Before, making any changes cache the current cursor position along with the text node cursor is in. Then, after making changes (like wrapping extra text in another element and other stuff), use range.setStart and range.setEnd on that node's firstChild as that will be the text node you can work the cursor with.
You can find the current node by using the anchorNode of the selection object. This return the text node on which the cursor currently is. In order to use this later on in the script, you use parentNode to get its parent element. This element can then be used later while setting the cursor back. In the example below, I am using the em tag (like Twitter), which is then used to get to the actual text node by picking its firstChild. If the cursor is in the first 60 characters zone, then you will get the contenteditable div instead of em.
Here is a crude example snippet: (Works with Chrome, not tested with other browsers)
Notice, how you can now start typing from anywhere and the cursor is preserved.
var div = document.querySelector('div[contenteditable]'), allowedSize = 60;
reformat(div);
div.addEventListener('keyup', highlight);
function highlight(e) { reformat(e.target); }
function reformat(elem) {
var size = elem.textContent.length,
wrapper, txt, firstSection, nextSection, cursorPos
;
cursorPos = getCursorPosition();
elem.dataset.size = allowedSize - size;
if (size > allowedSize) {
txt = elem.textContent;
wrapper = document.createElement('em');
firstSection = txt.slice(0, allowedSize);
nextSection = txt.slice(allowedSize);
wrapper.textContent = nextSection;
elem.innerHTML = firstSection;
elem.appendChild(wrapper);
if (cursorPos) { setCursorPosition(cursorPos) }
}
}
function getCursorPosition() {
var retVal = {};
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount > 0) {
retVal.node = sel.anchorNode.parentNode;
retVal.pos = sel.getRangeAt(0).startOffset;
return retVal;
}
}
return null;
}
function setCursorPosition(position) {
var textNode, range, sel;
if (position.node.tagName == 'EM') {
textNode = div.getElementsByTagName('EM')[0].firstChild;
} else {
if (position.pos > allowedSize) {
textNode = div.getElementsByTagName('EM')[0].firstChild;
position.pos = 1;
} else { textNode = position.node.firstChild; }
}
range = document.createRange();
range.setStart(textNode, position.pos);
range.setEnd(textNode, position.pos);
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
div[contenteditable] {
position: relative;
width: 70vw; height: 120px; padding: 4px;
margin: 8px auto; border: 1px solid #bbb;
font-family: sans-serif; outline: none;
}
div[contenteditable]::after {
content: attr(data-size);
position: absolute; top: 105%; right: 0;
}
div[contenteditable] em { background-color: #fcc; }
<div contenteditable="true" data-size="0">
Start typing anywhere in this box
</div>
Fiddle: https://jsfiddle.net/abhitalks/rmprz6rx/; for you to play with.
Note: The above example is a crude attempt. To make it cross-browser and polish off the kinks, is left as an exercise for the reader.
if you want to change text color of input . please follow below code
$('input').keypress(function(){
var len=$('input').val().length;
if(len>60)
{
$('input').css('color','red')
}
})
Thanks

Last letter in input needs to be in red color

I have got requirement in my project it says if the user is typing some thing on input field last typed character should be in red color
as follows
i have tried nth-child and all, no thing is working
Any help is appreciated
You can't do it with an input tag, unfortunately. We don't have a way to style an individual letter (except for the first letter).
However, who says we have to use an input tag? You can display a contenteditable span for instance, and feed all the updates into a hidden input tag:
span.addEventListener('input', function (e) {
input.value = this.textContent;
});
Style it with some css to make it look like an input:
.awesome-span {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
border: 2px inset;
background-color: white;
}
Of course, make the final character is highlighted:
span.addEventListener('input', function (e) {
input.value = this.textContent;
// dirty hack to flatten the tree
this.textContent = this.textContent;
var lastChar = document.createElement('span');
lastChar.classList.add('highlighted-span');
lastChar.appendChild(this.firstChild.splitText(this.textContent.length - 1));
this.appendChild(lastChar);
});
And make sure to place cursor at the end:
// add to end of event handler
var range = document.createRange();
range.selectNodeContents(this);
range.collapse(false);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
var input = document.getElementsByName('filler-up')[0],
span = input.previousElementSibling;
span.addEventListener('input', function (e) {
input.value = this.textContent;
// dirty hack to flatten the tree
this.textContent = this.textContent;
var lastChar = document.createElement('span');
lastChar.classList.add('highlighted-span');
lastChar.appendChild(this.firstChild.splitText(this.textContent.length - 1));
this.appendChild(lastChar);
var range = document.createRange();
range.selectNodeContents(this);
range.collapse(false);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
.awesome-span {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
border: 2px inset;
background-color: white;
width: 100px;
}
.highlighted-span {
color: red;
}
input {
opacity: 0.2;
}
<span class='awesome-span' contenteditable>blah</span>
<input name='filler-up' />
Is this a good solution? Nope. Is this a good idea? Maybe.
Accessibility is not the problem if you hide the input with js. You can embed this into any form and it'll submit as you expect. It's just Not Nice ™.
As #adeneo pointed out to you in the comments, the only way you could do something like that is to replace your input with a contenteditable thingy.
The idea is to have a contenteditable div next to your input with the input hidden. Handle the keyup event on this div and redirect the text to your input so that it the input is always updated with the text ready for use in your form or whatever logic.
When you handle the keyup event on this div, get the last character and wrap it in a span. Before doing that, just reset the innerHTML of the div to make sure you do not end up with multiple spans.
This way you can get the input value anytime as it is updated with the div.
Here is a very crude example to give you a head-start. Note that the last character is always red.
Demo Fiddle: http://jsfiddle.net/abhitalks/o06L0zaw/
Snippet:
$("#txt + div").on("keyup", function() {
var text = this.innerText; // select the text only
$("#txt").val(text); // update the input
this.innerHTML = text; // reset the innerHTML
var initial = text.substring(0, text.length - 1); // get content upto last char
var last = text.slice(-1); // get last char
var $span = ("<span class='red'>" + last + "</span>"); // wrap last char in span
$(this).empty().append(initial).append($span); // append all to empty div
moveCaret(this); // move caret position to end
});
function moveCaret(elem) {
range = document.createRange(); // create a range object
range.selectNodeContents(elem); // select all content
range.collapse(false); // collapse until end
selection = window.getSelection(); // create a selection object
selection.removeAllRanges(); // remove existing selection
selection.addRange(range); // add the range to the selection
// by the time this all completes, the caret is now at the end
}
input#txt { display: none; }
#txt + div { border: 1px solid gray; }
.red { color: #f00; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="txt" type="text" />
<div contentEditable="true" ></div>

How to mimic JS ranges/selections with divs as backgrounds?

I need to mimic ranges/selections (those that highlight content on a website, and when you press for ex CTRL + C you copy the content) with divs as backgrounds. Chances are that the "highlighting divs" will be position:absolute;
<div id="highlight">
<!-- The highlightor divs would go here -->
</div>
<div id="edit">
<!-- The divs to be highlighted (that have text) would go here -->
</div>
Edit: Functionalities such as copying are essential.
PS: If you're curious about "why", refer to this question.
I created a new question because I felt the old one was pretty much answered, and this one differed to much from that one.
Here's the concept, with some code to get you started. Every time text is selected in the page, append that text to a hidden textarea on the page and then select the textarea. Then, wrap the original selection in a span to make it look selected. This way, you have the appearance of a selection, and since the hidden textarea is actually selected, when the user hits "ctrl+c" they are copying the text from the textarea.
To get the full functionality you are looking for, you'll probably want to extend this. Sniff keys for "ctrl+a" (for select all). I don't think you'll be able to override right-click behavior... at least not easily or elegantly. But this much is at least a proof of concept for you to run with.
window.onload = init;
function init()
{
document.getElementById("hidden").value = "";
document.body.ondblclick = interceptSelection;
document.body.onmouseup = interceptSelection;
}
function interceptSelection(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
var hidden = document.getElementById("hidden");
if (target == hidden) return;
var range, text;
if (window.getSelection)
{
var sel = getSelection();
if (sel.rangeCount == 0) return;
range = getSelection().getRangeAt(0);
}
else if (document.selection && document.selection.createRange)
{
range = document.selection.createRange();
}
text = "text" in range ? range.text : range.toString();
if (text)
{
if (range.surroundContents)
{
var span = document.createElement("span");
span.className = "selection";
range.surroundContents(span);
}
else if (range.pasteHTML)
{
range.pasteHTML("<span class=\"selection\">" + text + "</span>")
}
hidden.value += text;
}
hidden.select();
}
Here's the css I used in my test to hide the textarea and style the selected text:
#hidden
{
position: fixed;
top: -100%;
}
.selection
{
background-color: Highlight;
color: HighlightText;
}

Categories

Resources