How to get neighbor character from selected text? - javascript

I have a string like this:
var comment = 'this is a test';
Assume this i is selected, Now I need to null (left side) and s (right side). How can I get them?
I can get selected text like this:
function getSelectionHtml() {
var html = "";
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
return html;
}
var selected_text = getSelectionHtml();

document.addEventListener("click", function() {
var selection = window.getSelection();
// Check if there are any ranges selected.
if (selection.rangeCount > 0 && selection.type == "Range") {
// Text content of the element.
var text = selection.anchorNode.textContent;
// selection.anchorOffset is the start position of the selection
var before = text.substring(selection.anchorOffset-1, selection.anchorOffset);
// selection.extentOffset is the end position of the selection
var after = text.substring(selection.extentOffset, selection.extentOffset+1);
// Check if there are any letters before or after selected string.
// If not, change before and after to null.
before = before.length === 1 ? before : null;
after = after.length === 1 ? after : null;
console.log(before, after);
}
});
<div>this is a test</div>
To get two characters:
document.addEventListener("click", function() {
var selection = window.getSelection();
// Check if there are any ranges selected.
if (selection.rangeCount > 0 && selection.type == "Range") {
// Text content of the element.
var text = selection.anchorNode.textContent;
// selection.anchorOffset is the start position of the selection
var before = text.substring(selection.anchorOffset-2, selection.anchorOffset);
// selection.extentOffset is the end position of the selection
var after = text.substring(selection.extentOffset, selection.extentOffset+2);
// Check if there are any letters before or after selected string.
// If not, change before and after to null.
before = before.length >= 1 ? before : null;
after = after.length >= 1 ? after : null;
console.log(before, after);
}
});
<div>this is a test</div>

You don't need jQuery for this. All you need is window.getSelection(). This returns an object. You can get the surrounding text with window.getSelection().anchorNode.data, and get the index of the selection within that text using window.getSelection().anchorOffset. Putting this all together, we have
var selection = window.getSelection();
var selectionText = selection.toString();
var surroundingText = selection.anchorNode.data;
var index = selection.anchorOffset;
var leftNeighbor = surroundingText[index - 1];
var rightNeighbor = surroundingText[index + selectionText.length];
Note that you will get undefined instead of null when there is no neighbor character.
window.addEventListener("click", function(){
var selection = window.getSelection();
var selectionText = selection.toString();
var surroundingText = selection.anchorNode.data;
var index = selection.anchorOffset;
var leftNeighbor = surroundingText[index - 1];
var rightNeighbor = surroundingText[index + selectionText.length];
alert(leftNeighbor + " " + rightNeighbor);
});
<div>
this is a test
</div>

The problem with the other two solutions is they both work if the user selects from left to right, but not if they select right to left. As such, I use this modified version:
const selection = window.getSelection();
const text = selection.anchorNode.textContent;
let beforeChar;
let afterChar;
if (selection.anchorOffset < selection.focusOffset) {
beforeChar = text.substring(selection.anchorOffset, selection.anchorOffset-1);
afterChar = text.substring(selection.extentOffset, selection.extentOffset+1);
} else {
beforeChar = text.substring(selection.extentOffset, selection.extentOffset-1)
afterChar = text.substring(selection.anchorOffset, selection.anchorOffset+1);
}
console.log(beforeChar, afterChar);

Related

Highlight text fragments selected by user without nesting tags

