Creating a custom selection using selection.addRange() at runtime giving unintended results - javascript

I am encountering an issue in my application which runs on internet explorer where a delete key action is resulting in selecting the parent element of a zero-width space character instead of the zero-width element itself.
To elaborate on what is going wrong,
The span element which contains the zero width space looks like below:
<span class="buffer start">'\u200b'</span>
Now when a delete key action is pressed around this element and when I try to get the selection using window.getSelection at this point, ideally, it was supposed to give me only the text node within the parent span element which is nothing but the zero-width space. Since IE is behaving strangely in some cases giving the entire span element, I tried coming up with a logic as seen below:
var selection = window.getSelection();
if(selection.focusNode && selection.focusNode.nodeName === 'SPAN' && selection.focusNode.children.length === 0 && selection.focusNode.firstChild.textContent === '\u200b')
{
var childNode = selection.focusNode.firstChild;
selection.removeAllRanges();
var range = document.createRange();
range.selectNodeContents(childNode);
selection.addRange(range);
}
Now this is correctly updating the selection variable with the intended result but when I am trying to access the selection.offsetAnchor, it is returning a value of 1, which means that the selection is not ending with the first character. This is again messing up further code flow since ideally it should have returned an offsetAnchor of 0.
I can't seem to understand why there's a difference in the offsetAnchor when I am setting the custom Selection when IE is not giving me the right selection. Does anyone have an idea what could be going wrong here?
Thanks in Advance!

Related

Reset content editable caret position

I am currently trying to create a syntax highlighter for Javascript and I currently facing the issue which I have found out is common with creating something like this which is setting the caret position to the end while the user types or edit contentEditable text.
I researched and found this and many other solutions here on SO but none works. It gets the position of the caret but never resets it so I am trying to find a workaround for this problem.
Below is the code I came up with.
html
<div id="editor" contentEditable="true" onkeyup="resetPosition(this)"></div>
<input type="text" onkeyup="resetPosition(this)" />
js
function getPos(e) {
// for contentedit field
if (e.isContentEditable) {
e.focus()
let _range = document.getSelection().getRangeAt(0)
let range = _range.cloneRange()
range.selectNodeContents(e)
range.setEnd(_range.endContainer, _range.endOffset)
return range.toString().length;
}
// for texterea/input element
return e.target.selectionStart
}
function setPos(pos, e) {
// for contentedit field
if (e.isContentEditable) {
e.focus()
document.getSelection().collapse(e, pos);
return
}
e.setSelectionRange(pos, pos)
}
function resetPosition(e) {
if(e.isContentEditable) {
let currentPosition = getPos(e);
e.innerHTML=e.innerHTML.replace(/[0-9]/g, "a");
setPos(currentPosition, e);
return;
}
e.value = e.value.replace(/[0-9]/g, "a");
setPos(currentPosition, e);
}
This works fine for text input but not for contentEditable divs.
When I type something like function, I get otincfun.
UPDATE: I was able to fix the setPos function by changing this line from document.getSelection().collapse(e, pos); to document.getSelection().collapse(e.firstChild, pos); but a new bug arose.
When I press ENTER Key, the caret goes back to the first line and first character. Please how do I fix?
Below is the fiddle link
https://jsfiddle.net/oketega/bfeh9nm5/35/
Thanks.
The Problems
document.getSelection().collapse(element, index) collapses the cursor to the child node that index points to, not the character index.
I was able to fix the setPos function by changing this line from document.getSelection().collapse(e, pos); to document.getSelection().collapse(e.firstChild, pos);
That will work if you are only replacing characters, but if you are creating a syntax highlighter, you will want to encase characters in span elements to style them. e.firstChild would then only set the position to an index within e's first child, excluding latter span's
Another thing to consider is that you may want to autocomplete the certain chars. The caret position before you manipulate the text may not be the same as after you do so.
The Solution
I recommend creating a <span id="caret-position"></span> element to track where the caret is.
It would work like this:
function textChanged(element) {
// 1
const text = setCursorMarker(element.innerText, element);
// 2
const html = manipulate(text);
element.innerHTML = html;
// 3
const index = findCursorIndex(element);
document.getSelection().collapse(element, index)
}
Every time the user types, you can get the current caret position and slip in the #caret-position element in there.
Overwrite the existing html with the syntax highlighted text
Find out where #caret-position is and put the caret there.
Note: The recommended way to listen for when the user types in the content-editable element is with the oninput listener, not onkeyup. It is possible to insert many characters by holding down a key.
Example
There is a working js fiddle here: https://jsfiddle.net/Vehmloewff/0j8hzevm/132/
Known Issue: After you hit Enter twice, it looses track of where the caret is supposed to be. I am not quite sure why it does that.

