I have been trying to build a web based text editor. And as part of the process, I am trying to dynamically create and modify elements based and keystroke events for font editing. In this particular jsfiddle example I'm trying to create a strong element upon hitting CTRL+b and setting the focus/caret inside the strong element so that subsequent text entered will be part of the bold element and hence will have bold text. But my code is just creating a strong element but not transferring the focus hence no text is getting bolder.
In the below code I'm creating event listener to capture keystroke events
p=document.getElementsByTagName("p")[0];
//console.log(p)
// adding eventlistener for keydown
p.addEventListener("keydown",listener);
// eventlistenerr callback function
function listener(){
e=window.event;
if(e.ctrlKey && e.keyCode==66)
{
console.log("CTRL+B");
// creating bold element
belm=document.createElement("strong");
belm.setAttribute("contenteditable","true")
p.appendChild(belm);
//bug below
// setting focus inside bold element
setfocus(belm,0);
e.preventDefault();
}
}
Here is the function for setting the focus.
function setfocus(context, position){
var range = document.createRange();
position =position || 0;
var sel = window.getSelection();
range.setStart(context, position);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
context.focus();
}
However, I have not doubt that the function which sets focus is faulty, because in the fiddle if you observe, I have created a separate setup just to test this
out. Click on the button "Click Here" and the focus dynamically shifts to paragraph element without any hassle. I am unable to figure out what is going wrong.
It's pretty much impossible to move the cursor into an empty element in a contenteditable div. However, as shay levi suggested in another post, you can insert the zero-width character ÈB into your empty element to give it an index value.
Here's an example*:
function insertNode(nodeName) {
var sel = window.getSelection(),
range;
range = sel.getRangeAt(0);
range.deleteContents();
var child = document.createElement(nodeName);
child.innerHTML = '';
range.insertNode(child);
}
var div = document.querySelector('div'),
btn = document.querySelector('button');
btn.addEventListener('click', function() {
insertNode('strong');
div.focus();
});
div.focus();
<div contenteditable></div><button><strong>B</strong></button>
*For the sake of simplicity, this script doesn't toggle bold text, it only sets it.
Related
I can insert html into a contenteditable div just fine using this code:
function insertHtmlAtCursor(html) {
let selection, range, node;
selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
range = window.getSelection().getRangeAt(0);
node = range.createContextualFragment(html);
range.insertNode(node);
}
}
However, afterwards the cursor is placed within the inserted html. Instead I need it to be positioned just after the inserted html. Any ideas?
It seems like https://developer.mozilla.org/en-US/docs/Web/API/Range/setEndAfter e.g. range.setEndAfter(node); range.collapse(); might do. Or even https://developer.mozilla.org/en-US/docs/Web/API/Selection/collapseToEnd selection.collapseToEnd();.
I looked at this question where it is asked for a way to simply copy text as plain text. I want to do exactly that but with one additional thing - not lose focus on the current element.
I need this for a Chrome extension, so I'm not bothered with cross-browser support. When the user types in an input (or contenteditable), a dropdown with choices appears. If he chooses one of them, it is copied to his clipboard. I don't want the element to lose focus because some sites might have implemented logic to run on the element's blur event.
Here's what I've tried:
Solution 1
Create an <input> element and use its select() method:
function clipWithInput(text) {
var input = document.createElement("input");
document.body.appendChild(input);
input.addEventListener("focus", function (e) {
e.preventDefault();
e.stopPropagation();
});
input.value = text;
input.select();
document.execCommand("copy");
document.body.removeChild(input);
}
document.getElementById("choice").onmousedown = function (e) {
e.preventDefault(); // prevents loss of focus when clicked
clipWithInput("Hello");
};
#main {background: #eee;}
#choice {background: #fac;}
<div id="main" contenteditable="true">Focus this, click the div below and then paste here.</div>
<div id="choice">Click to add "Hello" to clipboard</div>
As you can see, this works. The text is copied. However, when you focus the contenteditable and click on the "choice", the focus is lost. The choice element has preventDefault() on its mousedown event which causes it to not break focus. The dummy <input> element is the problem here, even though it has preventDefault() on its focus event. I guess the problem here is that it's too late - the initial element has already fired its blur, so my dummy input's focus is irrelevant.
Solution 2
Use a dummy text node and the Selection API:
function clipWithSelection(text) {
var node = document.createTextNode(text),
selection = window.getSelection(),
range = document.createRange(),
clone = null;
if (selection.rangeCount > 0) {
clone = selection.getRangeAt(selection.rangeCount - 1).cloneRange();
}
document.body.appendChild(node);
selection.removeAllRanges();
range.selectNodeContents(node);
selection.addRange(range);
document.execCommand("copy");
selection.removeAllRanges();
document.body.removeChild(node);
if (clone !== null) {
selection.addRange(clone);
}
}
document.getElementById("choice").onmousedown = function (e) {
e.preventDefault(); // prevents loss of focus when clicked
clipWithSelection("Hello");
};
#main {background: #eee;}
#choice {background: #fac;}
<div id="main" contenteditable="true">Focus this, click the div below and then paste here.</div>
<div id="choice">Click to add "Hello" to clipboard</div>
This works perfectly at first glance. The text is copied, no focus is lost, the caret stays at the same position. No drama. However, when you paste the text in a contenteditable (like Gmail's email composer), this is the result:
<span style="color: rgb(0, 0, 0); font-family: "Times New Roman"; font-size: medium;">Hello</span>
Not plain text.
I tried appending the element in the <head> where there are no styles - nope. Text isn't selected and nothing is copied.
I tried appending the text node in a <span> and set stuff like style.fontFamily to inherit, as well as fontSize and color. Still doesn't work. I logged the dummy element and it correctly had my inherit styles. However, the pasted text didn't.
Recap
I want to programmatically copy plain text with no styles while preserving focus on the currently active element.
Your solution (especially 2) was okay. When you paste in a contenteditable, it needs to be expected that there are span codes inserted, many use that in insertHTML. You are not to expect plain text programmatically. Some would suggest not using a contenteditable at all (though I understand you're talking about some extension). But your solution is more compatible with mobiles than MDN or such.
So, you programmatically copy plain with no style added (if no contenteditable) while preserving focus on the current element.
Sample of what I mean:
http://jsfiddle.net/vXnCM/3717/
<div id="editable" contenteditable="true">
text text text text <span id="test" style="color:red"></span>text text
</div>
<button id="button" onclick="setCaret()">focus</button>
<script language="javascript">
function setCaret() {
var el = document.getElementById("test");
var range = document.createRange();
var sel = window.getSelection();
range.selectNodeContents(el);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
el.focus();
}
</script>
Open the link in Chrome and click Focus, then start typing. Your new text is red.
But if you delete the "ASDF" from the HTML and run it again, your new text is black.
How can I have it be red with no starting text in the div?
(Or, equally effective, how can I have it be red and then clear the starting text from the div without losing the cursor location?)
This is a common problem. Here is some background and potential solutions (none of them are ideal):
js contenteditable - prevent from writing into newly inserted element
How to place caret inside an empty DOM Element Node
How to select a node in a range with webkit browsers?
Set cursor after span element inside contenteditable div
I'm not sure if this is what you want, but I think so, basically it's your range selection logic that needs to be revised. I don't delete the text, but highlight it so when the user types the text is removed, which I think might be what you want (a la 'start typing here').
function setCaret() {
var el = document.getElementById("test");
var range = document.createRange();
var sel = window.getSelection();
range.setStartBefore(el);
range.setEndAfter(el);
sel.addRange(range);
el.focus();
}
Checkout the forked version here:
http://jsfiddle.net/uhe76wup/
Good reference to learn about Range:
https://developer.mozilla.org/en-US/docs/Web/API/Range
I'm trying to replace selected text with another text with a function switchText called from the context menu.
function switchText(info) {
var text = info.selectionText; // selected text
// then I do some manipulations with 'text' and get 'text_to_replace'
var text_to_replace = "some text"; // text to replace
}
alert(text) and alert(text_to_replace) works fine, but I'm trying to replace selected text right on the page but I can't figure out how to do it. I tried different methods but they hadn't worked. Any special permissions needed? I'm sorry if it's stupid question, I'm beginner in JS.
If you want to be able to do this anywhere on a page, you need to be able to set some kind of identifying ID to your selection. You have to do this through a content script of some kind. You can read more about it in the Chrome Developer documentation.
This code will allow you to change the text of a single selection
(tested in Chrome only)
function switchText(id) {
// Gets the selection range
// This is from Tim Down, linked below
var range, sel = window.getSelection();
if (sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);
}
// Creates a new node range
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
// This is from user YeppThatsMe, also linked below
document.execCommand("insertHTML", false, "<span id='own-id'>"+ document.getSelection()+"</span>");
document.designMode = "off";
// You can use either a variable or a string
var someNewText = "-- You can make this whatever you want --";
// Finds the new span and replaces the selection with your new text
document.getElementById("own-id").innerHTML=someNewText;
};
Sourced scripts
Tim's script
HTML5 inserCommand
Last Note
I didn't spend too long testing, and the script as-is will only change one selection per page. You'll need to tweak the way the function gets the tag and attribute info (change it to getElementsByClassName?) to run it more than once, but this should work.
to update a html element targeted by Id
document.getElementById("idtotarget").innerHTML = switchText(document.getElementById("idtotarget").innerHTML)
Is it possible to use or adapt jQuery's .select() to set a selection range on the entire contents of a div?
I have a div which has a series of labels, inputs, select objects and a couple of other UI elements. I have found code on a separate StackOverflow post with some code hosted on jsFiddle: http://jsfiddle.net/KcX6A/570/
Can this be adapted to select the value of inputs also? Or how would you suggest I go about this?
Thanks,
Conor
Edit: More info
I know how to get the value of inputs using jQuery, that is easy, I also know how to select he values of independent elements using .select().
In my div I have a series of different element types including inputs, labels, selects, etc. I need an overall selection of all elements. The jsFiddle link I added earlier shows how to set the range of a div and select the text of elements like p tags etc. What I need is to set the range of the div's contents and when I hit ctrl+c or cmd+c it copies the values of the inputs as well as the labels.
So to summarise, using .val and .select won't work for this I don't think. I need to combine the above in some way but not sure exactly how this will be accomplished. Any ideas?
Check this fiddle: http://jsfiddle.net/JAq2e/
Basically the trick is to introduce a hidden text node whose content will be included in the selection when copied.
jQuery.fn.selectText = function(){
this.find('input').each(function() {
if($(this).prev().length == 0 || !$(this).prev().hasClass('p_copy')) {
$('<p class="p_copy" style="position: absolute; z-index: -1;"></p>').insertBefore($(this));
}
$(this).prev().html($(this).val());
});
var doc = document;
var element = this[0];
console.log(this, element);
if (doc.body.createTextRange) {
var range = document.body.createTextRange();
range.moveToElementText(element);
range.select();
} else if (window.getSelection) {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
};
And use it like:
$('#selectme').selectText();
You can couple the above plugin with an event handler if you want to create selection links :
Code :
$('.select-text').on('click', function(e) {
var selector = $(this).data('selector');
$(selector).selectText();
e.preventDefault();
});
Usage :
Select all
<div id="some-container">some text</div>
Demo : see js fiddle
If you want to select the input elements together with every thing.
Here is a jQuery mixed, JS solution
function selectElement(element) {
if (window.getSelection) {
var sel = window.getSelection();
sel.removeAllRanges();
var range = document.createRange();
range.selectNodeContents(element);
sel.addRange(range);
} else if (document.selection) {
var textRange = document.body.createTextRange();
textRange.moveToElementText(element);
textRange.select();
}
}
selectElement($("div")[0]); //Select the div
$("input").trigger("select"); //select the inputs
Demo
If you want to select inside form elements. Use .focus() /.blur() and .val() functions.
$('input').focus(); //focus on input element
$('input').val(); //return the value of input
Not really. In most browsers it's not possible for the contents of more than one input to be selected at once. See http://jsfiddle.net/timdown/D5sRE/1/