I have a <div>some test phrase<div> and I need to allow user to select different fragments of text and highlight them in different colors. Also I need to allow user to delete the the highlighting (but keep the text).
I use Angular, but the solution might be in pure JS.
I've got the partial solution received in response to my previous question:
function mark() {
var rng = document.getSelection().getRangeAt(0);
var cnt = rng.extractContents();
var node = document.createElement('MARK');
node.style.backgroundColor = "orange";
node.appendChild(cnt);
rng.insertNode(node);
}
document.addEventListener('keyup', mark);
document.addEventListener('mouseup', mark);
function unmark(e) {
var tgt = e.target;
if (tgt.tagName === 'MARK') {
if (e.ctrlKey) {
var txt = tgt.textContent;
tgt.parentNode.replaceChild(document.createTextNode(txt), tgt);
}
}
}
document.addEventListener('click', unmark);
::selection {
background: orange;
}
<p>some test phrase</p>
However, if user selects some test and test phrase after that, the selections will intersect and the mark tags will be nesting, while I need them to be like this: <mark>some</mark><mark>test phrase</mark>.
So, the general rule is: the last selection always wins, i.e. its color is always on top. How could I achieve this for any number of selections done?
Also deletion seems not to be working from time to time and I don't know why.
UPDATE:
Kind of implemented this, but I won't be surprised if there is a better way to do this.
Here is the code
I think you can start with this. You should thouroughly test it if it satisfy your case. Perhaps you should also refactor it to better fit you needs.
function mark() {
let selection = document.getSelection();
if(selection.type !== 'Range') { return;}
let pos = window.placeOfSelections;
let ranges = [];
let start = 0;
Array.prototype.forEach.call(pos.childNodes, function(chD)
{
ranges.push([start, start + chD.textContent.length, chD.nodeName === 'MARK']);
start += chD.textContent.length;
});
let text = pos.textContent;
let range = selection.getRangeAt(0);
let firstNode = range.startContainer;
let lastNode = range.endContainer;
selection.removeAllRanges();
let firstNodeIndex = Array.prototype.findIndex.call(pos.childNodes, node => node === firstNode || node.firstChild === firstNode);
let lastNodeIndex = Array.prototype.findIndex.call(pos.childNodes, node => node === lastNode || node.firstChild === lastNode);
let newSelectionStart = ranges[firstNodeIndex][0] + range.startOffset;
let newSelectionEnd = ranges[lastNodeIndex][0] + range.endOffset;
pos.innerHTML = text;
range.setStart(pos.childNodes[0], newSelectionStart);
range.setEnd(pos.childNodes[0], newSelectionEnd);
let node = document.createElement('MARK');
let cnt = range.extractContents();
node.appendChild(cnt);
range.insertNode(node);
let marks = ranges.filter(r => r[2]);
while(marks.length != 0)
{
let startEnd = marks.shift();
if(startEnd[0]>= newSelectionStart && startEnd[1] <= newSelectionEnd)
{
continue;
}
if(startEnd[0]>= newSelectionStart && startEnd[0] <= newSelectionEnd)
{
startEnd[0] = newSelectionEnd;
}
else
if(startEnd[1]>= newSelectionStart && startEnd[1] <= newSelectionEnd)
{
startEnd[1] = newSelectionStart;
}
else
if(startEnd[0] <=newSelectionStart && startEnd[1] >= newSelectionEnd)
{
marks.push([newSelectionEnd, startEnd[1]]);
startEnd[1] = newSelectionStart;
}
let tnStart = 0, tnEnd = 0;
let textNode = Array.prototype.find.call(pos.childNodes, function(tn)
{
tnEnd += tn.textContent.length;
if(tnStart <= startEnd[0] && startEnd[1] <= tnEnd )
{
return true;
}
tnStart += tn.textContent.length ;
});
range.setStart(textNode, startEnd[0] - tnStart);
range.setEnd(textNode, startEnd[1] - tnStart);
node = document.createElement('MARK');
node.appendChild(range.extractContents());
range.insertNode(node);
}
}
window.placeOfSelections.addEventListener('keyup', mark);
window.placeOfSelections.addEventListener('mouseup', mark);
function unmark(e) {
var tgt = e.target;
if ((tgt.tagName === 'MARK' || (e.parentNode && e.parentNode.tagName === "MARK")) && e.ctrlKey) {
let txt = tgt.textContent;
tgt.parentNode.replaceChild(document.createTextNode(txt), tgt);
}
}
window.placeOfSelections.addEventListener('mousedown', unmark);
mark {background-color: #BCE937 ;}
<p id="placeOfSelections">some test phrase</p>

How do I store a range into a database

I want to store user selection in a database in order to highlight them the next time user visits the page.
Here is the code I am using to add a span around the selected text.
window.rangeIntersectsNode = function(range, node) {
var nodeRange;
if (range.intersectsNode) {
return range.intersectsNode(node);
} else {
nodeRange = node.ownerDocument.createRange();
try {
nodeRange.selectNode(node);
} catch (e) {
nodeRange.selectNodeContents(node);
}
return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 && range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
}
}
window.getSelectedElementTags = function(win) {
var range, sel, elmlist, treeWalker, containerElement;
sel = win.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0);
}
if (range) {
containerElement = range.commonAncestorContainer;
if (containerElement.nodeType != 1) {
containerElement = containerElement.parentNode;
}
treeWalker = win.document.createTreeWalker(
containerElement, NodeFilter.SHOW_TEXT, function(node) {
return rangeIntersectsNode(range, node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}, false);
elmlist = [treeWalker.currentNode];
while (treeWalker.nextNode()) {
elmlist.push(treeWalker.currentNode);
}
return (elmlist);
}
}
window.wrapInSpan = function(range) {
var tempElement;
var selectedArr = getSelectedElementTags(window);
for (var i = 2; i < (selectedArr.length - 1); i++) {
tempElement = document.createElement("span");
tempElement.innerHTML = selectedArr[i].nodeValue;
selectedArr[i].parentElement.replaceChild(tempElement, selectedArr[i])
}
var sC = range.startContainer;
if (selectedArr.length == 2) {
if (sC.nodeType == 3) {
tempElement = document.createElement("span");
tempElement.innerHTML = sC.nodeValue.substring(range.startOffset, range.endOffset);
var anc1 = sC.splitText(range.startOffset);
var anc2 = (anc1.splitText(range.endOffset)).previousSibling;
anc2.parentElement.replaceChild(tempElement, anc2);
}
} else {
if (sC.nodeType == 3) {
tempElement = document.createElement("span");
tempElement.innerHTML = sC.nodeValue.substring(range.startOffset);
sC.parentElement.insertBefore(tempElement, sC.nextSibling);
sC.nodeValue = sC.nodeValue.replace(sC.nodeValue.substring(range.startOffset), "");
}
var eC = range.endContainer;
if (eC.nodeType == 3) {
tempElement = document.createElement("span");
tempElement.innerHTML = eC.nodeValue.substring(0, range.endOffset);
eC.parentElement.insertBefore(tempElement, eC);
eC.nodeValue = eC.nodeValue.replace(eC.nodeValue.substring(0, range.endOffset), "");
}
}
}
i am using the following code to get the details of the selection, but it in the console I get always get the variables as undefined.
var startNode=range.anchorNode;
var endNode=range.focusNode;
var startoffset=range.anchorOffset;
var endoffset=range.focusOffset;
var location=[startNode,endNode,startoffset,endoffset];
console.log(startNode);
return location;
I would greatly appreciate if some one can help me figure out how to store the range and to reload it. I am not looking to use rangy's library unless it is essentials. Also i am not sure if it will work in a chrome extension

