Add HTML element after text - javascript

I am looking for a way to add an HTML element using JavaScript. But the problem is that the new element might be in between some text. In all other cases I'm using the insertBefore() method.
I am using the following function to get the cursor position.
My initial approach was to split the target innerHTML and add the necessary tags but the cursor position provided does not take into account the character conversions such as space to . So if there are multiple continous spaces, the cursor position will not give the coreect position int he innerHTML.
function getCursorPos()
{
var cursorPos=-1;
if (window.getSelection)
{
var selObj = window.getSelection();
var selRange = selObj.getRangeAt(0);
cursorPos = findNode(selObj.anchorNode.parentNode.childNodes,
selObj.anchorNode) + selObj.anchorOffset;
/* FIXME the following works wrong in Opera when the document is longer than 32767 chars */
}
else if (document.selection)
{
var range = document.selection.createRange();
var bookmark = range.getBookmark();
/* FIXME the following works wrong when the document is longer than 65535 chars */
cursorPos = bookmark.charCodeAt(2) - 11; /* Undocumented function [3] */
}
return cursorPos;
}
function findNode(list, node)
{
for (var i = 0; i < list.length; i++)
{
if (list[i] == node)
{
return i;
}
}
return -1;
}
Is there any other method to do this?
The new element may be in the middle of the HTML ie, it may not be always at the end.
Thank You

You can do this:
var text = $("#container").html();//the target element has the id of container
Process text now, that is break up text the way you want or whatever you want to do with it and add html elements at the relevant position in this text using string addition.
Then do this ...
$("#container").html(text);
You can also take Keith's approach. It's important to realize what he said. Another way to look into it would be inserting a dom element (e.g., html tags) inside text may not be possible with insertAfter or insertBefore.

Here's part of the method I used. It works but I dont know if there are better ways.
function getCursorNode()
{
if (window.getSelection)
{
var selObj = window.getSelection();
return selObj.anchorNode;
}
}
function splitTextNode(pos)
{
selNode=getCursorNode();
if(selNode.nodeName=="#text")
{
var value=selNode.nodeValue;
if(value.length == pos)
{
return ({"node":selNode,"txt":value });
}
else if(pos==0)
{
return ({"node":selNode,"txt":""});
}
else
{
var splittxt1=value.slice(0,pos)
var tempsplit1=document.createTextNode(splittxt1);
var splittxt2=value.slice(pos)
var tempsplit2=document.createTextNode(splittxt2);
myInsertAfterMany([tempsplit1,tempsplit2],selNode);
child.parentNode.removeChild(selNode);
return ({"node":tempsplit1,"first":false,"txt":splittxt1});
}
}
}
But, it(getCursorNode) does not work in IE.
Now, I append the required node after "node" after checking for "first"

Related

Remove Text from Element Without Removing Reference

I would like to replace all the text in some element (including text in children) with some other text. For example, the html
<div id="myText">
This is some text.
This is some other text.
<p id="toHide">
This is even more text.
Click this text to hide it.
</p>
</div>
should become
<div id="myText">
That is some text.
That is some other text.
<p id="toHide">
That is even more text.
Click That text to hide it.
</p>
</div>
Essentially, I've replaced all of /this/gi with "That". However, I cannot use the following:
$("#myText").innerHTML = $("#myText").innerHTML.replace(/this/gi, "");
This is because I keep a lot of references to the children of myText. This references will be erased. I realize that in simple cases, I can just update these references, but I have a fairly large file, and many references (and it would be troublesome and error prone to have to update every reference every time this function is called).
I also store some data not visible to innerHTML. For example, I use
$("#toHide").test = "test";
This is lost when writing to innerHTML.
How can I replace text in a div without innerHTML (preferably without jquery)?
Jsfiddle http://jsfiddle.net/prankol57/ZEfM7/
Here's a solution:
var n, walker = document.createTreeWalker(document.getElementById("myText"), NodeFilter.SHOW_TEXT);
while (n = walker.nextNode()) {
n.nodeValue = n.nodeValue.replace(/this/ig, "that");
}
Basically, walk all the text nodes, and substitute their values.
For better compatibility, here's some reusable code:
function visitTextNodes(el, callback) {
if (el.nodeType === 3) {
callback(el);
}
for (var i=0; i < el.childNodes.length; ++i) {
visitTextNodes(el.childNodes[i], callback);
}
}
Then you can do:
visitTextNodes(document.getElementById("myText"), function(el) {
el.nodeValue = el.nodeValue.replace(/this/ig, "that");
});
You can use DOM methods (a.k.a. the old and safe way)
function replaceText(el, pattern, txt) {
for(var i=0; i<el.childNodes.length; ++i) {
var node = el.childNodes[i];
switch(node.nodeType){
case 1: // Element
replaceText(node, pattern, txt); continue;
case 3: // Text node
node.nodeValue = node.nodeValue.replace(/this/gi, "that"); continue;
}
}
}
Demo
Here my version of replaceText:
function replaceText(elem) {
if(elem.nodeType === Node.TEXT_NODE) {
elem.nodeValue = elem.nodeValue.replace(/this/gi, 'that')
return
}
var children = elem.childNodes
for(var i = 0, len = children.length; i < len; ++i)
replaceText(children[i]);
}
NB this take an element as the first parameter and traverse all children, hence it works even with complex elements.
Here the updated fiddle: http://jsfiddle.net/ZEfM7/6/

