I'm trying to wrap selected text from a "contenteditable" div in a given tag. Below seems to be working ok but startOffset/endOffset doesn't include HTML text. My question is how do I get the Range object to count the html tags if they exist in the selection?
getSelectedText: function() {
var range;
if (window.getSelection) {
range = window.getSelection().getRangeAt(0);
return [range.startOffset, range.endOffset];
}
}
toggleTagOnRange: function(range, tag, closeTag) {
var removeExp, val;
if (closeTag == null) {
closeTag = tag;
}
val = this.get("value");
removeExp = RegExp("<" + tag + ">(.+)</" + closeTag + ">");
if (removeExp.test(val)) {
this.set("value", val.replace(removeExp, function(match, $1) {
return $1;
}));
} else {
if (range.length > 1) {
val = val.splice(range[1], "</" + closeTag + ">").splice(range[0], "<" + tag + ">");
this.set("value", val);
}
}
return this.get("val");
}
// this is called from a bold button click handler.
this.toggleTagOnSelection(this.getSelectedText(), 'strong');
Interested in other solutions if you've got them.
Honestly, things can get pretty nasty when trying to write code for this type of thing yourself. There are a lot of cases you need to cover, like when you're selecting text across multiple <p> tags for example. You don't need to reinvent the wheel. Look into a library like rangy where they have already taken care of the nitty gritty details. Specifically for your situation, if you can get by with using CSS styles instead of using tag elements like <strong>, look into the CSS Class Applier Module, which allows you to do this simply by doing:
var cssApplier = rangy.createCssClassApplier("someClass", {normalize: true});
cssApplier.toggleSelection();
Where .someClass is a CSS class containing whatever styles you need to apply.
Related
Ok.. So i have a html element that displays a current value, value is changed via range slider, i change the value of the html element using javascript with the following code:
hex_out = document.querySelector('#hex');
hex_out.value=rangeValue;
Which the above works fine, but recently I've needed to be able to add a bit of html code into hex_out.value
for example if i try
hex_out.value="<font color='red'>"+rangeValue+"</font>"
it will change hex_out.value to the code shown below
<font color='red'>Range slider value</font>"
which is obviously not what i'm trying to accomplish. How would i add html code to hex_out.value and have it display correctly?
Before anyone trys to say "This is a duplicate question", realize i've tried nearly ALL solutions shown on stack overflow with no luck
You'll need to modify the color via the element's style property. Try the following:
hex_out.style.color = 'red';
If you need to add any HTML, then (assuming that the hex_out element is a container element, and not an <input> tag), you can assign the HTML to its innerHTML property like so:
hex_out.innerHTML = '<strong>This is a <em>formatted</em> value.</strong>';
Another edit: It looks like you're using an <output> element, which doesn't accept child elements as far as I know. In this case, you'll likely want to use a regular <div> or <span> tag instead of <output> and then update its value manually as your sliders move using the innerHTML property described above.
I think you are looking for something like this:
var hex_out = document.querySelector('#hex'),
rangeValue = document.querySelector('#val');
rangeValue .addEventListener('input', function () {
hex_out.innerHTML = "<font color='red'>"+rangeValue .value+"</font>";
}, false);
<input type=range id=val>
<span id=hex>50</span>
Your difficulty arose because you put an obsolete font tag in the output tag which only accepts "phrasing content". The font tag is not supported in HTML5 and not on the list of phrasing content elements.
As #Mark said, a simple div will do if you require HTML to be rendered.
The example you gave is the thing normally expecting from javascript. I consider you want to change html of an existing element not value. If it is you can use below code:
hex_out. innerHTML ="<font color='red'>"+rangeValue+"</font>"
Existing fiddle link JSFiddle
Tested and works:
function setColor() { debugger;
Rval = parseFloat((r_out.value / 255.00).toFixed(3));
Gval = parseFloat((g_out.value / 255.00).toFixed(3));
Bval = parseFloat((b_out.value / 255.00).toFixed(3));
hex = "R: " + Rval + " " + "G: " + Gval + " " + "B: " + Bval;
var r_hex = parseInt(r.value, 10).toString(16),
g_hex = parseInt(g.value, 10).toString(16),
b_hex = parseInt(b.value, 10).toString(16),
hex1 = "#" + pad(r_hex) + pad(g_hex) + pad(b_hex);
body.style.backgroundColor = hex1;
hex_out.innerHtml = hex + [
'<br/><font size="1px">GSC RGB Selector</font>'
].join('#hex');
}
I've known how to use the document.selection to do the highlighting. For example
/* http://jsfiddle.net/4J2dy/ */
$("#content").on('mouseup', function() {
highlighting();
});
var highlighting = function () {
var seleted_str = (document.all) ? document.selection.createRange().text : document.getSelection();
if(seleted_str != "") {
var stringToBeHighlighted = seleted_str.getRangeAt(0);
var span = document.createElement("span");
span.style.cssText = "background-color: #80deea";
span.className = "MT";
stringToBeHighlighted.surroundContents(span);
}
};
But there is something I don't know how to achieve.
Let's say that I have four layers created with the same content at the same time.
And I would like to select a sentence on the controlling layer while all the same sentence in the other three layers will be selected too.(See image below)
After the selection, I would like to pop out a menu(which I can do), and get the DOM element based on which button is pressed.(See image below)
Could anyone tell me how to achieve this? Or it just can't be done? I would be grateful if someone could answer for me.
It's kind of possible, and I would appreciate the input of SO user Tim Down as he knows a lot about JS Range/Selections, but I'll present my partial solution already.
Instead of selecting the 4 layers, you could just store the startOffset & endOffset in an external object that is updated on mouseup. The only by-effect this has, is that the user's selection will only get the color of the span when they click a layer button.
The advantage is that you can now simply work with DOM Textnodes as opposed to ranges/ selection (more complex, to me anyway).
I've chosen to find the layers with a data-layer attribute on the buttons and a corresponding id on the layers themselves. I handled the 'appending' of the 'selected span' by slicing the text content of the text nodes in the layers, like so:
layer.innerHTML = txt.slice(0, selText.start)
+ '<span class="MT" style="background-color: #80deea">'
+ txt.slice(selText.start, selText.end) + '</span>'
+ txt.slice(selText.end, txt.length);
See it in action here. I've added a cleanSelection function so only one selection is possible at a time (the start & end counters fail because selection ranges don't take into account HTML tags, so you have to get rid of the spans).
Final notes:
The fiddle will not work in browsers not supporting getElementsByClassName
The fiddle only supports one selection at a time.
The fiddle does not extensively test all conditions (eg, whether the nodetype of the first child is truly a text node, etc. But it ain't hard to add that yourself)
Entire JS code as reference (also in fiddle):
// this object will hold the start & end offsets of selection value
var selText = false;
// selText will be updated on mouseup
document.body.onmouseup = getSelText;
// on button click, selText will be highlighted
document.body.onclick = function(e) {
var target = e.target || e.srcElement, range, layer, txt;
// only do if it's a layer button & the selection is non-empty
if (target.getAttribute('data-layer') && selText !== false) {
// first remove previous spans, they break the startOffset & endOffset of the selection range
cleanSelection();
// get the clicked layer
layer = document.getElementById(target.getAttribute('data-layer'));
// this assumes that the first node in the layer is a textNode
txt = layer.firstChild.nodeValue;
// let's append the selection container now
layer.innerHTML = txt.slice(0, selText.start)
+ '<span class="MT" style="background-color: #80deea">'
+ txt.slice(selText.start, selText.end) + '</span>'
+ txt.slice(selText.end, txt.length);
// ...and empty the 'real selection'
window.getSelection().collapse();
// log results to console
console.log('From char ' + selText.start + ' to char ' + selText.end + ', in ' + layer.id);
}
};
function getSelText () {
var seleted_str = (document.all) ? document.selection.createRange().text : document.getSelection(), stringToBeHighlighted;
if(seleted_str !== "") {
stringToBeHighlighted = seleted_str.getRangeAt(0);
selText = {
start: stringToBeHighlighted.startOffset,
end: stringToBeHighlighted.endOffset
};
} else {
selText = false;
}
}
function cleanSelection() {
var getText, mtSpan = document.getElementsByClassName('MT');
for ( var i = 0; i < mtSpan.length; i++) {
getText = mtSpan[i].innerHTML;
mtSpan[i].previousSibling.nodeValue = mtSpan[i].previousSibling.nodeValue + getText + mtSpan[i].nextSibling.nodeValue;
mtSpan[i].parentNode.removeChild(mtSpan[i].nextSibling);
mtSpan[i].parentNode.removeChild(mtSpan[i]);
}
}
If I have a container div called #container which has a bunch of .inside divs in it, how would I go about checking whether a certain .inside div with a specified content (just a string of English text) exists or not? I'm doing this to prevent duplicates in a notification system I'm setting up for a website. I don't need the actual text - just whether it exists. Also, being able to modify the content of the .inside div if it's found would be good, so I can increment and show the number of times that message has occurred (grouping, if you like).
Thanks,
James
I like using selectors (others have used .filter, which is equally an option).
$(".inside:contains('waldo')").css({color: 'red'});
This is case sensitive.
Use the contains-selector[docs], then the length[docs] property to see how many were found.
var some_string = "test";
var els_with_string = $('#container .inside:contains(' + some_string + ')');
// use .length to check to see if there was at least one
if( els_with_string.length ) {
alert( "at least one already exists" );
}
From the docs:
Description: Select all elements that contain the specified text.
The matching text can appear directly within the selected element, in any of that element's descendants, or a combination thereof. As with attribute value selectors, text inside the parentheses of :contains() can be written as bare words or surrounded by quotation marks. The text must have matching case to be selected.
With respect to modifying the content if found, it would depend on what sort of modification you want to do. I don't know exactly what you mean by grouping.
EDIT: With respect to your comment, you could accomplish what you need like this:
var error = "ERROR:SomeError ";
var el_with_error = $('#container .inside:contains(' + error + ')');
if (el_with_error.length) {
var text = el_with_error.text();
if (/\(\d+\)/.test(text)) {
var new_text = text.replace(/\((\d+)\)/, function(s, g1) {
g1++;
return "(" + g1 + ")";
});
el_with_error.text(new_text);
} else {
el_with_error.text(text + " (2)");
}
} else {
$('#container').append('<div class="inside">' + error + '</div>');
}
Live Example: http://jsfiddle.net/ScZbV/
We could get by without the regular expression if you were able to wrap the grouping quantity in a <span> element.
var error = "ERROR:SomeError ";
var el_with_error = $('#container .inside:contains(' + error + ')');
if (el_with_error.length) {
var span = el_with_error.find('span');
if (span.length) {
var num = +span.text();
span.text( ++num );
} else {
el_with_error.append(" (<span>2</span>)");
}
} else {
$('#container').append('<div class="inside">' + error + '</div>');
}
Example: http://jsfiddle.net/ScZbV/1/
To check existence
$("#container .inside:contains('old text')").size() > 0
To modify the text
$("#container .inside:contains('old text')").text('new text');
Here's a slightly different way of looking at it...
Apply a class name for each "type" of notification. So your notification markup looks like:
<div class="inside error">Error</div>
Then inside of looking for a string inside these divs, use these new class names to your advantage and make use of .find(). If jQuery returns an object then its exists, so do something with it. But if it returns nothing then add it.
Example: http://jsbin.com/imexi4
How to select a part of string?
My code (or example):
<div>some text</div>
$(function(){
$('div').each(function(){
$(this).text($(this).html().replace(/text/, '<span style="color: none">$1<\/span>'));
});
});
I tried this method, but in this case is selected all context too:
$(function(){
$('div:contains("text")').css('color','red');
});
I try to get like this:
<div><span style="color: red">text</span></div>
$('div').each(function () {
$(this).html(function (i, v) {
return v.replace(/foo/g, '<span style="color: red">$&<\/span>');
});
});
What are you actually trying to do? What you're doing at the moment is taking the HTML of each matching DIV, wrapping a span around the word "text" if it appears (literally the word "text") and then setting that as the text of the element (and so you'll see the HTML markup on the page).
If you really want to do something with the actual word "text", you probably meant to use html rather than text in your first function call:
$('div').each(function(){
$(this).html($(this).html().replace(/text/, '<span style="color: none">$1<\/span>'));
// ^-- here
}
But if you're trying to wrap a span around the text of the div, you can use wrap to do that:
$('div').wrap('<span style="color: none"/>');
Like this: http://jsbin.com/ucopo3 (in that example, I've used "color: blue" rather than "color: none", but you get the idea).
$(function(){
$('div:contains("text")').each(function() {
$(this).html($(this).html().replace(/(text)/g, '<span style="color:red;">\$1</span>'));
});
});
I've updated your fiddle: http://jsfiddle.net/nMzTw/15/
The general practice of interacting with the DOM as strings of HTML using innerHTML has many serious drawbacks:
Event handlers are removed or replaced
Opens the possibility of script inject attacks
Doesn't work in XHTML
It also encourages lazy thinking. In this particular instance, you're matching against the string "text" within the HTML with the assumption that any occurrence of the string must be within a text node. This is patently not a valid assumption: the string could appear in a title or alt attribute, for example.
Use DOM methods instead. This will get round all the problems. The following will use only DOM methods to surround every match for regex in every text node that is a descendant of a <div> element:
$(function() {
var regex = /text/;
function getTextNodes(node) {
if (node.nodeType == 3) {
return [node];
} else {
var textNodes = [];
for (var n = node.firstChild; n; n = n.nextSibling) {
textNodes = textNodes.concat(getTextNodes(n));
}
return textNodes;
}
}
$('div').each(function() {
$.each(getTextNodes(this), function() {
var textNode = this, parent = this.parentNode;
var result, span, matchedTextNode, matchLength;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchLength = result[0].length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
span = document.createElement("span");
span.style.color = "red";
span.appendChild(matchedTextNode);
parent.insertBefore(span, textNode);
}
});
});
});
How could I change the text below so that the text within it has a number appended to it.
<div class="right">This is some text</div>
<div class="right">This is some text</div>
<div class="right">This is some text</div>
So the code above would become,
This is some text
This is some text
This is some text
you should use an ordered list... ol
or else you will need use css and add the content property your selector with the :after pseudo element.
How about the following?
$("div.right").each(function(i){
$(this).prepend((i + 1) + ". ");
});
UPDATE:
Here is one way that should work.
"number" is a custom element (it can be anything you want) that will/should be ignored by browsers.
$("div.right").each(function(i){
$(this).find("number").remove().end()
.prepend("<number>(i + 1) + ". </number>");
});
OR use the following which is probably a little slower but semantically correct...
$("div.right").each(function(i){
$(this).find("span.number").remove().end()
.prepend("<span class='number'>" + (i + 1) + ". </span>");
});
OR an even better way would be to prepend span.number before your first drag:
$(function(){ // document ready...
// caching the "numbers" will only work if you use the DOM
// for updating div position (like jQuery's "append()", "prepend()", "before()", and "after()") and not "innerHTML" or "html()"
var numbers = $("div.right").each(function(i){
$(this).prepend("<span class='number'>" + (++i) + "</span>. ");
}).find("span.number");
function dragEnd(){
// do your drag end stuff here...
numbers.each(function(i){
this.innerHTML = ++i;
});
)};
});
This is really an elaboration on another comment. I can't format code in a comment, I guess. You could use jQuery core's each:
$('div.right').each(function(ii){
html = $(this).html();
$(this).html(ii + '. ' + html);
});
jQuery selectors are your friend...
Get your stuff and loop on through something like this:
texts = $("div.right");
for(i = 0;i < texts.length;i++)
{
node = $(texts[i]);
content = node.html();
number = i + 1;
node.html(number + ". " + content);
}
Update: Jeez, last time post untested code straight off the dome here (disclaimer: not actually the last time). In the interest of correctness, I've updated it to at least run (and work!) if you still want to do it this way. Although I admit the other solutions are cleaner and more elegant.
Does this have to be done dynamically through jquery? Can't you just combine all that text into one div and then make a ordered list around it?
Using [] notation with a result set will give you the raw DOM element which does not have the html() function. Use the eq() function to get each element wrapped in a jQuery object.
You can also use each() as mentioned above, but I prefer straight 'for loops' so I don't have to adjust for 'this' if I'm in an event handler.
var texts = $("div.right");
var elem;
for(i = 1; i < texts.length; i++) {
elem = texts.eq(i);
html = elem.html();
elem.html(i + '. ' + html);
}