33

I have a form with a text box and a button:

<p><textarea rows="4" cols="30">Lorem ipsum dolor sit amet, consectetur adipisicing elit.</textarea></p>
<p><input type="button" value="Click me"></p>

The user makes a text selection, then clicks on the button. As textarea loses focus, selection is no longer visible.

Is there a way to make the selection remain visible? It doesn't need to be usable (i.e., there's no need that e.g. typing removes the selection or Ctrl+C copies it) but I'd expect some kind of visual feedback that the textarea contains a selection.

Mockup

Álvaro González
  • 142,137
  • 41
  • 261
  • 360
  • 1
    Have you seen this answer http://stackoverflow.com/questions/8438418/css3-how-to-style-the-selected-text-in-textareas-and-inputs-in-chrome it might be helpfull – Ateszki Mar 20 '13 at 12:42
  • But the textarea *doesn't* contain a selection anymore when it loses focus (at least on my Chrome). So the question AFAICT should be "how to have textarea retain selected text when it loses focus". – JJJ Mar 20 '13 at 12:47
  • @Juhana - Trust me, it does keep the selection, even if it gets lost when you focus back. My full code makes use of it successfully. – Álvaro González Mar 20 '13 at 12:53
  • Check this http://stackoverflow.com/questions/646611/programmatically-selecting-partial-text-in-an-input-field – inser Mar 20 '13 at 12:55
  • @inser - I don't want to create a selection, I already have one (the one created by the user). – Álvaro González Mar 20 '13 at 13:01
  • @Ateszki - I'm aware of the [`::selection` pseudo-element](https://developer.mozilla.org/en-US/docs/CSS/::selection) but there doesn't seem to be a style for non-active elements. – Álvaro González Mar 20 '13 at 13:06
  • Actually on jsFiddle, if you select some text in one frame and click somewhere else, the selection remains visible, just like the way you describe it. – Antony Mar 20 '13 at 13:07
  • @Antony - That's right. ` – Álvaro González Mar 20 '13 at 13:09
  • @Antony, JSFiddle doesn't use iFrames for the frames (CSS, HTML, Javascript). It's actually quite interesting, they use normal `
    `s and other HTML elements, they intercept the keyboard events and modify the HTML layout. Use a debugger and check it, I especially liked the way they blink the typing cursor (I don't know the correct name).
    – Adi Mar 20 '13 at 13:23
  • @Adnan It's called a blinking cursor :) – Antony Mar 20 '13 at 13:30
  • 1
    @Adnan It looks like jsFiddle has got quite a bit of hack into this. The selection is created by JavaScript ([codemirror.js](http://jsfiddle.net/js/codemirror/lib/codemirror.js?ThereIsNoSpring)) and the style is applied through CSS ([codemirror.css](http://jsfiddle.net/js/codemirror/lib/codemirror.css?ThereIsNoSpring)) (the color of the selection is defined by `.CodeMirror-selected { background: #d9d9d9; }`). – Antony Mar 20 '13 at 13:59

9 Answers9

6

After digging through jsFiddle, I found that CodeMirror has everything you need to create a highly customized textarea. It was built for writing codes but with a small trick, it can be applied to textareas in general.

See DEMO

First have a textarea ready:

<textarea id="a">Lorem ipsum dolor sit amet, consectetur adipisicing elit.</textarea>

Then place the following script after it to create a CodeMirror textarea, and provide additional settings to convert it to a normal textarea.

  • mode: I use "none" here to remove syntax highlighting.
  • lineWrapping: use true to wrap for long lines.
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("a"), {
    mode: "none",
    lineWrapping: true
});

Finally, use CSS to control the size and make it look like a standard textarea:

.CodeMirror {
    font-family: monospace;
    font-size: 12px;
    width: 300px;
    height: 100px;
    border: solid 1px #000;
}
Antony
  • 14,900
  • 10
  • 46
  • 74
4
<textarea onblur="this.focus()">this is a test</textarea>
<p><input type="button" value="Click me"></p>

This works as expected with IE/Chrome. But not with FF.


So here is a general solution:

<textarea onblur="doBlur(this)">this is a test</textarea>
<p><input type="button" value="Click me"></p>

<script>
function doBlur(obj) {
  setTimeout(function() { obj.focus(); }, 10);
}
</script>
kobik
  • 21,001
  • 4
  • 61
  • 121
  • 1
    this doesn't work if you are doing anything that takes less than 10 ms, for example opening an md-menu – Mike T Feb 28 '17 at 20:38
3

You could wrap the textarea in an iframe, that still shows the selection within the frame when the button is clicked.

This fiddle uses the srcdoc attribute, which is only supported by Chrome and Safari 6. You could still use an iframe without that attribute though.

EDIT: Here is a new even hackier fiddle that uses jquery to add the iframe, works at least in Chrome and FF (IE doesn't allow data URIs in an iframe for security reasons)

WildCrustacean
  • 5,896
  • 2
  • 31
  • 42
3

I know this isn't technically what you wanted, but I'm going to cheat anyways.

Javascript:

var textarea = document.getElementById('textarea');
var div = document.getElementById('holder');

var original_value = textarea.value;
textarea.onblur = function () {
    var start = textarea.selectionStart;
    var end = textarea.selectionEnd;
    textarea.style.display = "none";
    div.innerHTML = textarea.value.splice(end, 0, "</span>").splice(start, 0, "<span>");
    div.style.display = "block";
}

div.onclick = function () {
    div.style.display = "none";
    textarea.style.display = "block";
    textarea.value = original_value;
}

String.prototype.splice = function( idx, rem, s ) {
    return (this.slice(0,idx) + s + this.slice(idx + Math.abs(rem)));
};

HTML:

<p><textarea id="textarea" rows="4" cols="30">Lorem ipsum dolor sit amet, consectetur adipisicing elit.</textarea></p>
<div id="holder"></div>
<p><input id="click" type="button" value="Click me"></p>

CSS:

textarea, #holder{
    height: 120px;
    width: 300px;
    border: 1px solid black;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    padding: 3px;
    font-size: 10pt;
    font-family: Arial;
}
#holder{    
    display: none;
}
#holder span{
    background-color: #b4d5ff;
}

