Clone/Mimic selection range - javascript

Selection does exactly what I need: Set a selection while the TTS reads the part, highlight the next part, and so on... the kicker is: The user must be able to change the selection color. I can set this in css through ::selection, but I'm unable to change it while the extension is running. The selection color also blends with the word highlighting color within, changing z-index in css doesn't change this blending.
What it should look like:
To circumvent this, I have written following code which sets begin and end of the range to mimic this behaviour but with full (color) control, although when applying the style to the parentElement of every node, in some cases, it will apply to a div or p, highlighting a whole portion of the text instead of the sentence:
(Don't mind the sentence algorithm, it's of no importance right now)
//Start a range for the highlighting
let range = rangy.createRange();
//Set the range on the first element with an offSet of 0
range.setStart(document.getElementById('ali' + i), 0);
while (
document.getElementById('ali' + i) !== null) {
let element = document.getElementById('ali' + i);
senArray.push(element.innerText);
wordIdArray.push(i);
if (
element.innerText.indexOf('.') >= 0
|| element.innerText.indexOf('?') >= 0
|| element.innerText.indexOf('!') >= 0
) {
break;
}
i++;
}
//Set the end of the range by getting the last element and its last offSet
let endOffset = document.getElementById('ali' + i).childNodes.length;
range.setEnd(document.getElementById('ali' + i), endOffset);
console.log(range);
//get nodes from range and apply style to parent element
let nodes = range.getNodes();
nodes.forEach(function (el) {
el.parentNode("style", "background-color: " + readingUnitColor + ";")
});
//Make selection object, remove existing ranges (to be sure) and add the range to the selection object.
// let selection = window.getSelection();
// selection.removeAllRanges();
// selection.addRange(range);
I'm using rangy here, but I had the same results with native JS by using:
let CAContainer = range.commonAncestorContainer;
CAContainer.setAttribute("style", "background-color: " + readingUnitColor + ";");
I also tried wrapping the sentences in a span, but whenever I add '<span id="id">' to the setStart part of the range, chrome will auto-close that tag immediately after the first node, regardless of '</span>' being at the setEnd of the range.
Any help or input will be kindly appreciated !

Related

Random Kerning in text area (input) for each letter

