22

This question has already been asked but until now there is no working answer so I am tempting to open it again hopefully we can find a hack to it.

I have a contentEditable paragraph and a text input, when I select some text and click the input, the selection is gone.

So I've tried to save the selection on input mousedown and to restore it back on mouseup and yeah it works ( as expected in firefox) But... in chrome the input lose focus :(

See it in action (use chrome) : https://jsfiddle.net/mody5/noygdhdu/

this is the code I've used :

HTML

<p contenteditable="true"> Select something up here and click the input below <br> on firefox the input get the focus and the text still selected. <br> on chrome the text still selected but the input lose focus </p> <input type="text" id="special" style="border: solid blue 1px"> 

javascript

function saveSelection() { if (window.getSelection) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } } else if (document.selection && document.selection.createRange) { return document.selection.createRange(); } return null; } function restoreSelection(range) { if (range) { if (window.getSelection) { sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } else if (document.selection && range.select) { range.select(); } } } var specialDiv = document.getElementById("special"); var savedSel = null; specialDiv.onmousedown = function() { savedSel = saveSelection(); // save the selection }; specialDiv.onmouseup = function() { restoreSelection(savedSel); // restore the selection }; 
5
  • You haven't explained what is your aim exactly. What is the use of it in the awaiting task, or if there is any. You need to be more specific on these than everything else. Your requirement is cosmetic in nature, and the solution provided to such a requirement me be completely unusable to you in practice. So, what is the purpose? Commented May 12, 2016 at 14:58
  • 1
    @BekimBacaj I am not sure if the purpose can change the solution... so I want this for my wysiwyg editor, when you select the text you can make it bold, italic etc ... and the text remain selected, but in some case like if you want to make the selected text as a link then you have to show an input to enter the URL, so when you focus the input, the selection is lost. Does this make sens now ? Commented May 12, 2016 at 21:34
  • it can change both. 1. because demanding to have a focus on more than one single aim/task/subject on a single turn, is absurd, impossible, and the fundamental cause of an epileptic strike on human subject. Therefore we are talking cosmetics, pretending as if you have both elements on focus ,when they're not. Anyway, this kind of problem has had a solution since HTML3 or earlier. But you need buttons for commencing bold italic or link not a text input field. Commented May 12, 2016 at 22:59
  • I think if you are making a WYSIWYG Editor, you should give it your all and write your own text fields, selection ranges etc. There is no particular reason for a text to stay selected except for cosmetics right? So when you select the text, just wrap in it a span and give it a "selected" class. If you want the context menu for selected range, just override the context menu. It will be hard with <br/> and other stuff but you can't rely on things that is implemented differently across various browsers. Commented May 13, 2016 at 6:11
  • @GökhanKurt I like the Idea of "when you select the text, wrap in it a span and give it a -selected- class" do you have any snippet for that please ? I am not sure how to do that especially how to clean it again from the added span after applying a command to it, like : execCommand('createLink') Commented May 13, 2016 at 10:28

8 Answers 8

8
+250

Replacing selection with a <span> is probably the simplest way. You can also use an <iframe>, which is what google uses in Google Docs to maintain selection of text inside document while clicking UI elements.

With <span>, the solution might be like this (this solution build on your original code and the ideas of the other people here, especially @Bekim Bacaj).

!function(doc, win) { var input = doc.getElementById('special')	, editable = doc.getElementById('editable') , button = doc.getElementById('button') , fragment = null , range = null;	function saveSelection() { if (win.getSelection) { sel = win.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } } else if (doc.selection && doc.selection.createRange) { return doc.selection.createRange(); } return null; } /* Not needed, unless you want also restore selection function restoreSelection() { if (range) { if (win.getSelection) { sel = win.getSelection(); sel.removeAllRanges(); sel.addRange(range); } else if (doc.selection && range.select) { range.select(); } } } */ function saveRangeEvent(event) { range = saveSelection(); if (range && !range.collapsed) {	fragment = range.cloneContents(); toggleButton(); } } function toggleButton() { button.disabled = !fragment || !input.value.match(/^https?:.*/); } toggleButton(); editable.addEventListener('mouseup', saveRangeEvent); editable.addEventListener('keyup', saveRangeEvent); button.addEventListener('click', function(event) { // insert link	var link = doc.createElement('a'); link.href = input.value; input.value = ''; range.surroundContents(link); toggleButton(); }); input.addEventListener('keyup', toggleButton); input.addEventListener('change', toggleButton); input.addEventListener('mousedown', function(event) { // create fake selection if (fragment) { var span = doc.createElement('span'); span.className = 'selected'; range.surroundContents(span); } }); input.addEventListener('blur', function(event) { // remove fake selection	if (fragment) { range.deleteContents(); range.insertNode(fragment); //restoreSelection(); } fragment = null; }, true); }(document, window) 
.selected { background-color: dodgerblue; color: white; }
<p id="editable" contenteditable="true"> Select something up here and click the input below <br>on firefox the input get the focus and the text still selected. <br>on chrome the text still selected but the input lose focus </p> <table> <tr> <td> <input type="text" id="special" style="border: solid blue 1px" placeholder="insert valid link incl. http://"> </td> <td> <button id="button">Add link</button> </td> </tr> </table>


Link to jsFiddle

Sign up to request clarification or add additional context in comments.

7 Comments

I like your solution, but still there one or two thing : when you focus the input and after that click the content editable somewhere, the caret will not be show up at the exact place until you click the second time, also you are saving the content in fragment and restore it on input blur, but imagine I enter a url in the input and click a button which will make the selection as a link, in this case the link will be lost because you are restoring the old fragment
I'm afraid I did not found a way how to unwrap selection from <span>, other than delete the selection and insert back the original fragment. But there is solution. Since we have cloned DocumentFragment in fragment variable, we can wrap it in <a> before inserting back into original place. See the improved solution. The challenging part there is to subscribe callbacks on the right events, so there is a good timing between actions.
I have another different Idea, what do you think about applying execcommand of color #FFF and background of blue to the selected text when the input get focus which will give something like <font color="#FFFFFF"><span style="background:blue;">some text here</span></font> and on input blur we just replace the two first opened tag with something like <span class="temp-replacer"></span> and the two last closed tag with <span class="temp-replacer"></span> and at every new text selection we remove those "temp-replacer" to clean up things !
@medBo, I did not even knew Document.execCommand before and its very interesting! I tried use that interface, so I highlighted text with foreColor and backColor commands but I cannot successfully remove the format with removeFormat command on blur. If I comment out execCommeand in mousedown step, then on blur the restoreSelection() function works, but if I call execCommand on mousedown, then restoreSelection() works occasionally and very strangely. See jsFiddle.
haha really ? ok that's good to know about execCommand! the reason why you cannot restore the selection correctly is that you are saving selection before applying foreColor and backColor, and when you are trying to restore it, it saw that there are additional font tag added which it doesn't remember of before! so you have to save the selection after applying execCommand, I've made some modifications to your jsfiddle and the only things needed is to find how to get rid of the coloring format. please read the comments I write in the jsfiddle jsfiddle.net/mody5/cp6L291g/112
|
6

As I can't comment on maioman (some reputation needed :)), here a little addition to his aswer:

The reason it doesn't work in firefox is that putting the focus on the input field removes the selection.

It all works fine if you put a mouseup event on the p instead of a focus event on the inputfield:

 p.addEventListener('mouseup', () => { highlight(select()); // save the selection }) 

Comments

3

I worked a little on this one... It was a really fun and instructive exercise.
I mainly started from Maioman's answer.

I made it so that selected text would end up in an anchor with the href provided in the input field... And the selected text remains selected while inputing the link. That is my understanding of your question.

See my working Fiddle : https://jsfiddle.net/Bes7weB/rLmfb043/
Tested to be working on FF 46, Chrome 50, Safari 5.1 and Explorer 11.

Notice that classList is only supported in IE10 and later.
Also, the links are not "clickable" because of the mouseup event.
But you can see the title attribute on mouseover.
I assume you'll save the paragraph's innerHTML to output it somewhere else.
;)


CSS:

a.highlighted { background: blue; color:white; } 

HTML:

<h1>Select some text below and click GO!</h1> <br> <p contenteditable="true" tabindex="0"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nec risus turpis. Donec nisi urna, semper nec ex ac, mollis egestas risus. Donec congue metus massa, nec lacinia tortor ornare ac. Nulla porttitor feugiat lectus ut iaculis. In sagittis tortor et diam feugiat fermentum. Nunc justo ligula, feugiat dignissim consectetur non, tristique vitae enim. Curabitur et cursus velit. Etiam et aliquam urna. Duis pharetra fermentum lectus et fermentum. Phasellus eget nunc ultricies, ornare libero quis, porta justo. Sed euismod, arcu sed tempor venenatis, urna ipsum lacinia eros, ac iaculis leo risus ac est. In hac habitasse platea dictumst. Sed tincidunt rutrum elit, ornare posuere lorem tempor quis. Proin tincidunt, lorem ac luctus dictum, dui mi molestie neque, a sagittis purus leo a nunc. </p><br> <br> <b>Add a link to selected text:</b> <input type="text" id="hrefInput" style="border: solid blue 1px" value="http://www.test.com"> <input type="button" id="gobutton" value="GO!"><br> <span id="errorMsg" style="display:none;">No selected text!</span><br> <input type="button" id="undoButton" value="Undo"> 

JavaScript:

var p = document.querySelector('p'); var old = p.innerHTML; var HrefInput = document.getElementById("hrefInput"); var GoButton = document.getElementById("gobutton"); var UndoButton = document.getElementById("undoButton"); var errorMsg = document.getElementById("errorMsg"); var idCounter=0; var textSelected=false; UndoButton.addEventListener('focus', function() { console.log("Undo button clicked. Default text reloaded."); restore(); }) GoButton.addEventListener('click', function() { if(!textSelected){ errorMsg.style.display="inline"; errorMsg.style.color="rgb(166, 0, 0)"; errorMsg.style.fontWeight="bold"; return; } console.log("GO button clicked: Link id=a-"+idCounter+" created."); targetId="a-"+idCounter; document.getElementById(targetId).setAttribute("href",HrefInput.value); document.getElementById(targetId).classList.add("createdlink"); document.getElementById(targetId).setAttribute("title",HrefInput.value); document.getElementById(targetId).classList.remove("highlighted"); textSelected=false; idCounter++ }) p.addEventListener('focus', function() { errorMsg.style.display="none"; }); p.addEventListener('mouseup', function() { textSelected=true; console.log("Mouseup event in p : Text selected."); appendanchor(selectText()); // extract the selection HrefInput.focus(); // FireFox HrefInput.blur(); // Needs it. Try without, you'll see. }) function appendanchor(r) { // onmouseup if (!r) return; extracted = r.extractContents(); el = document.createElement('a'); el.setAttribute("id", "a-"+idCounter); el.setAttribute("class", "highlighted"); el.appendChild(extracted); r.insertNode(el) } function selectText() { // onmouseup if (window.getSelection) { console.log("window.getSelection"); sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { // Chrome, FF console.log(sel.getRangeAt(0)); return sel.getRangeAt(0); } else{console.log(sel);} } else if (document.selection && document.selection.createRange) { console.log("elseif"); return document.selection.createRange(); } return null; } function restore() { p.innerHTML = old; textSelected=false; } 

Comments

3

Substituting your selected region with a span element (and coloring that) could be a workaround:

var p = document.querySelector('p'); var old = p.innerHTML; var input = document.querySelector('input'); p.addEventListener('blur', () => { highlight(select()); // save the selection }) p.addEventListener('focus', () => { restore(); // restore the selection }) function highlight(r) { if (!r) return; var extracted = r.extractContents(); el = document.createElement('span'); el.appendChild(extracted); r.insertNode(el) } function select() { if (window.getSelection) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } } else if (document.selection && document.selection.createRange) { return document.selection.createRange(); } return null; } function restore() { p.innerHTML = old; }
span { background: tomato; color:white; }
<p contenteditable="true" tabindex="0"> Select something up here and click the input below <br> on firefox the input get the focus and the text still selected. <br> on chrome the text still selected but the input lose focus </p> <input type="text" id="special" style="border: solid blue 1px">

works on chrome, but not on FF

as suggested by Eric using mouseup event (I actually used blur) on p to call highlight(select()) will fix the issue on FF.

Comments

2

add the focus inside a timeout function, that should fix your issue.

setTimeout(function(){ document.getElementById("textToInsert").focus(); }, 1); 

jsfiddle: http://jsfiddle.net/mody5/L5hx9h3k/1/

3 Comments

it still doesn't work on chrome ?! have you tested it in chrome ? does the text still selected while the input get the focus ?
the issue here is that restoreSelection(selRange) is giving focus to the text, and putting the input focus inside a setTimeout will just give the focus back to the input and remove the selection again :/
This fiddle works in chrome and firefox at least. I think this is correct answer
2

Here is a very old snippet I've posted as an answer on some other list a long time ago. It might help you rethink your current strategy and completely avoid the need for hacking the naturally expected behavior of focus.

function createLink(e){ if(e.target){	var a = window.getSelection().getRangeAt(0);	var b = a.toString();	var z = document.createElement("span");	var l2 = prompt("Enter URL:", "http://");	b = b.link(l2);	z.innerHTML=b;	a.deleteContents();	a.insertNode(z) } else{ document.execCommand("CreateLink") } }
<!DOCTYPE html> <html> <head> <title>Text to Hyperlink</title> </head> <body> <h1>Create a link</h1> Select some text and click the button. On the presented toolbox provide the url and confirm. The selected text will become a hyperlink<br> My Homepage<br> My Favorite<br> My Search Page<br><br> <button onclick="createLink(event)">Make it a link</button> <script> </script> </body> </html>

4 Comments

Thank you for the snippet but I am using a custom popups instead of "prompt" and I need to keep the selection for cosmetic reason as you said... I like the idea of GökhanKurt which is wrapping the selected the text in a span or something like that and give it a style, or maybe when click the button, apply execCommand of color #FFF and background blue
I times I wrote it execCommand was not widely supported, and IE didn't support neither event argument nor target syntax but had a full support of document execCommand property. As I was using browser feature detection to defer which portion of code to use, the if block of code emulates a simplified version browser built-in link toolbox. In that part of code I was using a temporary span for link completion and injection. p.s.: notice that this solution doesn't even require the contentEditable mode to be turned on. Even though, it is not using the execCommand.
@medBo, why don't you provide a fiddle of a simplified code concentrating on this particular feature only - so we can get some idea of what can be done in respect of the route you've taken?
it's a lot of code to put on here, my jsfiddle above is really enough, because I just need to keep the text look like it's selected when I focus on an input (the input text will not be in a prompt! that's all)
1

I'll give you a hint and let you figure it out yourself. You will need to detect if chrome is being used. In your jsfiddle, add console.log(sel); after sel = window.getSelection();. Notice in the log that the selections are different on the different browsers. To be honest, I'm not sure why, but this may help you find out what the problem is.

Also notice the same issue if you comment out sel.removeAllRanges(); you will get an error telling you that they are different, as above.

Comments

0

In my use case, I was able to solve loosing the selection when using an input field with the help of a MutationObserver.

In my component I have got a state for the range which I initialize when it is connected:

private range: Range | undefined; componentWillLoad() { const selection: Selection | undefined = getSelection(); this.range = selection?.getRangeAt(0); } 

getSelection is a utility which returns the Selection according browser.

Then the function which applies the color looks like following:

private selectColor($event: CustomEvent) { const selection: Selection | undefined = getSelection(); if (!selection || !$event || !$event.detail) { return; } selection?.removeAllRanges(); selection?.addRange(this.range); const observer: MutationObserver = new MutationObserver( (_mutations: MutationRecord[]) => { observer.disconnect(); this.range = selection?.getRangeAt(0); }); const anchorNode: HTMLElement | undefined = getAnchorNode(selection); observer.observe(anchorNode, {childList: true}); document.execCommand('foreColor', false, $event.detail.value); 

What's happening: I get the selection, remove all ranges and add the one I preserve as state.

Then I attach a mutation observer on the selection anchor node. For such purpose I use a utility which return either the anchor node or its parent in case of a text or comment would be selected.

Then I call the execCommand.

Once the observer kicks, I query the selection (which is at that point the modified node of the document, not the input) for the new range and save it to my state.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.