demo: http://jsfiddle.net/Mb89X/4/

Prisoner
  • 27,391
  • 11
  • 73
  • 102
2

What you are trying to do is not possible with a textarea.

Instead of using a textarea, I would suggest using a div with contenteditable="true" and rangy. Then wrap the selected text or select the text on focus and blur events.

This rangy demo demonstrates how to select text: http://rangy.googlecode.com/svn/trunk/demos/highlighter.html

Skeets
  • 4,476
  • 2
  • 41
  • 67
Stefan
  • 14,826
  • 17
  • 80
  • 143
2

Taking the idea from @Prisoner, I'd like improve further until it's feels like a natural behaviour with scrolling and resizing functionality.

The main idea is the same. I also made full demo with comments here. I repost below for the context of this question.

var input = document.getElementById('input');
var div = document.getElementById('holder');

function syncSize() {
    div.style.width = (input.scrollWidth - 4) + "px";
    div.style.height = input.style.height;
}

new MutationObserver(syncSize).observe(input, {
    attributes: true,
    attributeFilter: ["style"]
});

syncSize();

input.onchange = () => syncSize();

input.onblur = () => {
    var start = input.selectionStart;
    var end = input.selectionEnd;
    var text = input.value;
    div.innerHTML = text.substring(0, start) +
        "<span>" + text.substring(start, end) +
        "</span>" + text.substring(end);
    div.style.display = "block";
}

input.onfocus = () => div.style.display = "none";

input.onscroll = () => div.style.top = -input.scrollTop + "px";

input.value = "Lorem ipsum dolor sit amet";

input.select();
#container {
  position: relative; 
  overflow: hidden;
}

#input {
  width: 400px;
  height: 150px;
}

#holder, #input {
    padding: 2px;
    font: 400 13.3333px monospace;
    border: 1px solid #a9a9a9;
    white-space: pre-wrap;
    word-wrap: break-word;
}

#holder {
    display: none;
    position: absolute;
    left: 0;
    top: 0;
    color: transparent;
    pointer-events: none;
    border-color: transparent;
}