I'm all new to this, but after spending a week trying to find an answer, I thought I would try asking directly.
I am building a text editor using javascript and jquery. I have a textarea (with contenteditable), a stylesheet and a js script. What I want is that for each letter pressed, the kerning will be random. I achieved that with a simple function, but I don't want ALL textarea text to have this kerning, only the last letter pressed and so on and so on, so this type of thing would be the result:
simulation
There is what I have so far in my js file:
$(document).ready(
function() {
$('#textarea').keypress(function(){
var KerningRandom = Math.floor((Math.random()*90)-20);
$(this).css('letter-spacing',KerningRandom);
});
Here is my jsfiddle that actually doesn't work in jsfiddle and I don't get why as it works fine in local...?
Thanks!
You cannot address individual characters ( and so glyphs ) in CSS. Only ::first-letter.
Options you have:
convert all characters to individual spans. That's too much I think.
use <canvas> to render text and so to implement text flow layout from scratch.
You can find a working plunker of what you want to achieve there (I forked yours).
https://jsfiddle.net/1gesLgsa/2/
Full code :
//Code from https://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
//Namespace management idea from http://enterprisejquery.com/2010/10/how-good-c-habits-can-encourage-bad-javascript-habits-part-1/
(function( cursorManager ) {
//From: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
var voidNodeTags = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR', 'BASEFONT', 'BGSOUND', 'FRAME', 'ISINDEX'];
//From: https://stackoverflow.com/questions/237104/array-containsobj-in-javascript
Array.prototype.contains = function(obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
}
//Basic idea from: https://stackoverflow.com/questions/19790442/test-if-an-element-can-contain-text
function canContainText(node) {
if(node.nodeType == 1) { //is an element node
return !voidNodeTags.contains(node.nodeName);
} else { //is not an element node
return false;
}
};
function getLastChildElement(el){
var lc = el.lastChild;
while(lc && lc.nodeType != 1) {
if(lc.previousSibling)
lc = lc.previousSibling;
else
break;
}
return lc;
}
//Based on Nico Burns's answer
cursorManager.setEndOfContenteditable = function(contentEditableElement)
{
while(getLastChildElement(contentEditableElement) &&
canContainText(getLastChildElement(contentEditableElement))) {
contentEditableElement = getLastChildElement(contentEditableElement);
}
var range,selection;
if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange();//Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
range.select();//Select the range (make it the visible selection
}
}
}( window.cursorManager = window.cursorManager || {}));
// ACTUAL CODE MADE FOR THIS ANSWER
$('#textarea').keypress(function(event) {
event.preventDefault();
var KerningRandom = Math.floor((Math.random() * 90));
if ($("#last").length > 0)
{
var previousLast = $("#textarea #last").html();
$("#textarea #last").remove();
}
else
var previousLast = "";
$("#textarea").html($("#textarea").html().slice() + previousLast + "<span id='last'>" + String.fromCharCode(event.which) + "</span>")
$("#last").css('margin-left', KerningRandom + "px");
var editableDiv = document.getElementById("textarea");
cursorManager.setEndOfContenteditable(editableDiv)
});
var editableDiv = document.getElementById("textarea");
cursorManager.setEndOfContenteditable(editableDiv)
Point by point explanation :
$('#textarea').keypress(function(event) {
event.preventDefault();
var KerningRandom = Math.floor((Math.random() * 90));
if ($("#last").length > 0)
{
var previousLast = $("#textarea #last").html();
$("#textarea #last").remove();
}
else
var previousLast = "";
$("#textarea").html($("#textarea").html() + previousLast + "<span id='last'>" + String.fromCharCode(event.which) + "</span>")
$("#last").css('margin-left', KerningRandom + "px");
var editableDiv = document.getElementById("textarea");
cursorManager.setEndOfContenteditable(editableDiv)
});
The event.preventDefault() prevent the letter to be added when pressing a key.
Then, we calculate our left margin value, save the previous last letter we had and remove the span that contains the last letter as it's not the last letter anymore.
We append the previous last letter , and the span that has a random left margin (to simulate the kerning) and the value of the pressed key (thanks to
How to find out what character key is pressed?) to the actual content.
After that, we needed to move the carret at the end of the textarea manually, because it would stay at the beginning otherwise.
For that, I used the code from
How to move cursor to end of contenteditable entity so goes there for explanation.

Firing an event when the caret gets within a particular div/span/a tag and also, when the caret leaves the tag

The idea is this -
There is a contenteditable element with some text in it. Am trying to build out a tagging mechanism (kind of like twitter's people tagging when you type '#'). Whenever a user types '#', it shows up a popover with suggestions and filters when they continue typing. Until here it's easy and I have got it figured out. The problem comes when I need to show the popover if/only if the caret is over the element containing the tag.
<div contenteditable="">
<p>Some random text before
<a href="javascript:;"
class="name-suggest"
style="color:inherit !important;text-decoration:inherit !important">#samadams</a>
Some random text after</p>
</div>
Now, whenever the user moves the caret over the a tag / clicks on it, I want to trigger an event that shows the popover, and remove it whenever the caret leaves the a tag. (kind of like focus / blur but they don't seem to work). onmousedown works but there is no way to tell if the cursor has been moved into the anchor tag with the keyboard.
Also, am doing this in angularjs, so, any solution targeted towards that would be preferable but not necessary.
Have been trying to get this to work for a day and any help is greatly appreciated.
This will let you know when your caret position is in an anchor node containing an #
$('#content').on('mouseup keydown keyup', function (event) {
var sel = getSelection();
if (sel.type === "Caret") {
var anchorNodeVal = sel.anchorNode.nodeValue;
if ( anchorNodeVal.indexOf('#') >= 0) {
$('#pop').show()
} else {
$('#pop').hide()
}
}
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="content" contenteditable="">
<p>Some random text before
<a href="javascript:;"
class="name-suggest"
style="color:inherit !important;text-decoration:inherit !important">#samadams</a>
Some random text after</p>
</div>
<div id="pop" style="display:none">Twitter node found</div>
You could add some regex to further validate the selection.
There is a weird move with RegExps and offset calculation in the code below, but let me explain why it's a better solution.
I've been building a complicated editor using contenteditable about a year ago. It wasn't just a disaster. It was a fucking disaster. There is no cover-all-the-cases spec. Browsers behave differently in every possible detail and it changes frequently. Put a caret before # char and you will get this is Gecko:
<a href="#">|#name
And this in WebKit:
|<a href="#">#name
Well, unless <a> is paragraph's first child. Then result would be the same as in Gecko. Try to put caret after the nickname and both will tell it's inside the link. Start typing, and caret will pop out the element - a year ago Gecko wasn't doing it.
I've used native Selection & Range APIs in this example, they are IE9+. You may want to use Rangy instead.
$el = $('#content');
var showTip = function (nickname) {
// ...
console.log('Show: ' + nickname);
};
var dismissTip = function () {
// ...
console.log('Hide');
};
// I'm sure there is a better RegExp for this :)
var nicknameRegexp = /(^|\b|\s)\#(\w+)(\s|\b|$)/g;
var trackSelection = function () {
var selection = window.getSelection(),
range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
if (range == null || $el[0].contains(range.commonAncestorContainer) == false) {
return dismissTip();
}
var comparer = range.cloneRange();
comparer.setStart($el[0], 0);
var offset = comparer.toString().length;
var match, from, to;
while (match = nicknameRegexp.exec($el[0].textContent)) {
from = match.index + match[1].length;
to = match.index + match[1].length + match[2].length + 1;
if (offset >= from && offset <= to) {
// Force rewind, otherwise next time result might be incorrect
nicknameRegexp.lastIndex = 0;
return showTip(match[2]);
}
}
return dismissTip();
};
$el.on({
// `mousedown` can happen outside #content
'mousedown': function (e) {
$(document).one('mouseup', function (e) {
// Calling function without a tiny delay will lead to a wrong selection info
setTimeout(trackSelection, 5);
});
},
'keyup': trackSelection
});
Just looked at Fire event when caret enters span element which led me here, pretending your case was quite similar except finding if current word is specifically beginning with # for the modal to show...
The thing you need is a way to get the word we're on at the moment we move or type, then check the first character and hide/show the modal pane accordingly will be pretty easy.
function getSelectedWord(grab=document.getSelection()) {
var i = grab.focusOffset, node = grab.focusNode, // find cursor
text = node.data || node.innerText, // get focus-node text
a = text.substr(0, i), p = text.substr(i); // split on caret
return a.split(/\s/).pop() + p.split(/\s/)[0]} // cut-out at spaces
Now you can listen for keydown or selectionchange events and show your pane knowning what have already been written of the current/selected word.
editor.addEventListener('keydown', ev => {
if (ev.key.substr(0, 5) != 'Arrow') // react when we move caret or
if (ev.key != '#') return; // react when we type an '#' or quit
var word = getSelectedWord(); // <-- checking value
if (word[0] == '#') showModal(word.substr(1)); // pass without '#'
});
Note that social networks and code completion usually stops at caret position while I did check for word tail... You can go usual by removing p off of getSelectedWord function definition if desired.
Hope this still helps; Happy coding ! ;)

How can I best make an inline span draggable within a paragraph of text?

I have a paragraph of text in which the user may place a "pin" to mark a position. Once a pin has been placed, I would like to allow the user to move its position by dragging it to a new location in the paragraph. This is simple to do with block elements, but I have yet to see a good way to do it with inline elements. How might I accomplish this?
I have already implemented it using window.selection as a way to find the cursor's location in the paragraph, but it is not as smooth as I would like.
As a note, I am using the Rangy library to wrap the native Range and Selection functionality, but it works the same way as the native functions do.
Here is the code:
$(document).on("mousedown", '.pin', function () {
//define what a pin is
var el = document.createElement("span");
el.className = "pin";
el.id = "test";
//make it contain an empty space so we can color it
el.appendChild(document.createTextNode("d"));
$(document).on("mousemove", function () {
//get the current selection
var selection = rangy.getSelection();
//collapse the selection to either the front
//or back, since we do not want the user to see it.
if (selection.isBackwards()) {
selection.collapseToStart();
} else {
selection.collapseToEnd();
}
//remove the old pin
$('.pin').remove();
//place the new pin at the current selection
selection.getAllRanges()[0].insertNode(el);
});
//remove the handler when the user has stopped dragging it
$(document).on("mouseup", function () {
$(document).off("mousemove");
});
});
And here is a working demo: http://jsfiddle.net/j1LLmr5b/22/ .
As you can see, it works(usually), but the user can see the selection being made. Have any ideas on how to move the span without showing the selection highlight? I will also accept an alternate method that does not use the selection at all. The goal is to allow movement of the span as cleanly as possible.
You can do this using ranges instead using code similar to this answer. Unfortunately the code is a bit longer than ideal because IE hasn't yet implemented document.caretPositionFromPoint(). However, the old proprietary TextRange object, still present in IE 11, comes to the rescue.
Here's a demo:
http://jsfiddle.net/j1LLmr5b/26/
Here's the relevant code:
var range, textRange, x = e.clientX, y = e.clientY;
//remove the old pin
$('.pin').remove();
// Try the standards-based way first
if (document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
range.collapse();
}
// Next, the WebKit way
else if (document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
}
// Finally, the IE way
else if (document.body.createTextRange) {
textRange = document.body.createTextRange();
textRange.moveToPoint(x, y);
var spanId = "temp_" + ("" + Math.random()).slice(2);
textRange.pasteHTML('<span id="' + spanId + '"> </span>');
var span = document.getElementById(spanId);
//place the new pin
span.parentNode.replaceChild(el, span);
}
if (range) {
//place the new pin
range.insertNode(el);
}
Try this my friend
el.appendChild(document.createTextNode("d"));
You have create empty span tag that's why you found empty.
add after
el.id = "test";
this
var value = $('.pin').text();
$(el).text(value);
You can hide selection with css
::selection {color:red;background:yellow;}
::-moz-selection {color:red;background:yellow;}
that's all how i can help for a now

Position changes every time with window.getSelection()

Can anyone help me? I got these codes here https://stackoverflow.com/a/17836828/2338164
$(document).on("mouseup",".wrap",function(){
var highlight = window.getSelection();
if(highlight.toString().length>=1){
var spn = '<span class="highlight">' + highlight + '</span>';
var text = $(this).text();
var range = highlight.getRangeAt(0);
var startText = text.substring(0, range.startOffset);
var endText = text.substring(range.endOffset, text.length);
$('#q3txt').append(range.startOffset+"<br>");
$(this).html(startText + spn + endText);
}
});
I tried to use it and it's working fine, until you highlight again...
Here's a link http://jsfiddle.net/AN76g/.
What im trying to do is... user will highlight a block then wrap it in span, but if the user made a mistake and tries to highlight again, the span is removed and will try to wrap the new highlighted text. But either the position changes or parts of the text are being appended.
See this update: jsfiddle.
First, on the mousedown, you can unwrap the span as so:
$(document).on("mousedown",".wrap",function(){
$('.highlight').contents().unwrap();
});
Secondly, the problem with using the range.startOffset and range.endOffset is that you the start is relative to the containing element which could be the highlight span which causes you to replace the incorrect text on subsequent selections. Instead, replace the selection with the span as so:
$(document).on("mouseup",".wrap",function(){
var highlight = window.getSelection();
if(highlight.toString().length>=1){
var range = highlight.getRangeAt(0);
var selectionContents = range.extractContents();
var spn = document.createElement("span");
spn.className='highlight';
spn.appendChild(selectionContents);
range.insertNode(spn);
highlight.removeAllRanges();
}
});
Information from MDN Range.startOffset, specifically:
the startContainer is a Node of type Text, Comment, or CDATASection, then the offset is the number of characters from the start of the startContainer to the boundary point of the Range. For other Node types, the startOffset is the number of child nodes between the start of the startContainer and the boundary point of the Range.
Also, this answer.

html - selection range - getting the range + starting node + ending node + distance

From my previous question for selecting specific html text, I have gone through this link to understand range in html string.
For selecting a specific text on html page. We need to follow this steps.
Assumed HTML:
<h4 id="entry1196"><a
href="http://radar.oreilly.com/archives/2007/03/call_for_a_blog_1.html"
class="external">Call for a Blogger's Code of Conduct</a></h4>
<p>Tim O'Reilly calls for a Blogger Code of Conduct. His proposals are:</p>
<ol>
<li>Take responsibility not just for your own words, but for the
comments you allow on your blog.</li>
<li>Label your tolerance level for abusive comments.</li>
<li>Consider eliminating anonymous comments.</li>
</ol>
java script to make selection by range
var range = document.createRange(); // create range
var startPar = [the p node]; // starting parameter
var endLi = [the second li node]; // ending parameter
range.setStart(startPar,13); // distance from starting parameter.
range.setEnd(endLi,17); // distance from ending parameter
range.select(); // this statement will make selection
I want to do this in invert way. I mean, assume that selection is done by user on browser (safari). My question is that How can we get starting node (as we have 'the p node' here) and ending node (as we have 'the second li node' here) and the range as well (as we have 13,17 here)?
Edit : my efforts (From this question)
var sel = window.getSelection();
if (sel.rangeCount < 1) {
return;
}
var range = sel.getRangeAt(0);
var startNode = range.startContainer, endNode = range.endContainer;
// Split the start and end container text nodes, if necessary
if (endNode.nodeType == 3) {
endNode.splitText(range.endOffset);
range.setEnd(endNode, endNode.length);
}
if (startNode.nodeType == 3) {
startNode = startNode.splitText(range.startOffset);
range.setStart(startNode, 0);
}
But, yet I am confused about getting like, if selected is first paragraph or second or third, or selected is in first heading or second heading or what....
Storing the selected range is simple. The following will return only the first selected range (Firefox at least supports multiple selections):
<script type="text/javascript">
function getSelectionRange() {
var sel;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
return sel.getRangeAt(0);
}
} else if (document.selection) {
return document.selection.createRange();
}
return null;
}
var range;
</script>
<input type="button" onclick="range = getSelectionRange();"
value="Store selection">
range will have properties startContainer (the node containing the start of the range), startOffset (an offset within the start container node: a character offset in the case of text nodes and child offset in elements), endContainer and endOffset (equivalent behvaiour to the start properties). Range is well documented by its specification and MDC.
In IE, range will contain a TextRange, which works very differently. Rather than nodes and offsets, TextRanges are concerned with characters, words and sentences. Microsoft's site has some documentation: http://msdn.microsoft.com/en-us/library/ms533042%28VS.85%29.aspx, http://msdn.microsoft.com/en-us/library/ms535872%28VS.85%29.aspx.

Categories