user1521685
user1521685

Reputation: 210

How to programmatically only select all text nodes that are selectable by the user

I need to select all nodes in an HTML document using the DOM selection API getSelection.
Nodes that are not selectable by the user (i.e. with mouse) should be excluded.
So, if an element has the CSS rule user-select: none or -moz-user-select: none applied to it, my programmatical selection should exclude those elements.

If I select text manually (via mouse) those elements won't be selected. If I apply window.getSelection().selectAllChildrenon one of its parent elements the non-selectable element is getting selected as well.

I tried different methods both of the Selection and the Range objects, but haven't found a way to only select those elements programmatically that are selectable manually.

<body>
    <div>Selectable</div>
    <div style="-moz-user-select:none">
        <span id="span">Non-Selectable</span>
    </div>

    <script>
        const sel = window.getSelection();
        sel.selectAllChildren(document.body);
        console.log(sel.containsNode(document.getElementById('span')));
        // outputs true
    </script>
</body>  

Does anyone know a way to programmatically select only those elements that are selectable manually?

EDIT So what I need is a function that receives a node as argument and returns a Boolean on wether this node is selectable:

function isSelectable(node) {
    // determine if node is selectable
}

Upvotes: 0

Views: 1676

Answers (3)

user1521685
user1521685

Reputation: 210

Here is a possible way without having to loop thru the node's ancestors:

function isSelectable(textNode) {
    const selection = getSelection();
    selection.selectAllChildren(textNode.parentNode);
    const selectable = !!selection.toString();
    selection.collapseToStart();
    return selectable;
}

Explanation:
If a node is not user-selectable you can still select it programmatically (selectAllChildren), but toString() won't include the node's text content anyway.
In my case I need to iterate over all text nodes of document.body and unfortunately this solution is still too slow for my purpose.

Upvotes: 0

Ouroborus
Ouroborus

Reputation: 16896

Possibly something like this:

var userselect = [
    '-webkit-touch-callout', /* iOS Safari */
    '-webkit-user-select', /* Safari */
    '-khtml-user-select', /* Konqueror HTML */
    '-moz-user-select', /* Firefox */
    '-ms-user-select', /* Internet Explorer/Edge */
    'user-select'
];
function isSelectable(element) {
    var style = getComputedStyle(element);
    var canSelect = !userselect.some(key => style[key] === 'none');
    if(canSelect) {
        if(element.parentElement) return isSelectable(element.parentElement);
        return true;
    }
    return false;
}

Basically, if this element or any of its ancestors are non-select-able then this element is non-select-able. We check this element and then use recursion to check the ancestor elements, stopping either when we run out of ancestors or when we find one that is set non-select-able.

My assumption on how user-select works could be wrong; It might be possible to force an inner element to be select-able even after setting an ancestor non-select-able. The logic could be re-organized to be less confusing. It's certainly possible to remove recursion, using a loop instead. The userselect array could use some intelligence; If this is for an extension, you can use that to inform which attributes you need to check for. This code expects an Element rather than a Node. I haven't actually tested this code but it seems like it should work.

Upvotes: 1

Dejan Dozet
Dejan Dozet

Reputation: 1019

Well, as I suspected your code is partially good (99% good) and that is because of different browsers, combining your script and link that I've already sent you I manage this:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <style>
    .noselect {
  -webkit-touch-callout: none; /* iOS Safari */
    -webkit-user-select: none; /* Safari */
     -khtml-user-select: none; /* Konqueror HTML */
       -moz-user-select: none; /* Firefox */
        -ms-user-select: none; /* Internet Explorer/Edge */
            user-select: none; /* Non-prefixed version, currently
                                  supported by Chrome and Opera */
}
  </style>
</head>
<body>
    <div>Selectable</div>
    <div class="noselect">
        <span id="span">Non-Selectable</span>
    </div>
    <div id="r">
    </div>


    <script>
      window.onload = function() {
        var sel = window.getSelection();
        sel.selectAllChildren(document.body);
        document.getElementById('r').innerHTML = sel.containsNode(document.getElementById('span'));
        // outputs true
      };

    </script>
</body>
</html>

When you run it here you will see that it works! I mean -moz-user-select: none; only works in firefox...

After saying that I've checked other browsers too (IE, Firefox, Chrome and Edge) and this here only works in Chrome.

Upvotes: 0

Related Questions