#holder span {
    background-color: #c8c8c8;
    color: black;
}
<div id="container">
  <textarea id="input"></textarea>
  <div id="holder"></div>
</div>
willnode
  • 1,377
  • 13
  • 17
1

If the :focus of the button doen't matter, you could use

$('input[type=button]').click(function () {
    $('textarea').focus();
});

Here is the Fiddle.

Linus Caldwell
  • 10,908
  • 12
  • 46
  • 58
  • Funnily enough, it's a hack that might do the trick in my full code because all I have is a select and a button—nothing that needs to be focused in order to work. I definitively need to test that. – Álvaro González Mar 20 '13 at 13:19
0

Try this:

var element = document.getElementById('text')
if(element.setSelectionRange)
{
    element.focus();
    element.setSelectionRange(2, 10);    
}
else if(element.createTextRange)
{

    var selRange = element.createTextRange();
          selRange.collapse(true);
          selRange.moveStart('character', 2);
          selRange.moveEnd('character', 10);
          selRange.select();
          field.focus();
} else if( typeof element.selectionStart != 'undefined' ) {
      element.selectionStart = 2;
      element.selectionEnd = 10;
      element.focus();
}

http://jsfiddle.net/inser/YJzvb/2/

inser
  • 1,190
  • 8
  • 17
  • 2
    I think you misread the question. If we [add a button](http://jsfiddle.net/hTRZL/), your code behaves like mine. You are only creating a selection programmatically—I don't want to create a selection, I already have one. I just want to make it *visible*. – Álvaro González Mar 20 '13 at 12:57
0

I wrote a small jQuery plugin which modifies a specified <textarea> element when invoked, adding a grayed out selection when the element is unfocused.

// jQuery plugin to make textarea selection visible when unfocused.                                                                                                                                      
// Achieved by adding an underlay with same content with <mark> around                                                                                                                                   
// selected text. (Scroll in textarea not supported, resize is.)                                                                                                                                         
$.fn.selectionShadow = function () {
  const $input = this
  const prop = n => parseInt($input.css(n))
  const $wrap = $input.wrap('<div>').parent()   // wrapper                                                                                                                                               
    .css({
      ...Object.fromEntries(
        'display width height font background resize margin overflowWrap'
          .split(' ').map(x => [x, $input.css(x)])),
      position: 'relative',
      overflow: 'hidden',
      border: 0,
      padding: ['top', 'right', 'bottom', 'left'].map(
        x => prop(`padding-${x}`) + prop(`border-${x}-width`) + 'px'
      ).join(' '),
    })
  const $shadow = $('<span>').prependTo($wrap)  // shadow-selection                                                                                                                                      
    .css({ color: 'transparent' })
  $input                                        // input element                                                                                                                                         
    .on('focusin',  () => $shadow.hide())       //   hide shadow if focused                                                                                                                              
    .on('focusout', () => $shadow.show())
    .on('select', evt => {                      //   if selection change                                                                                                                                 
      const i = evt.target                      //     update shadow                                                                                                                                     
      const [x, s, a] = [i.value ?? '', i.selectionStart, i.selectionEnd]
      $shadow.html(x.slice(0, s) + '<mark class=selectionShadow>' +
                   x.slice(s, a) + '</mark>' + x.slice(a))
    })
    .css({
      boxSizing: 'border-box',
      position: 'absolute', top: 0, left: 0, bottom: 0, right: 0,
      overflow: 'hidden',
      display: 'block',
      background: 'transparent',
      resize: 'none',
      margin: 0,
    })
  $('head').append(
    `<style>mark.selectionShadow { background: #0003; color: transparent }`)
}

Invoke the plugin by with $('textarea').selectionShadow().

The above works by adding an 'underlay' to the textarea, and making sure that the underlay have the exact same padding, font style, and word wrapping rules, so that the text of the textarea and the underlay will precisely overlap. Whenever selection in updated the underlay is also updated, and the current selection is marked with <mark>, which is styled with a gray background (text color is set to transparent in the underlay, so as not to interfere with the antialias edges of the text).

I wrote the above for a web page of mine, but YMMV. Use freely!

zrajm
  • 1,361
  • 1
  • 12
  • 21