Prefix/Suffix highlighted text in textbox

I have to make a small javascript function that adds a prefix and suffix to a selected text within a textbox.
This is what I have so far:
function AddTags(name, prefix, suffix) {
try
{
var textArea = document.getElementById(name).value;
var i = 0;
var textArray = textArea.split("\n");
if (textArray == null) {
document.getElementById(name).value += prefix + suffix
}
else {
for (i = 0; i < textArray.length; i++) {
textArray[i] = prefix + textArray[i] + suffix;
}
document.getElementById(name).value = textArray.join("\n");
}
}
catch (err) { }
}
Now this function adds the provided prefix and suffix to every line, but I need to find out how to break up my textbox's text in Text before selection, Selected text and Text after selection.
Anybody any experience on this?
EDIT:
TriniBoy's function set me on the right track. I didn't need the whole suggestion.
This is the edited version of my original code:
function AddTags(name, prefix, suffix) {
try
{
var textArea = document.getElementById(name);
var i = 0;
var selStart = textArea.selectionStart;
var selEnd = textArea.selectionEnd;
var textbefore = textArea.value.substring(0, selStart);
var selected = textArea.value.substring(selStart, selEnd);
var textAfter = textArea.value.substring(selEnd);
if (textAfter == "") {
document.getElementById(name).value += prefix + suffix
}
else {
document.getElementById(name).value = textbefore + prefix + selected + suffix + textAfter;
}
}
catch (err) { }
}
Thx TriniBoy, I'll mark your leg-up as answer.
Based on your demo and your explanation, hopefully I got your requirements correct.
See code comments for a break down.
See demo fiddle here
var PreSuffApp = PreSuffApp || {
selText: "",
selStart: 0,
selEnd: 0,
getSelectedText: function (id) {
var text = "",
docSel = document.selection, //For IE
winSel = window.getSelection,
P = PreSuffApp,
textArea = document.getElementById(id);
if (typeof winSel !== "undefined") {
text = winSel().toString(); //Grab the current selected text
if (typeof docSel !== "undefined" && docSel.type === "Text") {
text = docSel.createRange().text; //Grab the current selected text
}
}
P.selStart = textArea.selectionStart; //Get the start of the selection range
P.selEnd = textArea.selectionEnd; //Get the end of the selection range
P.selText = text; //Set the value of the current selected text
},
addTags: function (id, prefix, suffix) {
try {
var textArea = document.getElementById(id),
P = PreSuffApp,
range = P.selEnd - P.selStart; //Used to calculate the lenght of the selection
//Check to see if some valuable text is selected
if (P.selText.trim() !== "") {
textArea.value = textArea.value.splice(P.selStart, range, prefix + P.selText + suffix); //Call the splice method on your text area value
} else {
alert("You've selected a bunch of nothingness");
}
} catch (err) {}
}
};
//Extend the string obj to splice the string from a start character index to an end range, like an array.
String.prototype.splice = function (index, rem, s) {
return (this.slice(0, index) + s + this.slice(index + Math.abs(rem)));
};

