24

I have text in a <p> tag:

<p>Hello world... and goodbye mind A B!</p>

How do I increase the area in which the text is selectable? I know I can increase the font-size and that would increase the area which is selectable, but is there a better way?

To clarify this question. For example, on mobile screens, I find it difficult to highlight words that are one letter like i, but if the hit detection would be on a wider area, it would be a lot easier to select it.

How to do it? A mind-teasing puzzle.

Bounty info

Looking for a working cross browser solution. Please read the question thoroughly and the comments before posting an answer to avoid confusion. User @mmm posted a question that's quite close, but in his approach, while the <p> tag is has a wider hit detection (perfect!), it auto-selects upon click. I need the user to interact with the <p> tag just like we do with normal text based <p> tags... however with a larger hit detector.

EDIT

Further clarification. As an example, the selection area for a comment to this very question is this large:

enter image description here

You can find this comment below. Hover your cursor over it until the cursor gets changed to cursor:text. That's the default selection area.

But my aim is to extend it to a larger area, like this:

enter image description here

Henrik Petterson
  • 6,862
  • 20
  • 71
  • 155
  • 1
    I'll bounty this question with 200 points when it is eligible. This issue has been lurking on my mind for a year. – Henrik Petterson Feb 20 '16 at 22:04
  • 1
    What does "More selectable" mean? – Oriol Feb 20 '16 at 22:06
  • 2
    @Oriol For example, target text in a

    tag via JS and click on it on your mobile screen, if you aim for a single-letter word, then you need to have the fingers of Deadpool when he had it cut off and regenerated in order to click on the single-word letter unless if the font-size is **massive**. Hope that explained it. That's just one scenario. :D

    – Henrik Petterson Feb 20 '16 at 22:09
  • 2
    @oriol - He is meaning, as in if you were to use your finger on a mobile device touch screen to highlight this certain paragraph, is there a way to make that selectable area around this paragraph larger than normal to make it easier to highlight, but also with out affecting the layout – Steve Hartley Feb 20 '16 at 22:10
  • 1
    have you ever tried padding with a pixel value and setting the overflow to hidden ? – Steve Hartley Feb 20 '16 at 22:11
  • @SteveHartley Interesting. But I think padding would affect the layout...? – Henrik Petterson Feb 20 '16 at 22:13
  • 1
    If you don't bounty this question, I will. I'm desperately in need of a solution to this as well. – Gary Woods Feb 20 '16 at 22:14
  • @HenrikPetterson The interface to select text is implementation dependent. My phone has no such interface. Newer versions of Firefox OS have one, and I don't think I would have problems selecting small words with it. Not sure if it's different for Android or other mobile OSes. – Oriol Feb 20 '16 at 22:17
  • I like your question. But I'd like further clarification, if you wouldn't mind. `

    I am Sam.

    ` `

    Sam I am.<\p>` `

    I am Sam.

    ` 1. Will there be particular paragraphs/divs that you would like the text to be more selectable in? That is to say, in the example above, might you like the 'I' in '*Sam I am*' to be selectable? Or would you want all given 'I's on a page to be more selectable?
    – J. Nilles Feb 25 '16 at 03:17
  • `

    Not in a car. Not in a tree.

    ` 2. Following up, within paragraphs, will there be particular characters that you would like to make more selectable. Thus, in the example above would you like the 'a' in '*Not in a tree.*' to be more selectable but not care about the 'a' in '*Not in a car.*'?
    – J. Nilles Feb 25 '16 at 03:20
  • `

    I do not like them with a fox.

    ` 3. Are there particular glyphs/characters that you wish to be more selectable? So, referring to the example above, you might wish to for 'a' to be more selectable but not the 'I'?
    – J. Nilles Feb 25 '16 at 03:21
  • `

    Sam I am.<\p>` 4. Is it acceptable for the single letter words to be more selectable at the expense of the surrounding words. In the example above, would it be ok to make the 'Sam' and 'am' less selectable in order to make the 'I' more selectable?

    – J. Nilles Feb 25 '16 at 03:21
  • @J.Nilles Thanks for the questions. To clarify this further, please see the edit I've made to the question and let me know if it explains it properly. – Henrik Petterson Feb 25 '16 at 13:47
  • Interesting question. I put together a little demo of changing the font size and position when it's active. It makes it more difficult to pick out specific words and probably doesn't work on mobile but maybe someone can see if they can improve it - https://jsfiddle.net/lostmybrain/5e7n9dzy/ – Adam Hughes Feb 25 '16 at 14:13
  • Hi @HenrikPetterson check this . it might help http://stackoverflow.com/questions/8772731/how-to-highlight-clicked-div – Taj Ahmed Mar 01 '16 at 12:29
  • @HenrikPetterson have you found a solution yet? If not, can you provide a jsfiddle or codepen with the html code that is similar to yours? I'm not exactly sure what you want or what you want to avoid, but I can imagine that an example resembling your code could make it easier. And maybe there is a very simple way working with line-height and padding. Do you want this to happen with all `

    `-tags or only a specific one? If specific: would there be a possibility to add a class or id to it?

    – Kathara Aug 15 '17 at 07:27

6 Answers6

10

From my test it works on the iphone as well as ff and chrome - if someone can test on android I'll appreciate feedback!

The border obviously can be removed.

This code uses code from this answer (part of the SelectText() function): Selecting text in an element (akin to highlighting with your mouse)

Fiddle

Code:

function extendSelection() {
    var extendBy = arguments.length <= 0 || arguments[0] === undefined ? 15 : arguments[0];

    var extended = document.getElementsByClassName('extendedSelection');
    [].slice.call(extended).forEach(function (v) {
        var bounds = v.getBoundingClientRect();
        var x = bounds.left;
        var r = textWidth(v.innerHTML, ''+ css(v, 'font-weight') +' ' + css(v, 'font-size') + ' ' + css(v, 'font-family') );
        var y = bounds.top;
        var w = bounds.width;
        var h = bounds.height;
        var element = document.createElement('div');
        element.style.position = 'absolute';
        element.style.height = h + extendBy + 'px';
        element.style.width = r + extendBy + 'px';
        element.style.left = x - extendBy / 2 + 'px';
        element.style.top = y - extendBy / 2 + 'px';
        element.style.border = '1px dotted black';
        document.body.appendChild(element);
        element.addEventListener('click', function (e) {
            SelectText(v);
        });
        element.addEventListener('touchend', function (e) {
            SelectText(v);
        });
    });
}

function css(element, property) {
    return window.getComputedStyle(element, null).getPropertyValue(property);
}

function textWidth(text, font) {
    var el = textWidth.canvas || (textWidth.canvas = document.createElement("canvas"));
    var draw = el.getContext("2d");
    draw.font = font;
    var m = draw.measureText(text);
    return m.width;
};

function SelectText(element) {
    var doc = document,
        text = element,
        range,
        selection;
    if (doc.body.createTextRange) {
        range = document.body.createTextRange();
        range.moveToElementText(text);
        range.select();
    } else if (window.getSelection) {
        selection = window.getSelection();
        range = document.createRange();
        range.selectNodeContents(text);
        selection.removeAllRanges();
        selection.addRange(range);
        selection.setSelectionRange(0, element.value.length)
    }
}

extendSelection();
Community
  • 1
  • 1
baao
  • 71,625
  • 17
  • 143
  • 203
  • 1
    **Very** interesting. I'll put this code to test, but before I proceed, why does it cut the "d" in "extend"? Shouldn't the reach be a bit further? EDIT: Seems to not work accurately, see this example: https://jsfiddle.net/jknnou4r/2/ -- EDIT 2: I tested it on Firefox and you can see the bug. On webkit, it works fine... – Henrik Petterson Feb 20 '16 at 23:14
  • @HenrikPetterson I've updated the code, please copy again. – baao Feb 20 '16 at 23:16
  • Yes, on chrome it's quite nice, I didn't see in firefox - one second I'll have a look – baao Feb 20 '16 at 23:17
  • 1
    Thanks! The problem is the canvas textWidth detection - one second I'll work on it! – baao Feb 20 '16 at 23:18
  • @HenrikPetterson the code is updated to work properly on ff – baao Feb 20 '16 at 23:30
  • 1
    Outstanding! There is one issue though. Is there any way to not make it autoselect the text? The aim is to allow the user to make their own selections (with an enhanced area of clickability). So the selection area is enhanced with is exactly what we aim for, but let's allow the user to make their own selections just like when they select normal text... – Henrik Petterson Feb 20 '16 at 23:36
  • Hmm, I have to think about it if I find a solution. But I think it's going to be hard. On the iphone, it acts like a resizable selection after it has been auto-selected - do you experience different behavior? @HenrikPetterson – baao Feb 20 '16 at 23:38
  • or didn't I understand what you meant? – baao Feb 20 '16 at 23:38
  • I am yet to test it on iphone. To explain this further, I need the user to interact with the p tag just like we do with a normal text based p tag. The only difference is that I want the hit detector to be wider (which it is with your code) but I don't want text to be auto-selected etc. – Henrik Petterson Feb 20 '16 at 23:40
  • @HenrikPetterson I've posted this question, feel free to edit if you can explain better - my english isn't the best... http://stackoverflow.com/questions/35530536/make-auto-selection-of-text-act-like-normal-selection-on-the-iphone – baao Feb 20 '16 at 23:51
  • @GaryWoods did you try the new version? – baao Feb 20 '16 at 23:54
2

You could add padding around paragraphs as someone already suggested but also use negative margin of the same value to prevent it from affecting layout.

Here is DEMO (double-clicking or long-tapping anywhere inside gray area should select text)

Relevant code:

HTML:

<p>normal paragraph</p>
<hr />
<p class="fat-fingers">fat fingers paragraph</p>

CSS:

p {
    //resetting default browser styles for brevity
    //otherwise adjust negative margin value so it's == default margin - padding
    margin: 0;
}

.fat-fingers {
    padding: 10px;
    margin: -10px;
}

note: I didn't test case of two areas overlapping but I assume that the one with higher stacking order wins.

Teo Dragovic
  • 3,438
  • 20
  • 34
  • 2
    Interesting approach but it doesn't essentially increase the **selection area**. For example, if I click-drag the text by clicking on the grey area, it doesn't work. – Henrik Petterson Feb 27 '16 at 14:27
  • I see now that this approach works best for single lines of text. Since `

    ` is block element, if you where trying to click and drag on empty area to the right it would work because it would be selecting that empty part of paragraph. Also if paragraph has more lines and last line end at half width, if you try to click and drag after that line it wouldn't select line above for the same reason. I adjusted my demo a little to better showcase this limitations.

    – Teo Dragovic Feb 27 '16 at 14:53
  • Thanks for the answer. It works perfectly if I want to select the whole paragraph, but I can't click-drag in the middle of it. For example. if I click above "adipisicing" and drag to the left or right, it will not select the word as if I clicked right on it. Please let me know if I am not making sense. – Henrik Petterson Feb 27 '16 at 15:14
  • Hmm, this seems to be cross-browser issue. I get behavior you are describing if I try to click-drag in IE11 but latest FF and Chrome work fine (I'm on Windows 8.1 and also using Chome on Android). Even using caret mode (from this answer: http://stackoverflow.com/questions/30330359/internet-explorer-11-difficulties-in-selecting-text) is futile. Interesting problem... – Teo Dragovic Feb 27 '16 at 15:40
  • I would be also intersted to know how this behaves in Safari and Safari iOS. – Teo Dragovic Feb 27 '16 at 15:43
  • So far, I have tested this on FF and Safari on OSX and doesn't work unfortunately. – Henrik Petterson Feb 27 '16 at 15:57
  • 1
    Thats to bad. But now I'm thinking that using padding/margin hack paragraphs get bigger focus area so maybe I could improve on this using JS focus() and Range methods...I'll try that approach tommorow. – Teo Dragovic Feb 27 '16 at 16:13
  • I look forward this to approach. Thank you very much! – Henrik Petterson Feb 27 '16 at 16:52
  • Just checking in to see if you had any chance to overlook this any further? Bounty ends tomorrow. – Henrik Petterson Feb 29 '16 at 10:05
  • Sorry, I didn't have much time. I tried to do something using [Range](http://www.quirksmode.org/dom/range_intro.html) and found [this](https://jsfiddle.net/abrady0/ggr5mu7o/) example for getting offset of the clicked word which can be used to set selection manually depending on positions of start and end clicks (note line 27, here you could add something like +5 to increase area of discovery) – Teo Dragovic Feb 29 '16 at 11:59
1

I'm assuming the scenario where you have a body of text, and inside that body of text is a fairly important or relevant piece of information to an end user and you would like them to be able to easily highlight and copy the information.

This would be considered as a last option if no other solution was found,

<p class="surroundingText"> BLAH BLAH BLAH  <span class="importantText"> This is the information you would like users to be able to highlight </span> BLAH BLAH BLAH BLAH ETC ETC ETC </p>

If you wrap the text around it in separate paragraph tags and give them a class then set the following in CSS:

.surroundingText {

  -webkit-user-select: none;  /* Chrome all / Safari all */
  -moz-user-select: none;     /* Firefox all */
  -ms-user-select: none;      /* IE 10+ */
  user-select: none;         
}

.importantText {

        -webkit-user-select: all;  /* Chrome all / Safari all */
      -moz-user-select: all;     /* Firefox all */
      -ms-user-select: all;      /* IE 10+ */
      user-select: all;
    }

So the end result is only the text between the span tag is able to be selected.

Steve Hartley
  • 715
  • 5
  • 10
  • Can you please elaborate on this approach? What point is there to disallow selection on other p tags to the tag I'm targeting if they aren't overlapping. – Henrik Petterson Feb 20 '16 at 22:26
  • Thanks for the updated answer. Although I can see how this is very useful if I have a lot of tags around the important one, it isn't a direct answer to the question. The single-letter term doesn't become easier selectable (the area where you select isn't enlarged). – Henrik Petterson Feb 20 '16 at 22:48
  • 1
    Yep, no worries, I thought that might be the case just wanted to put it out there as a back up option . Sorry I couldn't help, ill have a look around for ya. – Steve Hartley Feb 20 '16 at 22:50
0

Exaggerated for effect but what about:

<head>
    <style>   
    p {
        letter-spacing: 2px;
        line-height: 3em;
        word-spacing: 1.5em;
        vertical-align: middle;
    }
    </style>
</head>
<body >

    <p>this is a line.</p>
    <p>this is a line.</p>
    <p>this is a line.</p>
    <p>this is a line.</p>
    <p>this is a line.</p>
    <p>this is a line.</p>
    <p>this is a line.</p>
</body>
J. Nilles
  • 87
  • 5
  • Yes, but that does not look very appealing, especially if used on a website. – kzhao14 Feb 20 '16 at 22:26
  • As OP stated, "But I think padding would affect the layout" – kzhao14 Feb 20 '16 at 22:27
  • Interesting approach, but unfortunately doesn't work well on single-letter terms on mobile. – Henrik Petterson Feb 20 '16 at 22:33
  • Well I did say exaggerated for effect. The OP would need to find a happy medium between spacing the words/letters apart and keeping it appealing/readable. Also, I assumed that the OP wanted to be able to select any given word/section from a text. Anything that doesn't space out the words risks making the select area overlap - making the words difficult to select. – J. Nilles Feb 20 '16 at 22:35
  • @HenrikPetterson why would you only be selecting one letter on a website? – Tom Cupis Feb 23 '16 at 17:17
0

One approach is to increase the line-height of the <p> element. On a mobile device, you can better select a text fragment, because of the larger spacing between the lines. If you define the values in em, the spacing is always relative to the font-size.

A second approach is to increase the word-spacing of the text element to a level which is still acceptable. I would recommend a maximal value of of 0.2em.

HTML:

<p class="extendedSelection">Extended Selection</p>

CSS:

p.extendedSelection {
   line-height: 2em;
   word-spacing: 0.2em;
}

JSFiddle here

If those two approaches are not good enough, you could of course create an absolute positioned element for each word, which overlays the text element, but has an opacity of 0. The text inside of this element should be equal to the text behind but with a larger font-size. This approach has several drawbacks: You need to calculate the positions of every word and duplicate the text content. It is a rather large calculation for just a little effect.

ssc-hrep3
  • 15,024
  • 7
  • 48
  • 87
0

Here is one approach that can be considered if you are ok to have some extra clicks

  1. user needs to first click somewhere around the text that s/he wants to select.
  2. based on the click position we find out the word that was clicked
  3. now we take the previous word, current (clicked) word and next word and show it in a popup within H1 (or any other means to show bigger area to make it more selectable)
  4. when user selects the text from this popup, we close the popup and select appropriate text in the original element that user had clicked

Here is my attempt: https://jsfiddle.net/vnathalye/rtw5bvLx/6/

$('.expandable').click(function(e){
    var clicked = findClickedWord(e.target.childNodes[0], e.clientX, e.clientY);
  if(clicked){
    var $expanded = $('<span>')
                    .appendTo('body')
                    .addClass('expanded')
                    .css({
                      position: "absolute",
                      left: clicked[3].left,
                      top: clicked[3].top,
                      //width: "100px",
                      //height: "100px"
                    })
                    .append($("<h1>").text(clicked[0]));

        var data = {originalElem: e.target.childNodes[0], index: clicked[1], starts: clicked[2]};

    $expanded.data("parentData", data);
    $expanded.on('mouseup', selectionChanged);
    $expanded.on('touchend touchcancel', selectionChanged);

    //alert(JSON.stringify(clicked));
  }
});

function selectionChanged(e){
try {
    var $expanded = $(e.target);
  var data = $expanded.parents(".expanded").data("parentData");
  var selection = window.getSelection();
  if(selection.rangeCount){
    var range1 = selection.getRangeAt(0);
    //alert(range1.startOffset + ":" + range1.endOffset);

    var range2 = document.createRange();
    var originalOffset = data.index>0? data.starts[data.index-1] : data.starts[0];
    range2.setStart(data.originalElem, originalOffset + range1.startOffset);
    range2.setEnd(data.originalElem, originalOffset + range1.endOffset);
    selection.removeAllRanges();
      selection.addRange(range2);
  }
 } catch(err){
 alert(err);
 }
  $expanded.parents(".expanded").remove();
}

function findClickedWord(parentElt, x, y) {
    if (parentElt.nodeName !== '#text') {
        console.log('didn\'t click on text node');
        return null;
    }
    var range = document.createRange();
    var words = parentElt.textContent.split(' ');
    var start = 0;
    var end = 0;
    var starts=[];
    var ends=[];
    for (var i = 0; i < words.length; i++) {
        var word = words[i];
        end = start+word.length;
        starts.push(start);
        ends.push(end);

        range.setStart(parentElt, start);
        range.setEnd(parentElt, end);
        // not getBoundingClientRect as word could wrap
        var rects = range.getClientRects();
        var clickedRect = isClickInRects(rects);

        if (clickedRect) {
                var str = (i==0)? word : words[i-1] + " " + word;
            if(i!=words.length-1) str += " " + words[i+1];
            return [str, i, starts, clickedRect];
        }
        start = end + 1;
    }

    function isClickInRects(rects) {
        for (var i = 0; i < rects.length; ++i) {
            var r = rects[i]
            if (r.left<x && r.right>x && r.top<y && r.bottom>y)
            {            
                return r;
            }
        }
        return false;
    }
    return null;
}

Note:

  1. Positioning of the popup can be improved to suit your needs. I've focused on getting the code for text selection working and have not fine-tuned popup position logic.
  2. With limited time I could test it only in FF on PC and Chrome on Android.
  3. Its the first time I've used touch events, so I'm not sure if I've used them in best possible way. Any criticism, suggestions are most welcome.

Credits

  1. This code is based on the idea that @mmm started with, to use a dummy element to show the extra area for selection
  2. I've used modified version of the code @ https://jsfiddle.net/abrady0/ggr5mu7o/ shared by @TeoDragovic

Let me know your thoughts

Vivek Athalye
  • 2,974
  • 2
  • 23
  • 32