CSS styling a single character within a word

My client has asked for the letter 4 to appear in red, wherever it is used in his website navigation.
For instance, where he has 'bikes4kids' as a menu item.
Unfortunately, I am using a 'mega menu' style plugin for his Magento site that only allows for plain text menu items - I cannot use HTML code in the menu item title box, which takes away the chance of me using <span>.
Is there a way of achieving this with JS? I assume not with CSS alone.
EDIT: The mega menu I am working with can be seen here: http://www.magentech.com/extensions/commercial-extensions/item/246-sm-mega-menu-responsive-magento-module
I did it.
Please have a look at this Link
<div class="title">menu1</div>
<div class="title">bike4kids</div>
<div class="title">menu2</div>
var avno = $(".title:nth-child(2)").text();
var avn = avno.split('4');
var item = avn[0]+"<span style='color:red'>4</span>"+avn[1];
$(".title:nth-child(2)").html(item);
No, within “plain text menu items” (as described in the question) you cannot style one character differently from others (except in a few very special cases, which do not apply here: styling the first letter, and setting the font of some characters different from others). JavaScript won’t help, because you would still need to make the character an element, and anything containing an element is by definition not plain text.
So you need to consider other approaches, like menus with items that allow some markup.
If you can process the document after it's finished loading, or sometime after magento has finished doing its thing, you can try the following. It will wrap a provided character in a span with a supplied class. A root element can be provided to limit the scope of the replace. If no root is provided, it searches the entire document.
// Simple function to convert NodeList to Array
// Not suitable for general application
function toArray(obj) {
var a = [];
for (var i=0, iLen=obj.length; i<iLen; i++) {
a[i] = obj[i];
}
return a;
}
// Highlight character c by wrapping in a span with class className
// starting with element root. If root not provided, document.body is used
function highlightChar(c, className, root) {
if (!root) root = document.body;
var frag, idx, t;
var re = new RegExp(c);
// Add tag names to ignore
var ignoreTags = {'script':'script'};
// Child nodes is a live NodeList, convert to array
// so don't have to deal with changing as nodes are added
var node, nodes = toArray(root.childNodes);
var span = document.createElement('span');
span.appendChild(document.createTextNode(c));
span.className = 'highlightChar';
for (var i=0, iLen=nodes.length; i<iLen; i++) {
node = nodes[i];
// If node is a text node and contains the chacter, highlight it
if (node.nodeType == 3 && re.test(node.data)) {
t = node.data.split(re);
frag = document.createDocumentFragment();
// Insert higlight spans after first but not after last
for (var j=0, jLen = t.length-1; j<jLen; j++) {
frag.appendChild(document.createTextNode(t[j]));
frag.appendChild(span.cloneNode(true));
}
// Append last text node
if (j > 0 && t[j]) {
frag.appendChild(document.createTextNode(t[j]));
}
// Replace the original text node with higlighted fragment
node.parentNode.replaceChild(frag, node);
// Otherwise, if node is an element, process it
} else if (node.nodeType == 1 && !(node.tagName.toLowerCase() in ignoreTags)) {
highlightChar(c, className, node);
}
}
}
It can be used to process the entire document using:
window.onload = function() {
highlightChar('4','highlightChar');
};
Edit:
Modified to find menu-items in 'mega menu'... I hope. In the demo site the "$" variable isn't jQuery so I modified the answer as well to use the jQuery function.
Testing in the demo site I found that the letter I modified did color yellow, but there was a bullet added to the left of it - apparently their css adds a bullet to the left (ie. :before) every span...
After the plugin completes its DOM modifications - simply run over the menu items and search-and-replace "4" with a colored span
eg.
// loop over all dom elements with class 'menu-item'
// - I assume here below them exist only text
jQuery('.sm-megamenu-child span').each(function() {
var $item = jQuery(this);
var text = $item.text();
var modified = text.replace(/4/g, "<span style='color:yellow'>4</span>");
$item.html(modified);
})