JavaScript window.find() does not select search term

When I try to pass text which spreads throughout a few block elements the window.find method dosent work:
HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
</head>
<body>
<p>search me</p><b> I could be the answer</b>
</body>
</html>
JavaScript:
window.find("meI could be");
Or:
str = "me";
str+= "\n";
str+="I could be t";
window.find(str);
This happens when the <p> element is present between the search term.
The end result should be a GUI selection on the text in the page, I do not want to search if it exists.
I would like to know how to achieve this in Firefox(at least) and internet explorer.
Note: I can't change the dom (e.g. change to display inline).
Edit:
Here is something I tried after #Alexey Lebedev's comment, but it also finds the script (tag [...] text):
Can I make it more simple? (better)?
function nativeTreeWalker(startNode) {
var walker = document.createTreeWalker(
startNode,
NodeFilter.SHOW_TEXT,
null,
false
);
var node;
var textNodesV = [];
var textNodes = [];
node = walker.nextNode();
while(node ) {
if(node.nodeValue.trim()){
textNodes.push(node);
textNodesV.push(node.nodeValue);
//console.log(node.nodeValue);
}
node = walker.nextNode();
}
return [textNodes,textNodesV];
}
var result = nativeTreeWalker(document.body);
var textNodes = result[0];
var textNodesV = result[1];
var param = " Praragraph.Test 3 Praragr";
paramArr = param.split(/(?=[\S])(?!\s)(?=[\W])(?=[\S])/g);
//Fix split PARAM
for(i=0;i<paramArr.length-1;i++){
paramArr[i]= paramArr[i]+paramArr[i+1].charAt(0);
paramArr[i+1] = paramArr[i+1].substring(1,paramArr[i+1].length);
}
//Fix last element PARAM
if(paramArr[paramArr.length-1] === ""){
paramArr.splice(paramArr.length-1,1);
}
//console.log(paramArr);
var startNode,startOffset,sFound=false,
endNode,endOffset;
for(i=0;i<paramArr.length;i++){
for(j=0;j<textNodesV.length;j++){
//Fully Equal
var pos = textNodesV[j].indexOf(paramArr[i]);
if(pos != -1){
if(!sFound){
startNode = textNodes[j];
startOffset = pos;
sFound=true;
}else{
endNode = textNodes[j];
endOffset = pos+paramArr[i].length;
break;
}
}
}
}
console.log(startNode);
console.log(startOffset);
console.log(endNode);
console.log(endOffset);
var range = document.createRange();
range.setStart(startNode,startOffset);
range.setEnd(endNode,endOffset);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
Note: No jQuery (only Raw JS).
JS Bin demo: http://jsbin.com/aqiciv/1/
If you want this to work in IE < 9 you'll need to add MS-specific selection code (nightmare), or use Rangy.js (pretty heavy).
function visibleTextNodes() {
var walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ALL,
function(node) {
if (node.nodeType == 3) {
return NodeFilter.FILTER_ACCEPT;
} else if (node.offsetWidth && node.offsetHeight && node.style.visibility != 'hidden') {
return NodeFilter.FILTER_SKIP;
} else {
return NodeFilter.FILTER_REJECT;
}
},
false
);
for (var nodes = []; walker.nextNode();) {
nodes.push(walker.currentNode);
}
return nodes;
}
// Find the first match, select and scroll to it.
// Case- and whitespace- insensitive.
// For better scrolling to selection see https://gist.github.com/3744577
function highlight(needle) {
needle = needle.replace(/\s/g, '').toLowerCase();
var textNodes = visibleTextNodes();
for (var i = 0, texts = []; i < textNodes.length; i++) {
texts.push(textNodes[i].nodeValue.replace(/\s/g, '').toLowerCase());
}
var matchStart = texts.join('').indexOf(needle);
if (matchStart < 0) {
return false;
}
var nodeAndOffsetAtPosition = function(position) {
for (var i = 0, consumed = 0; consumed + texts[i].length < position; i++) {
consumed += texts[i].length;
}
var whitespacePrefix = textNodes[i].nodeValue.match(/^\s*/)[0];
return [textNodes[i], position - consumed + whitespacePrefix.length];
};
var range = document.createRange();
range.setStart.apply(range, nodeAndOffsetAtPosition(matchStart));
range.setEnd.apply( range, nodeAndOffsetAtPosition(matchStart + needle.length));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
range.startContainer.parentNode.scrollIntoView();
}
highlight('hello world');