Cannot delete HTML element in contentEditable parent if said element is the first child

The problem
I am building a custom boolean search input that should accept the boolean tags under the form of some "boolean tag bubbles" that can be added to the query via a click. Basically, instead of typing {AND} the user can click on the "AND" boolean tag and it is added to the input. Please see the picture attached to understand the layout - https://i.imgur.com/Fwa00zA.png
LE:This behaviour appears to happen only on Chrome.
Regarding the problem: if a tag is added first in the input, (like in the layout example picture) and after adding the tag the types characters only to return before the tag and press backspace, the tag is not going to get deleted.
The tag can be removed via backspace only if the user deletes all the characters added - basically moving the caret at the end and then via backspace deleting everything.
What I have already tried
Initially I made a connection between the problem and all the spans that were generated when I was moving between with my "left/right" arrow keys between the characters toward the boolean tag. Therefore I wrote some code that every time I press a key, I scans the contentEditable parent and clears all the spans and brs created. This cleared some strange cases but I am still stuck with not being able to delete the tag if it's the first element and if there are characters or other elements after the boolean tag.
Several hours ago I found this - contenteditable div backspace and deleting text node problems.
The function that inserts my boolean tag creates the tag as an element. I tried creating the node element as a as suggested in that post. Even as a button, if my element is the first element and it has characters after, it cannot be deleted.
Some of my code
For my current version with the boolean tags as elements, this is the method that creates, on click, my boolean tags and adds them to the parent element.
addBooleanTag($event){
this.$refs.divInput.focus();
if(this.typed == false & this.input_length == 0){
this.$refs.divInput.innerHTML = ''
var space = '';
this.typed = true
this.saveCursorLocation();
}
rangy.restoreSelection(this.saved_sel);
var node = document.createElement('img');
node.src = $event.img;
node.className = "boolean-button--img boolean-button--no-margin";
node.addEventListener('click', () => {
this.$refs.divInput.removeChild(node);
})
this.insertNode(node);
this.saveCursorLocation();
},
This is how the contentEditable parent element looks like
<div
#keydown.enter.prevent
#blur="addPlaceholder"
#keyup="saveCursorLocation(); clearHtmlElem($event)"
#input="updateBooleanInput($event); clearHtmlElem($event)"
#paste="pasted"
v-on:click="clearPlaceholder(); saveCursorLocation(); deleteBooleanTag();"
class="input__boolean input__boolean--no-focus"
ref="divInput"
contenteditable="true">Boolean search..</div>
This is the method that clears my contentEditable parent of breaks and spans
clearHtmlElem($event){
var i = 0;
var temp = $event.target.querySelectorAll("span, br");
if(temp.length > 0){
for(i = 0; i < temp.length; i++){
if(!temp[i].classList.contains('rangySelectionBoundary')){
if (temp[i].tagName == "br"){
temp[i].parentNode.removeChild(temp[i]);
} else {
temp[i].outerHTML = temp[i].innerHTML;
}
}
}
}
},
Expected behaviour vs actual results
I expect that when I press backspace with the caret being after the boolean tag (img element) to delete the element. A normal behaviour.
Instead, it does nothing. The only way to delete that added element/boolean tag is if the tag is the last thing to be deleted.
Please see this gif that I made while reproducing the problem. https://imgur.com/a/vAXop2s - when the console "freaks out" it's basically me pressing backspace against that img element/boolean tag and the element refusing to be deleted.
With some help, I managed to figure this out. I am posting this here for anyone who has problems with contentEditable.
Do not style the contentEditable element. I had some style on the contentEditable div and that was messing a lot of stuff like caret positioning, backspace not working properly etc. Make a wrap around it and move the styling on that wrapper.
LE: One (and probably the main) reason why the contentEditable element was misbehaving was due to having display: flex attached to it.

