Reputation: 210
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().selectAllChildren
on 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
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
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
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