0

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
}
TylerH
  • 20,799
  • 66
  • 75
  • 101
user1521685
  • 210
  • 2
  • 13
  • 1
    did you try looping through all elements and select only those based on required conditions? – Dejan Dozet Jan 12 '19 at 15:54
  • @DejanDozet: I would have to check with `window.getComputedStyle(node).getPropertyValue('-moz-user-select')` - but also for all parents of the node. Let alone that there are also `user-select`, `-ms-user-select`, ... . Also this would be too time-consuming for my purpose – user1521685 Jan 12 '19 at 15:57
  • @DejanDozet: Edit: My code is iterating over all text nodes already using a `NodeIterator`. If there was an easy way to check if a node is selectable manually then this would do the trick. But I wouldn't know how – user1521685 Jan 12 '19 at 16:05
  • If it's possible to quickly decide if a given node is user-selectable it would solve my problem. Maybe it's best to open a new question for this – user1521685 Jan 12 '19 at 18:33
  • can you add noselect class to every no-select element and then get the result you want, you can look at this post: https://stackoverflow.com/a/9314458/4541566 for it and maybe other posts on that page – Dejan Dozet Jan 12 '19 at 19:13
  • Not sure how this would help. If I make every element non-selectable then I already know that they are non-selectable. But I want to find out if an element is selectable – user1521685 Jan 12 '19 at 19:29
  • Agree, but that style on div (-moz-user-select:none) where that came from? – Dejan Dozet Jan 12 '19 at 19:36
  • My code should work on any arbitrary webpage (it's a browser extension). That's why I have to determine programmatically which elements are not selectable – user1521685 Jan 12 '19 at 19:57

3 Answers3

1

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.

Ouroborus
  • 16,237
  • 4
  • 39
  • 62
  • Thank you for your answer:) Yes, it's for an extension. I know that I can find out if an element is selectable by iterating over all its ancestors checking for `getComputedStyle(node).MozUserSelect`, but unfortunately this is way to time-costy, since I'd do that for every node within `body`. I was hoping there is an easy check without having to loop thru all ancestors;) – user1521685 Jan 13 '19 at 13:24
  • @user1521685 There isn't. Perhaps if you supplied a use case, an alternative could be suggested. – Ouroborus Jan 13 '19 at 22:10
0

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.

Dejan Dozet
  • 948
  • 10
  • 26
  • I really appreciate your help! :) Unfortunately I still don't see how this helps to determine if a node is selectable. In your example I can tell if it's selectable because of the class `noselect` - but most webpages won't make it so easy for me – user1521685 Jan 12 '19 at 20:12
  • So, what I need is a function like this: `isSelectable(node) { ... return selectable }` – user1521685 Jan 12 '19 at 20:14
  • yes, and because it is browser specific I think we should start building that function for every browser separately. Like on chrome noselect and your javascript works perfectly, but on firefox, edge and IE it doesn't. (I am using noselect because it works on all browsers for user selection) – Dejan Dozet Jan 12 '19 at 20:26
0

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.

user1521685
  • 210
  • 2
  • 13