getRangeAt(0) bug in MS Edge, workaround? - javascript

I'm working on a project where users can highlight text in a PDF file, and then use getSelection(), getRangeAt(), and getClientRects() to save the boundaries of the highlight for future reference. The problem I'm running into is Microsoft Edge appears to have a bug when executing getRangeAt(0) if the selection crosses elements with position:absolute. See below fiddle and view console in both Edge (broken) and Chrome/Firefox (working as expected). After running in Edge, remove the position:absolute style and run again, Edge now correctly returns the expected rects.
http://jsfiddle.net/Lbgce2km/2/
document.addEventListener('mouseup', function(e) {
let sel = window.getSelection();
let range = sel.getRangeAt(0);
let rects = range.getClientRects();
//check console for result.
console.log('Rects: ', rects);
});
div {
position:absolute
}
<div style='margin-top: 10px'>Microsoft Edge is the default web browser on Windows 10, Windows 10 Mobile, and Xbox One consoles, replacing Internet Explorer 11 and Internet Explorer Mobile. engine</div>
<div style='margin-top: 75px' >
Microsoft initially announced that Edge would support the legacy Trident (MSHTML) layout engine for backwards compatibility, but later said that, due to "strong feedback", Edge would use a new engine, while Internet Explorer would continue to provide the legacy
</div>
<div style='margin-top: 175px'>
The browser includes an integrated Adobe Flash Player (with an internal whitelist allowing Flash applets on Facebook websites to load automatically, bypassing all other security controls requiring user activation)[15] and a PDF reader. It also supports asm.js.
</div>
I've tried working around problem by creating an additional range, setting the position equal to that of the selected range, then extracting the contents of the selected range and insert that inside of a span into the additional range, then returning the rects of the span but it's of course out of place.
const sel = window.getSelection();
const range = sel.getRangeAt(0)
let addRange = document.createRange();
addRange.setStart(sel.anchorNode, sel.anchorOffset);
addRange.setEnd(sel.focusNode, sel.focusOffset);
let span = document.createElement("span");
let con = range.extractContents();
span.appendChild(con);
addRange.insertNode(span);
Anyone have any suggestions on a possible workaround?

Related

Click to select all text in div not working on mobile

I want to select all text inside a div via a single click/tap. See example code (jsFiddle):
document.getElementById('target').addEventListener('click', function (event) {
setTimeout(function() {
window.getSelection().selectAllChildren(
document.getElementById('target')
)
}, 100);
});
<div id="target">
<span>click to select all </span>
<span>text</span>
</div>
It works on desktop, but not properly on iOS using Safari (likely same problem on Android as well).
As you can see, I added a setTimeout() in an attempt to capture the mobile tap, but it only works after taping many times or focusing on the div via a long tap.
How can I make it select the full text with a single tap on iOS using pure JS?
Edit 1: Setting touchstart as event trigger might work, but avoiding it as it can be triggered by scrolling down the page and accidentally touching the element.
Edit 2: Making a bounty for a tested solution which works on iOS and mobile in general.
Instead of JS you could use a CSS only approach with user-select: all; (MDN)
#target {
/*
-webkit-touch-callout: all; /* iOS Safari /
-webkit-user-select: all; /* Safari /
-khtml-user-select: all; /* Konqueror HTML /
-moz-user-select: all; /* Firefox /
-ms-user-select: all; /* Internet Explorer/Edge /
*/
user-select: all;
}
<div id="target">
<span>click to select all </span>
<span>text</span>
</div>
Be aware that if you want users to be able to select a custom range within your #target it won't let them do that with this approach as it will "force" an all-text selection. But if that is not an issue/requirement the CSS approach is more elegant (IMHO) and should work cross device/platform (if you need to support really old browsers, you can use vendor prefixes as well).
I actually like #exside's solution, but cross platform support is sadly poor.
Anyways, the solution.
We'll use the window.getSelection api which returns a Selection object and move on from there.
const selectableTarget = document.getElementById('target');
selectableTarget.onclick = () => {
// Get reference to the Selection object and remove all selected ranges
const selection = window.getSelection();
selection.removeAllRanges();
// Create a new range and select the target node
const range = document.createRange();
range.selectNode(selectableTarget);
// Add the range to the selection
selection.addRange(range);
};
<div id="target">
<span>Select me please</span>
</div>
You can try window selection APIs or document.selection API for old browsers: -
document.getElementById('target').addEventListener('click', function (event) {
if (window.getSelection && document.createRange) { //Browser compatibility
sel = window.getSelection();
if(sel.toString() == ''){ //no text selection
window.setTimeout(function(){
range = document.createRange(); //range object
range.selectNodeContents(event.target); //sets Range
sel.removeAllRanges(); //remove all ranges from selection
sel.addRange(range);//add Range to a Selection.
},100);
}
}else if (document.selection) { //older ie
sel = document.selection.createRange();
if(sel.text == ''){ //no text selection
range = document.body.createTextRange();//Creates TextRange object
range.moveToElementText(el);//sets Range
range.select(); //make selection.
}
}
});
jsfiddle: https://jsfiddle.net/u4yhn3sk/41/

