This is specifically for the developers of CKEditor.
The goal is to know when the cursor is either inside or outside a span of custom atttributes.
When using the Ckeditor, if inserted a custom span like below from a plug-in, occassionally when you stop typing, un focus the textarea and replace your cusor to the end of the line - as though you are continuing typing where you had left off. The text can either be inside the span or outside with no encapsulation.
Example before unfocus:
<span style="color:blue;line-height:12px;font-size:10px;font-family:arial;" class="master-span">
<span style="color:red;line-height:20px;font-size:18px;font-family:museo;" class="child-span">Text is here</span>
</span>
Re-Focus and positioning cusor on screen at the end of "here" and start typing.
<span style="color:blue;line-height:12px;font-size:10px;font-family:arial;" class="master-span">
<span style="color:red;line-height:20px;font-size:18px;font-family:museo;" class="child-span">Text is here</span> Text is now outside!!!
</span>
What I'm asking is that where would I target my troubleshoot to know when the cursor/anchor is within the span or outside of it i.e. Is there a calculation being made in the code to determine this?
The reason for this enquiry is that the span formmating is extremely important and we can't use a master span in the surrounding textarea as this can be altered by the user. We find also, that using bulletpoints are troublesome due to the browsers inability to set line-height so ensuring text is always within the span would be a major help.
P.S I've downloaded the source code to see where I could find it.
Many Thanks for any help.
Upon clicking of the text area of any of my CKeditor instances I looked for the parent of the cursor to see if it was an LI node. This indicating that the cursor had landed outside of my span within a bullet point. I proceeded to find the most outside Text Node I could find as my assumption is that if the cursor was outside the span thus the user wanted to continue typing from the last character.
In my setup, within a span there can be a number of multiple sub spans with no limitation of number. So I had to find the most outside span that had a text node at the end (or close to the end).
I did by getting the Node List and reversing the order to find the first text node within the reverse order. I created a function that could loop the number of span and break upon finding the first text node.
Once I had my text node I then set up ranges and calculated the length of the text in order to place it at the end. Below is my final solution which works on latest Chrome/FF/IE.
$("#preview-text-3827").on("click", function(){
var CKEDITOR = window.parent.CKEDITOR;
var ck_instance_name = false;
for ( var ck_instance in CKEDITOR.instances ){
if (CKEDITOR.instances[ck_instance].focusManager.hasFocus){
ck_instance_name = ck_instance;
break;
}
}
var editor = CKEDITOR.instances[ck_instance_name];
var selection = editor.getSelection();
var parent_attrs = "";
if (selection.getType() == CKEDITOR.SELECTION_TEXT) {
parent_attrs = selection.getStartElement();
if(parent_attrs.getName() == "li"){
var nodeList = parent_attrs.getChildren();
console.log( "Now Reverse" );
for ( var i = nodeList.count() - 1; i > -1; --i ) {
//console.log( nodeList.getItem( i).nodeName );
var el = nodeList.getItem( i);
if(el.$.nodeName == "#text"){
// This is the last text but it's inside the li which is WRONG
}
if(el.$.nodeName == "SPAN"){
// This should be the last span
// Jump into this span and find the children.
var result = retractLiPosition(el);
if(result.text == true){
var inner = result.result;
var text = inner.$.textContent || inner.$.innerText;
var length = text.length;
var range = editor.createRange();
range.setStart( inner , length);
range.setEnd( inner, length);
editor.getSelection().selectRanges( [ range ] );
break;
}
}
}
}
}
});
function retractLiPosition(element){
var returning = new Object();
returning.result = element;
returning.text = false;
var result = element;
var nodeList = element.getChildren();
for ( var i = nodeList.count() - 1; i > -1; --i ) {
var el = nodeList.getItem( i);
if(el.$.nodeName == "#text"){
returning.result = el;
returning.text = true;
break;
}
if(el.$.nodeName == "SPAN"){
// This should be the last span
// Jump into this span and find the children.
returning = retractLiPosition(el);
}
}
return returning;
}
});
Related
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 ! ;)
My end goal is for the user to be able to :
select text from a paragraph
wrap the text in a span
place an action button / div at the end of the selection that they could click to take further action
Here is my code so far:
function Discussion(){
var $this = this;
$(document).bind("mouseup",function(){
$this.selectText($this);
});
}
Discussion.prototype.selectText = function($this){
var selection = $this.getSelectedText();
if(selection.length > 3){
console.log(selection);
var spn = '<span class="selected">' + selection + '</span>';
$(this).html($(this).html().replace(selection, spn));
//ERROR here; it says that it can't replace() on undefined $(this).html()
}
}
Discussion.prototype.getSelectedText = function(){
if(window.getSelection){
return window.getSelection().toString();
}
else if(document.getSelection){
return document.getSelection();
}
else if(document.selection){
return document.selection.createRange().text;
}
}
As you might expect, so far I can get the text of the selection with window.getSelection().toString(). If I remove the .toString(), I get a Selection object. With this I can use window.getSelection().anchorNode.parentNode to get most of the information I need.
I also see that Selection.anchorOffset and Selection.extentOffset will give me the range of characters I've selected. Is there a way I can use this information to place the span?
I guess my questions would be:
Why isn't this code wrapping the selection in divs?
Will this fail with multiple instances of the same text?
After wrapping the selection with a span or inline-block div or something, will I be able to get (and use) its position for positioning an additional button / div?
Phew, thanks for your time!
Edit: I added a JS fiddle here : http://jsfiddle.net/nn62G/
I'm going to post my js fiddle here with the solution for posterity. Thanks for all the help, everyone!
http://jsfiddle.net/prodikl/mP8KT/
function Discussion(){
var $this = this;
$(document).bind("mouseup",function(){
$this.selectText($this);
});
}
Discussion.prototype.selectText = function($this){
$("mark").contents().unwrap();
$("mark").remove();
var selection = $this.getSelection();
var range = selection.getRangeAt(0);
var cssclass = $(selection.anchorNode.parentNode).attr("class");
if(selection.toString().length > 2){
$this.startPoint = selection.anchorOffset;
$this.endPoint = selection.extentOffset;
var newNode = document.createElement("mark");
range.surroundContents(newNode);
$("mark").attr('title', ' ');
$("mark").tooltip({
content : "<a class='content' href='http://www.google.com' target='_blank'>Here's a link.</a>"
});
$("mark").on('mouseleave',function(event){
event.stopImmediatePropagation();
}).tooltip("open");
}
}
Discussion.prototype.getSelection = function(){
if(window.getSelection){
return window.getSelection();
}
else if(document.getSelection){
return document.getSelection();
}
else if(document.selection){
return document.selection.createRange().text;
}
}
var discussion = new Discussion();
The value returned by getSelectedText is a selection not an element in the DOM. That is why your cal to the html function is failing, selection has no such property.
You can do what you want as follows, set the part of the document you want to process as contentEditable for example a <p> or <div>. When you select within that area then you can get the HTML element using document.activeElement. If you select outside a contentEdiatble area then activeElement returns the body element. The activeElement you CAN process :
document.activeElement.innerHTML =
document.activeElement.innerHTML.replace (selection, spn);
However, if you make parts of your document editable other things happen which you may not want. More seriously, if the selected text occurs multiple times you cannot determine which one has actually been selected so this may not be a solution to your problem.
Updated fiddle here
Ok well, a possible solution could be to find the element that contains the current selection and replace it's html something like:
// this get's you all the elements (including the top parent) that contain the selection
var existsInElements = $('*:contains("' + selection + '")');
// the exact element match is in the last one
var exactElement = existsInElements[existsInElements.length - 1];
Here is a fiddle that works.
I am using the highlighter module available in Rangy, and it work great in creating a highlight for the text that is selected.
In terms of changes to the html, the selected text is replaced by a span tag like the following for example:
the selected text is <span class="highlight">replaced by a span tag</span> like the
What I want to do is get a reference to the span element once it has been created so I can do some other stuff with it. How can this be done?
Please note there may be other spans with or without the highlight tag elsewhere, so these cannot be used to find it.
The important part of the code I have to create the highlight for the selected text is:
var highlighter = null;
var cssApplier = null;
rangy.init();
cssApplier = rangy.createCssClassApplier("highlight", { normalize: true });
highlighter = rangy.createHighlighter(document, "TextRange");
highlighter.addClassApplier(cssApplier);
var selection = rangy.getSelection();
highlighter.highlightSelection("highlight", selection);
I was waiting for #TimDown to update his answer with working code. But as he hasn't done that then I will post some myself (which is based on his answer).
The following function will return an array of highlight elements that have been creating, assuming the selection is still valid:
function GetAllCreatedElements(selection) {
var nodes = selection.getRangeAt(0).getNodes(false, function (el) {
return el.parentNode && el.parentNode.className == "highlight";
});
var spans = [];
for (var i = 0; i < nodes.length; i++) {
spans.push(nodes[i].parentNode);
}
return spans;
}
There is no guarantee that only one <span> element will be created: if the selection crosses element boundaries, several spans could be created.
Anyway, since the selection is preserved, you could use the getNodes() method of the selection range to get the spans:
var spans = selection.getRangeAt(0).getNodes([1], function(el) {
return el.tagName == "SPAN" && el.className == "highlight";
});
I have a highlighter function that formats the matched words to an anchor with yellow bg-color and I need a function to remove the anchor elements for the next search.
The markup of a matched word, for the first one looks like this:
<a id="searchword1" class="searchword" style="background-color: yellow; text-decoration: none; color: black;">my text</a>
I need to remove the anchor but leave my text there. There are other anchors in my document that I dont want to interfere with. I need to do this in pure Javascript (no jQuery).
An addational requirement: Don't create new lines after tag removal, leave it as it was.
Thanks to enhzflep, the code until now:
for (z=0;z<szam;z++){
var removal = parent.frames['pagina'].document.getElementById("searchword"+z);
var highlightedText = removal.innerHTML.toLowerCase;
removeh(removal,highlightedText,doc);
}
function removeh(node,high,doc) {
doc = typeof(doc) != 'undefined' ? doc : document;
if (node.hasChildNodes) {
var hi_cn;
for (hi_cn=0;hi_cn<node.childNodes.length;hi_cn++) {
removeh(node.childNodes[hi_cn],high,doc);
}
}
//1. Get element containing text
if (node.nodeType == 3) {
tempnode = node.nodeValue.toLowerCase();
if (tempnode.indexOf(high) != -1) {
nv = node.nodeValue;
nv = node.nodeValue;
ni = tempnode.indexOf(high);
//2. Get the text it contains
before = doc.createTextNode(nv.substr(0,ni));
dochighVal = nv.substr(ni,high.length);
after = doc.createTextNode(nv.substr(ni+high.length));
//3. Get the highlighted element's parent
var daddy = node.parentNode;
//4. Create a text node:
var newNode = document.createTextNode(dochighVal);
//5. Insert it into the document before the link
daddy.insertBefore(before, node);
daddy.insertBefore(newNode, node);
daddy.insertBefore(after, node);
//6. Remove the link element
daddy.removeChild(node);
}
}
}
Where num is the number of matched words.
For some reason this wont work, please help, I will accept the answer that solves this minor problem too.
Two answers had the method right but it is still buggy as it separates the resulting text with new lines. This makes the highlighter function to get the "my word" as separate temporary node values and won't be able to highlight a match for /my.word/.
If I understand you correctly, you wish to turn this:
<a id="searchword1" class="searchword" style="background-color: yellow; text-decoration: none; color: black;">my text</a>
into this:
my text
If that's the case, then it's very easy.
As it stands, it looks like you're asking for an child of the element you showed (the element doesn't have any children, other than the text-node. I expect your script is hosed by line 2 - when it tries to get a non-existent child)
//1. Get element containing text
var element = document.getElementById('searchWord1');
//2. Get the text it contains
var highlightedText = element.innerHTML;
//3. Get the highlighted element's parent
var parent = element.parentNode;
//4. Create a text node:
var newNode = document.createTextNode(highlightedText);
//5. Insert it into the document before the link
parent.insertBefore(newNode, element);
//6. Remove the link element
parent.removeChild(element);
If you are using jQuery it will be simple DEMO
$('#searchword1').contents().unwrap();
But if you only want to use js for this there is solution by user113716 on the Link
DEMO
var b= document.getElementsByClassName('searchword');
while(b.length) {
var parent = b[ 0 ].parentNode;
while( b[ 0 ].firstChild ) {
parent.insertBefore( b[ 0 ].firstChild, b[ 0 ] );
}
parent.removeChild( b[ 0 ] );
}
I have an HTML-document:
<html>
<body>
<p>
A funny joke:
<ul>
<li>do you know why six is afraid of seven?
<li>because seven ate nine.
</ul>
Oh, so funny!
</p>
</body>
</html>
Now I want to identify the first occurence of "seven" and tag it with
<span id="link1" class="link">
How can this be accomplished?
Do you have to parse the DOM-tree or is it possible to get the whole code within the body-section and then search for the word?
In both cases, after I found the word somewhere, how do you identify it and change it's DOM-parent to span (I guess that's what has to be done) and then add the mentioned attributes?
It's not so much a code I would expect, but what methods or concepts will do the job.
And I am not so much intersted in a framework-solution but in a pure javascript way.
You need to find a DOM node with type TEXT_NODE (3) and containig your expected word. When you need to split a that node into three ones.
First is a TEXT_NODE which contains a text before the word you search, second one is a SPAN node containing the word you search, and third one is another TEXT_NODE containing an original node's tail (all after searched word).
Here is a source code...
<html>
<head>
<style type="text/css">
.link {
color: red;
}
</style>
</head>
<body>
<p>
A funny joke:
<ul>
<li>do you know why six is afraid of seven?
<li>because seven ate nine.
</ul>
Oh, so funny!
</p>
<script type="text/javascript">
function search(where, what) {
var children = where.childNodes;
for(var i = 0, l = children.length; i < l; i++) {
var child = children[i], pos;
if(child.nodeType == 3 && (pos = child.nodeValue.indexOf(what)) != -1) { // a TEXT_NODE
var value = child.nodeValue;
var parent = child.parentNode;
var start = value.substring(0, pos);
var end = value.substring(pos + what.length);
var span = document.createElement('span');
child.nodeValue = start;
span.className = 'link';
span.id = 'link1';
span.innerHTML = what;
parent.appendChild(span);
parent.appendChild(document.createTextNode(end));
return true;
} else
if(search(child, what))
break;
}
return false;
}
search(document.getElementsByTagName('body')[0], 'seven');
</script>
</body>
</html>
This is a function I’ve written a few years ago that searches for specific text, and highlights them (puts the hits in a span with a specific class name).
It walks the DOM tree, examining the text content. Whenever it finds a text node containing the looked-for text, it will replace that text node by three new nodes:
one text node with the text preceding the match,
one (newly created) span element containing the matching text,
and one text node with the text following the match.
This is the function as I have it. It’s part of a larger script file, but it should run independently as well. (I’ve commented out a call to ensureElementVisible which made the element visible, since the script also had folding and expanding capabilities).
It does one (other) thing that you probably won’t need: it turns the search text into a regular expression matching any of the multiple words.
function findText(a_text, a_top) {
// Go through *all* elements below a_top (if a_top is undefined, then use the body)
// and check the textContent or innerText (only if it has textual content!)
var rexPhrase = new RegExp(a_text.replace(/([\\\/\*\?\+\.\[\]\{\}\(\)\|\^\$])/g, '\\$1').replace(/\W+/g, '\\W*')
, 'gi');
var terms = [];
var rexSearchTokens = /[\w]+/g;
var match;
while(match = rexSearchTokens.exec(a_text)) {
terms.push(match[0]);
}
var rexTerm = new RegExp('\\b(' + terms.join('|') + ')', 'gi');
var hits = [];
walkDOMTree(a_top || document.body,
function search(a_element) {
if (a_element.nodeName === '#text') {
if(rexPhrase.test(a_element.nodeValue)) {
// ensureElementVisible(a_element, true);
hits.push(a_element);
}
}
});
// highlight the search terms in the found elements
for(var i = 0; i < hits.length; i++) {
var hit = hits[i];
var parent = hit.parentNode;
if (parent.childNodes.length === 1) {
// Remove the element from the hit list
hits.splice(i, 1);
var text = hit.nodeValue;
parent.removeChild(hit);
var match, prevLastIndex = 0;
while(match = rexTerm.exec(text)) {
parent.appendChild(document.createTextNode(text.substr(prevLastIndex, match.index - prevLastIndex)));
var highlightedTerm = parent.appendChild(document.createElement('SPAN'));
highlightedTerm.className = 'search-hit';
highlightedTerm.appendChild(document.createTextNode(match[0]));
prevLastIndex = match.index + match[0].length;
// Insert the newly added element into the hit list
hits.splice(i, 0, highlightedTerm);
i++;
}
parent.appendChild(document.createTextNode(text.substr(prevLastIndex)));
// Account for the removal of the original hit node
i--;
}
}
return hits;
}
I found the following so question:
Find text string using jQuery?
This appears to be close to what you're trying to do. Now are you attempting to wrap just the text "seven" or are you attempting to wrap the entire content of the <li>?