Broken HTML tags when using .innerHTML

As part of a larger script, I've been trying to make a page that would take a block of text from another function and "type" it out onto the screen:
function typeOut(page,nChar){
var txt = document.getElementById("text");
if (nChar<page.length){
txt.innerHTML = txt.innerHTML + page[nChar];
setTimeout(function () {typeOut(page,nChar+1);},20);
}
}
This basically works the way I want it to, but if the block of text I pass it has any html tags in it (like links), those show up as plain-text instead of being interpreted. Is there any way to get around that and force it to display the html elements correctly?
The problem is that you will create invalid HTML in the process, which the browser will try to correct. So apparently when you add < or >, it will automatically encode that character to not break the structure.
A proper solution would not work literally with every character of the text, but would process the HTML element by element. I.e. whenever you encounter an element in the source HTML, you would clone the element and add it to target element. Then you would process its text nodes character by character.
Here is a solution I hacked together (meaning, it can probably be improved a lot):
function typeOut(html, target) {
var d = document.createElement('div');
d.innerHTML = html;
var source = d.firstChild;
var i = 0;
(function process() {
if (source) {
if (source.nodeType === 3) { // process text node
if (i === 0) { // create new text node
target = target.appendChild(document.createTextNode(''));
target.nodeValue = source.nodeValue.charAt(i++);
// stop and continue to next node
} else if (i === source.nodeValue.length) {
if (source.nextSibling) {
source = source.nextSibling;
target = target.parentNode;
}
else {
source = source.parentNode.nextSibling;
target = target.parentNode.parentNode;
}
i = 0;
} else { // add to text node
target.nodeValue += source.nodeValue.charAt(i++);
}
} else if (source.nodeType === 1) { // clone element node
var clone = source.cloneNode();
clone.innerHTML = '';
target.appendChild(clone);
if (source.firstChild) {
source = source.firstChild;
target = clone;
} else {
source = source.nextSibling;
}
}
setTimeout(process, 20);
}
}());
}
DEMO
Your code should work. Example here : http://jsfiddle.net/hqKVe/2/
The issue is probably that the content of page[nChar] has HTML chars escaped.
The easiest solution is to use the html() function of jQuery (if you use jQuery). There a good example given by Canavar here : How to decode HTML entities using jQuery?
If you are not using jQuery, you have to unescape the string by yourself. In practice, just do the opposite of what is described here : Fastest method to escape HTML tags as HTML entities?

Rangy range contained within a jquery selector