document.execCommand('copy') scrolls to bottom in IE and Edge

I am using the copyToClipboard in my application. It works good in Chrome and Firefox browsers except in IE and Firefox.
In I.E. (esp. version 11), on click of Copy, it introduces a native browser popup, asking to Allow or Deny and also it scrolls to bottom of the page.
The popup introduction is fine, but I need to stop the scrolling downwards.
Code:
$scope.copyData = function(id) {
var copyAreaObject = document.createElement('textarea');
$scope.copyInitialize(id, copyAreaObject);
var selector = document.querySelector('#copyWrapper');
selector.select();
document.execCommand('copy');
document.body.removeChild(copyAreaObject);
};
Click Here for Demo
Tried Deigo Plutino's answer with additional properties as shown below:
copyAreaObject.style.position = 'fixed';
copyAreaObject.style.bottom= 0;
copyAreaObject.style.left= 0;
It works fine and prevents the scrolling issue. Tested in Edge and IE 11.
For more info, you can also refer to this post: How do I copy to the clipboard in JavaScript?
A simple workaround is setting the style "position" of the textarea to "fixed", so IE doesn't need to scroll.
copyAreaObject.style.position = "fixed";

Maintaining scroll position while inserting elements above: glitching only in Safari

I have HTML page which contains a lot of sections. Content of these sections is loaded lazily. After page load, user is scrolled to some particular section. Then content above and below the section is loaded and inserted. Obviously inserting content above changes scroll position so I need to maintain scroll position relatively to the current section.
I have implemented the simplest solution which works perfectly in Chrome, FF, Edge and even IE11 but in Safari - it has glitches.
Here is the code:
function insertElementAbove() {
var elem;
// load elem
var prevOffset = window.pageYOffset;
// insert element
container.insertBefore(elem, container.firstChild);
// measure inserter element height and adjust scroll pos
var elemHeight = elem.offsetHeight;
window.scrollTo(0, prevOffset + elemHeight);
}
I assume that the last 4 lines are run synchronously so according to the browser rendering pipeline no repaint can be done in between:
But in Safari it seems that sometimes paint and composite happens before changing scroll. Why only Safari behaves like this? Can it be because of Safari use IOSurface framework (https://apple.stackexchange.com/a/56820)? Are there any ways to solve/workaround this behavior?
Here is plunkr.
The issue is not reproducible in iframe mode so go to the "Preview in separate window" using button in the top-right corner:
Safari: Version 10.0.1 (12602.2.14.0.7)
macOS Sierra: Version 10.12.1

Is possible to exactly find position of KeyPress event in browsers except IE?

From my recent question, I use KeyPress event for detecting '#' character in rich text editor. By the way, I just found that KeyPress event in other browser like Firefox 3.5 and Google Chrome 4 do not return any position in this event.
For clarify, my position is a distance from top or left of screen. In this case, it should be distance between new character and IFrame screen. In IE, I can find it in event object like x, y, offsetX, offsetY.
Is possible to find position from last character that was typed? Or you have any other idea for finding it.
For an input/textarea, you can find out the current position of the cursor using input.selectionStart/selectionEnd. If it was a simple insertion keypress that will be just ahead of the new character. (Don't rely on it, there are browsers that support neither the IE nor the Mozilla extensions.)
If your ‘rich text editor’ is a HTML designMode/contentEditable thing (the horror!) then I would guess you'd have to use window.getSelection() to read the position of the cursor. This is even worse:
if (window.getSelection) {
var selection= window.getSelection();
if (selection) {
if (selection.getRangeAt) { // Mozilla
if (selection.rangeCount>=1) {
var range= selection.getRangeAt(0);
return [range.startContainer, range.startOffset];
}
} else if (selection.focusNode) { // Webkit
return [selection.focusNode, selection.focusOffset];
}
}
}
Seems to work with designMode for Mozilla, haven't tested in the others.

How do I get the (x, y) pixel coordinates of the caret in text boxes?

I am using jQuery and trying to find a cross browser way to get the pixel coordinates of the caret in <textarea>s and input boxes such that I can place an absolutely positioned div around this location.
Is there some jQuery plugin? Or JavaScript snippet to do just that?
I've looked for a textarea caret coordinates plugin for meteor-autocomplete, so I've evaluated all the 8 plugins on GitHub. The winner is, by far, textarea-caret-position from Component.
Features
pixel precision
no dependencies whatsoever
browser compatibility: Chrome, Safari, Firefox (despite two bugs it has), IE9+; may work but not tested in Opera, IE8 or older
supports any font family and size, as well as text-transforms
the text area can have arbitrary padding or borders
not confused by horizontal or vertical scrollbars in the textarea
supports hard returns, tabs (except on IE) and consecutive spaces in the text
correct position on lines longer than the columns in the text area
no "ghost" position in the empty space at the end of a line when wrapping long words
Here's a demo - http://jsfiddle.net/dandv/aFPA7/
How it works
A mirror <div> is created off-screen and styled exactly like the <textarea>. Then, the text of the textarea up to the caret is copied into the div and a <span> is inserted right after it. Then, the text content of the span is set to the remainder of the text in the textarea, in order to faithfully reproduce the wrapping in the faux div.
This is the only method guaranteed to handle all the edge cases pertaining to wrapping long lines. It's also used by GitHub to determine the position of its # user dropdown.
Note: this answer describes how to get the character co-ordinates of the text-cursor/caret. To find the pixel-co-ordinates, you'll need to extend this further.
The first thing to remember is that the cursor can be in three states
a regular insertion cursor at a specific position
a text selection that has a certain bounded area
not active: textarea does not have focus. Has not been used.
The IE model uses the Object document.selection, from this we can get a TextRange object which gives us access to the selection and thus the cursor position(s).
The FF model/Opera uses the handy variables [input].selectionStart and selectionEnd.
Both models represent a regular ative cursor as a zero-width selection, with the left bound being the cursor position.
If the input field does not have focus, you may find that neither is set.
I have had good success with the following code to insert a piece of text at the current cursor location, also replacing the current selection, if present.
Depending on the exact browser, YMMV.
function insertAtCursor(myField, myValue) {
/* selecion model - ie */
if (document.selection) {
myField.focus();
sel = document.selection.createRange();
sel.text = myValue;
}
/* field.selectionstart/end firefox */
else if (myField.selectionStart || myField.selectionStart == '0' ) {
var startPos = myField.selectionStart;
var endPos = myField.selectionEnd;
myField.value = myField.value.substring(0, startPos)
+ myValue
+ myField.value.substring(endPos, myField.value.length);
myField.selectionStart = startPos + myValue.length;
myField.selectionEnd = startPos + myValue.length;
myField.focus();
}
// cursor not active/present
else {
myField.value += myValue;
}
Bug Note: links are not being correctly marked up in the top para.
Selection object: http://msdn.microsoft.com/en-us/library/ms535869(VS.85).aspx
TextRange object: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Categories