How to stop contentEditable from adding spans on carriage return

I am modifying the inner html of a contenteditable to wrap a span around certain text that the user types. This is working great, except every time the user creates a line break, the new line also comes across as a span with the same id and styling as the span they were just typing in.
I have seen prevent contenteditable mode from creating <span> tags but in that case, they are trying to remove all spans (or at least that's what everyone's advising them to do). I however, need the spans that I intentionally create, I just don't want them multiplying.
I'm using Chrome 61 right now, but I need solutions that will not produce problems with Chrome, FireFox, Edge, IE 11, or Safari since I need to support those browsers.
I could put a span-killer in the onKeyDown for the enter key like so:
document.addEventListener('keydown', function(event) {
if(event.keycode == 13) {
var span = window.getSelection().anchorNode.parentNode;
if(span.id === "spanThatShouldntAppearAfterPressingEnter"){
unwrap(span);
}
}
}, true);
function unwrap(el){
var parent = el.parentNode; // get the element's parent node
while (el.firstChild){
parent.insertBefore(el.firstChild, el); // move all children out of the element
}
parent.removeChild(el); // remove the empty element
}
I don't really want to do that, can anyone think of something more elegant?

Javascript get range compared to a parent element

I have a function that return an array (won't work in IE) with two elements
the html code of what the user select inside a div (id=text)
the range of the selection
In case the user select a simple string inside the text div the range return the correct values but when the user select a string inside an element child of div (div#text->p for example) range's values are related to the child element but i want them to be related to the parent (div#text)
Here there's a JsFiddle http://jsfiddle.net/paglia_s/XKjr5/: if you select a string of normal text or normal text + bolded text in the teatarea you'll get the right selection while if you select the bolded word ("am") you'll get the wrong one because the range is related to the child element.
There's a way to do so that the range is always related to div#text?
You could use my Rangy library and its new TextRange module, which provides methods of Range and selection to convert to and from character offsets within the visible text of a container element. For example:
var container = document.getElementById("text");
var sel = rangy.getSelection();
if (sel.rangeCount > 0) {
var range = sel.getRangeAt(0);
var rangeOffsets = range.toCharacterRange(container);
}
rangeOffsets has properties start and end relative to the visible text inside container. The visible text isn't necessarily the same as what jQuery's text() method returns, so you'll need to use Rangy's innerText() implementation. Example:
http://jsfiddle.net/timdown/KGMnq/5/
Alternatively, if you don't want to use Rangy, you could adapt functions I've posted on Stack Overflow before. However, these rely on DOM Range and Selection APIs so won't work on IE < 9.
If you don't want to use a library here is a way which worked for me.
The function returns the cursor offset relative to the textContent of the given node (not in relation to the sub nodes).
Note: The current cursor position must lie in the given node or in any of its sub-nodes.
It's not cross-browser compatible (specially not for IE), but I think it's not much work to fix that as well:
function getCursorPositionInTextOf(element) {
var range = document.createRange(),
curRange = window.getSelection().getRangeAt(0);
range.setStart(element, 0);
range.setEnd(curRange.startContainer, curRange.startOffset);
//Measure the length of the text from the start of the given element to the start of the current range (position of the cursor)
return document.createElement("div").appendChild(range.cloneContents()).textContent.length;
}

how to get selected text, but can I get surrounding context in javascript?

I am able to grab the text that a user has selected on a web page,
using this code:
function getSelected() {
var userSelection;
if (window.getSelection) {
selection = window.getSelection();
} else if (document.selection) {
selection = document.selection.createRange();
}
}
is it posible for me to get the words around the
selected word.
Take these sentences for example: "If you need to
framglubble the zartbox, then you should buy the red widget.
Otherwise you can buy the blue widget and save some money."
My code will tell me if the person has selected the word "widget".
But I'd like to know if the selection is after "red" or "blue". Is
this possible? I've been scouring the Internet for some advice, and
I'm having trouble finding an answer.
thank you for your help
I have written quick script that can identify the part before selection and after selection inside the same DIV element.
However if the same DIV contains the same word more than one time and you select only that word, the current code I wrote can't identify if it's the first or second selected word so bottom line it will not answer your needs.
Anyway, you can see/copy/test the code here: http://jsfiddle.net/kvHxJ/ just select something and see the alert that appears.
If it's enough for your needs after all then great, accept this answer and move on... otherwise I need to know: can we assume the user will select whole words only, one word only? If the answer is yes I do have idea how to go around this.
The way to do this in non-IE browsers is to obtain a Range object from the selection. The range has a start and end boundary, and each boundary of the range is expressed as an offset within a node; if the boundary is within a text node, this offset will be a character offset.
For example, if the following was a text node and the selection is delimited by pipes:
"red |widget| blue widget"
... then the range you'd get from the selection would have a start offset of 4 within the text node.
The following will get you a Range representing the selection and alert the start boundary:
var sel = window.getSelection();
var selectedRange = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
alert("Offset " + selectedRange.startOffset
+ " in node " + selectedRange.startContainer.nodeName);
}
Ranges may be compared to other Ranges, so if you wanted to know, for example, if the current selection came after the word "blue" in the above text node, you could create a Range encompassing the word "blue" and compare it with the selected Range:
// Assume the text node is stored in a variable called textNode
var blueRange = document.createRange();
blueRange.setStart(textNode, 11);
blueRange.setEnd(textNode, 15);
var selectionIsAfterBlue =
(selectedRange.compareBoundaryPoints(Range.END_TO_START, blueRange) == 1);
In IE, none of this works and everything is done differently, generally with much more difficulty. To normalize this to single consistent interface, you could use my Rangy library.
IE has the move set of methods, which reduces this problem to just a couple of lines to expand the selection forward or backward any number of words (see http://www.webreference.com/js/column12/trmethods.html). From there, it's just a matter of comparing text against any arbitrary list of values. Other browsers don't have this feature AFAIK. Fate of the browser wars: one develops an awesome feature ignored or barred by patent from any other, so the feature is forever lost and avoided as burden of cross-browser support for all these innovations inevitably falls squarely on the website designers.
So, below is a generalized function to only get the ID of the parent element of the selected text. And, to work with this cross-browser solution, you have to wrap each word in it's own element complete with unique ID or other attribute. With this setup, it should then be a relatively painless jump to looking ahead and back at sibling or sequentially ID'd/named elements.
The catch here is that the client has to click/drag from the start of the word or phrase to the end, and absolutely no bordering spaces. Even double-clicking on a word will cause it to reference the next element (or in the case of IE, the parent DIV). Additionally, you should add code to restrict the selection boundary to a single parent DIV, as the below code may also expand the selection to surrounding elements. But hopefully you can take fixing that up from here. Otherwise, it's up to using vectors to pinpoint the coordinates of a text compared to all surrounding text.
<script type="text/javascript">
function get_selected_element_id() {
if (window.getSelection) {
// FF
var range = window.getSelection();
}
else if (document.selection) {
// IE
var range = document.selection.createRange();
}
if (range.focusNode) {
// FF
var test_value = range.focusNode.parentNode.id;
}
else {
// IE
var test_value = range.parentElement().id;
}
return test_value;
}
</script>
</head>
<body>
<div id="test_div">
<span id="test1">test</span> <span id="test2">asdf</span> <span id="test3">test2</span> <span id="test4">bla</span>
</div>
<button onclick="alert(get_selected_element_id());">go</button>

Categories