How do you get the cursor position in a textarea?

I have a textarea and I would like to know if I am on the last line in the textarea or the first line in the textarea with my cursor with JavaScript.
I thought of grabbing the position of the first newline character and the last newline character and then grabbing the position of the cursor.
var firstNewline = $('#myTextarea').val().indexOf('\n');
var lastNewline = $('#myTextarea').val().lastIndexOf('\n');
var cursorPosition = ?????;
if (cursorPosition < firstNewline)
// I am on first line.
else if (cursorPosition > lastNewline)
// I am on last line.
Is it possible to grab the cursor position within the textarea?
Do you have a better suggestion for finding out if I am on the first or last line of a textarea?
jQuery solutions preferred unless JavaScript is as simple or simpler.
If there is no selection, you can use the properties .selectionStart or .selectionEnd (with no selection they're equal).
var cursorPosition = $('#myTextarea').prop("selectionStart");
Note that this is not supported in older browsers, most notably IE8-. There you'll have to work with text ranges, but it's a complete frustration.
I believe there is a library somewhere which is dedicated to getting and setting selections/cursor positions in input elements, though. I can't recall its name, but there seem to be dozens on articles about this subject.
Here's a cross browser function I have in my standard library:
function getCursorPos(input) {
if ("selectionStart" in input && document.activeElement == input) {
return {
start: input.selectionStart,
end: input.selectionEnd
};
}
else if (input.createTextRange) {
var sel = document.selection.createRange();
if (sel.parentElement() === input) {
var rng = input.createTextRange();
rng.moveToBookmark(sel.getBookmark());
for (var len = 0;
rng.compareEndPoints("EndToStart", rng) > 0;
rng.moveEnd("character", -1)) {
len++;
}
rng.setEndPoint("StartToStart", input.createTextRange());
for (var pos = { start: 0, end: len };
rng.compareEndPoints("EndToStart", rng) > 0;
rng.moveEnd("character", -1)) {
pos.start++;
pos.end++;
}
return pos;
}
}
return -1;
}
Use it in your code like this:
var cursorPosition = getCursorPos($('#myTextarea')[0])
Here's its complementary function:
function setCursorPos(input, start, end) {
if (arguments.length < 3) end = start;
if ("selectionStart" in input) {
setTimeout(function() {
input.selectionStart = start;
input.selectionEnd = end;
}, 1);
}
else if (input.createTextRange) {
var rng = input.createTextRange();
rng.moveStart("character", start);
rng.collapse();
rng.moveEnd("character", end - start);
rng.select();
}
}
http://jsfiddle.net/gilly3/6SUN8/
Here is code to get line number and column position
function getLineNumber(tArea) {
return tArea.value.substr(0, tArea.selectionStart).split("\n").length;
}
function getCursorPos() {
var me = $("textarea[name='documenttext']")[0];
var el = $(me).get(0);
var pos = 0;
if ('selectionStart' in el) {
pos = el.selectionStart;
} else if ('selection' in document) {
el.focus();
var Sel = document.selection.createRange();
var SelLength = document.selection.createRange().text.length;
Sel.moveStart('character', -el.value.length);
pos = Sel.text.length - SelLength;
}
var ret = pos - prevLine(me);
alert(ret);
return ret;
}
function prevLine(me) {
var lineArr = me.value.substr(0, me.selectionStart).split("\n");
var numChars = 0;
for (var i = 0; i < lineArr.length-1; i++) {
numChars += lineArr[i].length+1;
}
return numChars;
}
tArea is the text area DOM element

Categories

Resources