I'm working on a JavaScript wrapper around the Rangy JavaScript plugin. What I'm trying to do: given a jQuery selector and a range, detect if the range is contained within the selector. This is for a space where a user will read a document and be able to make comments about particular sections. So I have a div with id="viewer" that contains the document, and I have an area of buttons that do things after a user selects some text. Here is the (broken) function:
function selectedRangeInRegion(selector) {
var selectionArea = $(selector);
var range = rangy.getSelection().getRangeAt(0);
var inArea = (selectionArea.has(range.startContainer).length > 0);
if (inArea) {
return range;
} else {
return null;
}
}
It appears that selectionArea.has(range.startContainer) returns an array of size 0. I have tried wrapping like: $(range.startContainer). Any tips?
I developed a solution for this problem. This assumes you have a div selector and that your content does not have any divs:
function containsLegalRange(selector, range) {
var foundContainingNode = false;
var container = range.commonAncestorContainer
var nearestDiv = $(container).closest("div");
if (nearestDiv.attr("id") == selector) {
return true
}
else {
return false
}
}
That's not how has() works: the parameter you pass to it is either a selector string or a DOM element, whereas range.startContainer is a DOM node that may in practice be a text node or an element.
I don't think there will be a way that's as easy as you're hoping. The following is as simple as I can think of off the top of my head.
jsFiddle: http://jsfiddle.net/TRVCm/
Code:
function containsRange(selector, range, allowPartiallySelected) {
var foundContainingNode = false;
$(selector).each(function() {
if (range.containsNode(this, allowPartiallySelected)) {
foundContainingNode = true;
return false;
}
});
return foundContainingNode;
}
.has() can be weird sometimes and produce .length == 0 when it is not supposed to. Try this way instead:
function selectedRangeInRegion(selector) {
var range = rangy.getSelection().getRangeAt(0);
var selectionArea = selector + ':has(\'' + range.startContainer + '\')';
var inArea = $(selectionArea).length > 0);
if (inArea) {
return range;
}
else {
return null;
}
}

Use javascript to extend a DOM Range to cover partially selected nodes

I'm working on a rich text editor like web application, basically a XML editor written in javascript.
My javascript code needs to wrap a selection of nodes from the contentEditable div container.
I'm using the methods described at MDC. But since I need to synchronize the div containers content to my XML DOM I would like to avoid partial selections as described in w3c ranges:
<BODY><H1>Title</H1><P>Blah xyz.</P></BODY
............^----------------^............
This selection starts inside H1 and ends inside P, I'd like it to include H1,P completely.
Is there an easy way to extend the selection to cover partially selected children completely?
Basically I want to use range.surroundContents() without running into an exception.
(The code doesn't need to work with opera/IE)
Looking at the MDC documentation, I manage do something like this:
Selection.prototype.coverAll = function() {
var ranges = [];
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
while(range.startContainer.nodeType == 3
|| range.startContainer.childNodes.length == 1)
range.setStartBefore(range.startContainer);
while(range.endContainer.nodeType == 3
|| range.endContainer.childNodes.length == 1)
range.setEndAfter(range.endContainer);
ranges.push(range);
}
this.removeAllRanges();
for(var i=0; i<ranges.length; i++) {
this.addRange(ranges[i]);
}
return;
};
You can try it here : http://jsfiddle.net/GFuX6/9/
edit:
Updated to have the browser display correctly the augmented selection. It does what you asked for, even if the selection contains several ranges (with Ctrl).
To make several partial nodes Bold, here is a solution:
Selection.prototype.boldinize = function() {
this.coverAll();
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
var parent = range.commonAncestorContainer;
var b = document.createElement('b');
if(parent.nodeType == 3) {
range.surroundContents(b);
} else {
var content = range.extractContents();
b.appendChild(content);
range.insertNode(b);
}
}
};
Thanks to Alsciende I finally came up with the code at http://jsfiddle.net/wesUV/21/.
This method isn't as greedy as the other one. After coverAll(), surroundContents() should always work.
Selection.prototype.coverAll = function() {
var ranges = [];
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
var ancestor = range.commonAncestorContainer;
if (ancestor.nodeType == 1) {
if (range.startContainer.parentNode != ancestor && this.containsNode(range.startContainer.parentNode, true)) {
range.setStartBefore(range.startContainer.parentNode);
}
if (range.endContainer.parentNode != ancestor && this.containsNode(range.endContainer.parentNode, true)) {
range.setEndAfter(range.endContainer.parentNode);
}
}
ranges.push(range);
}
this.removeAllRanges();
for(var i=0; i<ranges.length; i++) {
this.addRange(ranges[i]);
}
return;
};
And the boldinize function:
Selection.prototype.boldinize = function() {
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
var b = document.createElement('b');
try {
range.surroundContents(b);
} catch (e) {
alert(e);
}
}
};
If you mean you want to include the tags H1 and P (i.e., the valid markup), don't worry. You get that for free. If you mean you want to it to include all the content within the (partial) selection, you need to access the Selection object. Read about it on Quirksmode's introduction to Range.

Categories