I have: Simple block of html text:
<p>
The future of manned space exploration and development of space depends critically on the
creation of a dramatically more proficient propulsion architecture for in-space transportation.
A very persuasive reason for investigating the applicability of nuclear power in rockets is the
vast energy density gain of nuclear fuel when compared to chemical combustion energy...
</p>
I want: wrap word into span when user click on it.
I.e. User clicked at manned word, than I should get
<p>
The future of <span class="touched">manned</span> space exploration and development of space depends critically on the
creation of a ....
Question: How to do that? Is there way more efficient that just wrap all words into span at loading stage?
P.S. I'm not interested in window.getSelection() because I want to imply some specific styling for touched words and also keep collection of touched words
Special for #DavidThomas: example where I get selected text, but do not know how to wrap it into span.
I were you, I'd wrap all words with <span> tags beforehand and just change the class on click. This might look like
$( 'p' ).html(function( _, html ) {
return html.split( /\s+/ ).reduce(function( c, n ) {
return c + '<span>' + n + ' </span>'
});
});
and then we could have a global handler, which listens for click events on <span> nodes
$( document.body ).on('click', 'span', function( event ) {
$( event.target ).addClass( 'touch' );
});
Example: http://jsfiddle.net/z54kehzp/
I modified #Jonast92 solution slightly, I like his approach also. It might even be better for huge data amounts. Only caveat there, you have to live with a doubleclick to select a word.
Example: http://jsfiddle.net/5D4d3/106/
I modified a previous answer to almost get what you're looking for, as demonstrated in this demo.
It finds the currently clicked word and wraps a span with that specific class around the string and replaced the content of the paragraph with a new content which's previously clicked word is replaced with the newly wrapped string.
It's limited a bit though because if you click on a substring of another word, let's say 'is' then it will attempt to replace the first instance of that string within the paragraph.
You can probably play around with it to achieve what you're looking for, but the main thing is to look around.
The modified code:
$(document).ready(function()
{
var p = $('p');
p.css({ cursor: 'pointer' });
p.dblclick(function(e) {
var org = p.html();
var range = window.getSelection() || document.getSelection() || document.selection.createRange();
var word = $.trim(range.toString());
if(word != '')
{
var newWord = "<span class='touched'>"+word+"</span>";
var replaced = org.replace(word, newWord);
$('p').html(replaced);
}
range.collapse();
e.stopPropagation();
});
});
Then again, #jAndy's answer looks very promising.
Your answers inspired me to the next solution:
$(document).ready(function()
{
var p = $('p');
p.css({ cursor: 'pointer' });
p.dblclick(function(e) {
debugger;
var html = p.html();
var range = window.getSelection() || document.getSelection() || document.selection.createRange();
var startPos = range.focusOffset; //Prob: isn't precise +- few symbols
var selectedWord = $.trim(range.toString());
var newHtml = html.substring(0, startPos) + '<span class=\"touched\">' + selectedWord + '</span>' + html.substring(startPos + selectedWord.length);
p.html(newHtml);
range.collapse(p);
e.stopPropagation();
});
});
We haven't there wrap each word in span. Instead we wrap word only on click.
use
range.surroundContents(node)
$('.your-div').unbind("dblclick").dblclick(function(e) {
e.preventDefault();
// unwrap .touched spans for each dblclick.
$(this).find('.touched').contents().unwrap();
var t = getWord();
if (t.startContainer.nodeName == '#text' && t.endContainer.nodeName == '#text') {
var newNode = document.createElement("span");
newNode.setAttribute('class', 'touched');
t.surroundContents(newNode);
}
e.stopPropagation();
});
function getWord() {
var txt = document.getSelection();
var txtRange = txt.getRangeAt(0);
return txtRange;
}
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 ! ;)
Now that my brain aches after trying by myself and searching all over without finding any answer.
i ask you.
I am trying to change the colors of the single letters in a text which is contained inside a <h5>Hello</h5> When the pointer is hovering over each single one, I can manage this by spamming alot of <span></span> and put letter by letter inside each <span></span>, then using CSS to just change color when hovering.
BUT
I want to do this by using Javascript.
Here i've accomplished extracting every single letter in a <h5>, but I am not getting them to change color when I hover each one of them.
$('h6').ready(
function () {
var T = $('h6').text();
for (letters in T) {
$(T[letters]).hover(
function () {
$(T[letters]).toggle("color", "red");
})
}
});
So with the great help of GolezTrol i achieved victory doing what i wanted to do!
Though i use Css instead of the javascript to handle the hover =)
result - java - syntax:
$(function () {
$('h2').each(
function () {
//Extract text from html, and attach it to "Txt" variable.
var Txt = $(this).text();
//Empty var-string waiting for loop.
var Gtxt = '';
//Loop through text to add <span id="green> on every letter.
for (i in Txt) {
//add letter by letter to Gtxt ( <span id="green"> letter </span> )
Gtxt = (Gtxt + '<span id="green">' + Txt[i] + '</span>');
//IF for Newline on period.
if (Txt[i] == '.') {
Gtxt = (Gtxt + '<br>');
}
}
//Add processed text to Html $('h2')
$(this).html(Gtxt);
});
});
CSS:
#green{
color: "color"
}
#green:hover{
color: "green"
}
var T = $('h6').text();
That line just gets the text of the element into a string. So the code after that, if it would work at all, just works on the in-memory string and won't be visible in your browser.
To make this work, you will have to do the same you did by hand: add a span around each of the letters and give each span a different color.
You can do this with the following HTML: ;)
<h6>Hello world</h6>
Javascript to embed all letters in a span inside every h6 in the document.
// Function that embeds each letter with a span. Maybe this can be done
// simpler, but it works.
$(function()
{
$('h6').each(function(){
var txt = $(this).text();
var html = '';
for (t in txt)
{
html = html + '<span>' + txt[t] + '</span>';
}
// Put the generated HTML back in the document.
$(this).html(html);
});
});
Javascript to handle the hover:
Of course you can do this by simply declaring CSS as well if you are just toggling colors, but if you want to have more complex effects or random colors, this might be your Javascript solution:
// Attaches hover events to each span within a h6. Using document.on, this
// event will work for any span this is or will be in the document.
$(document).on('hover', 'h6 span', function(event){
// 'hover' is a shorthand. The event is linked to mouseenter and mouseleave, so
// you'll have to check event.type to see which one it is.
if (event.type == 'mouseenter')
$(this).css('color', 'red');
else
$(this).css('color', 'blue');
});
JS Fiddle:
http://jsfiddle.net/KdzQ7/
E.g. I have a div that contains all the text like:
<div class="text-container">
<p>
Though we welcome the idea of a smaller connector, we're miffed that Apple couldn't just adopt the semi-industry standard of Micro-USB. That would make things easier for smartphone users across the globe. Yet, even so, the smaller connector may be a smart move for the future. The 30-pin connector has been around since 2003, long before the iPhone even existed: frankly, it's a dust magnet. A smaller connector helps shave extra space to achieve a smaller phone with perhaps a bigger battery. The new connector cable will mainly be used for syncing and charging by most people who own an Apple TV or Bluetooth/AirPlay accessories.
</p>
</div>
And I would like to create something like this:
<div class="text-container">
<p>
Though we welcome the idea of a smaller connector...[show more]
</p>
</div>
I guess I should get all the content of the div and then Find the first e.g. 50 character and put there a link and all the other text put in some div which will be hidden, and after the click on the link the other stuff show up.
It should be toggle-like and change the text from [show more] to [show less] if it is expanded and vice versa.
Any advice how to achieve this with plain javascript and jquery itself and without other jQuery plugins?
Here is another solution.
It doesn't simply cut the words in the middle but checks endings, punctuation, and long words.
$(".text-container p").each(function() {
var val = $.trim(this.innerHTML),
parsed = val.split(/\s+/),
cut = parsed;
// for each word
for (var i = 0, k = 0; i < parsed.length; i++) {
k += parsed[i].length + 1;
if (k - 1 > 50) {
cut = parsed.slice(0, i);
break;
}
}
// if one long word
if (cut.length == 0) {
cut.push(parsed[0].substring(0, 50));
}
val = cut.join(" ");
// if the text is long enough to cut
if (cut.length != parsed.length) {
this.innerHTML = val.replace(/[.,;?!]$/, "")
+ "<span>...</span> ";
$("<span />")
.css("display", "none")
.html(parsed.slice(cut.length).join(" ") + " ")
.appendTo(this);
$("<a />", {
href : "#",
text : "[show more]"
}).on("click", function(e) {
var sh = this.innerHTML == "[show more]";
$(this).prev("span").toggle(sh).prev("span").toggle(!sh);
this.innerHTML = sh ? "[show less]" : "[show more]";
e.preventDefault();
}).appendTo(this);
} else {
this.innerHTML = val;
}
});
DEMO: http://jsfiddle.net/xRuch/
Build a quick demo for you
jQuery
$(function(){
var $el = $(".text-container").find("p");
var str = $el.text();
var str1 = str.substr(0,50), str2= str.substr(51);
$el.html(str1+" <span class='showMore'><a href='#'>Show more...</a></span><span class='moreText'>"+str2+"</span><span class='showLess'><a href='#'>Show Less</a></span>");
$(".showMore").on("click", function(){
$(this).next(".moreText").show();
$(this).next(".moreText").next(".showLess").show();
$(this).hide();
});
$(".showLess").on("click", function(){
$(this).prev(".moreText").hide();
$(this).prev(".moreText").prev(".showMore").show();
$(this).hide();
})
});
css
.moreText, .showLess { display:none};
on modern browsers you could use the css property text-overflow:ellipsis;
http://jsfiddle.net/Y8skB/1/
i used css in my example to expand the text, but you could also do it with jquery if you want a show more link.
The tricky part is not selecting the elements here, but just selecting the text within. The only true jQuery that will give you back text contents is .contents(). So I'm getting the contents of every element not he page, and I want to pick out a word, such as "hashtag". Then append to it.
What am I doing wrong here:
<html>
<p>
The word hashtag is in this sentence.
</p>
</html>
jQuery:
$(function() {
$('*')
.contents()
.filter(function(){
return this.nodeType === 3;
})
.filter(function(){
return this.nodeValue.indexOf('hashtag') != -1;
})
.each(function(){
alert("It works!")
});
});
$('*') grabs every element
.contents() grabs the contents of every element
.filter(function(){ return this.noteType === 3; refines it down to the text contents of elements. (#3 node type is text)
return this.nodeValue.indexOf('hashtag') should grab the word "hashtag". Not sure if this is working.
!= -1; should prevent it from grabbing every single element in the HTML. Not sure about that one.
Why doesn't it work? I know I have anything appending tags yet, but can I select the word "hashtag" thanks!
If you want to do this for the whole page you can work on the HTML of the body element:
$(function() {
var regExp = new RegExp("\\b(" + "hashtag" + ")\\b", "gm");
var html = $('body').html();
$('body').html(html.replace(regExp, "<a href='#'>$1</a>"));
});
Keep in mind that this may be slow if your page is large. Also, all elements will be rewritten and thus loose their event handlers etc.
If you don't want this or want to restrict the replacement to certain elements, you can select and iterate over them:
$(function() {
var regExp = new RegExp("\\b(" + "hashtag" + ")\\b", "gm");
$('div, p, span').each(function() { // use your selector of choice here
var html = $(this).html();
$(this).html(html.replace(regExp, "<a href='#'>$1</a>"));
});
});
JS :
function replaceText() {
$("*").each(function() {
if($(this).children().length==0) {
$(this).html($(this).text().replace('hashtag', '<span style="color: red;">hashtag</span>'));
}
});
}
$(document).ready(replaceText);
$("html").ajaxStop(replaceText);
HTML :
<html>
<p>
The word hashtag is in this sentence.
</p>
</html>
Fiddle : http://jsfiddle.net/zCxsY/
Source : jQuery - Find and replace text, after body was loaded
This is done with span but will work with obviously
The clean variant would be this:
$(function() {
var searchTerm = 'hashtag';
$('body *').contents()
.filter(function () {
return this.nodeType == 3
&& this.nodeValue.indexOf(searchTerm) > -1;
})
.replaceWith(function () {
var i, l, $dummy = $("<span>"),
parts = this.nodeValue.split(searchTerm);
for (i=0, l=parts.length; i<l; i++) {
$dummy.append(document.createTextNode(parts[i]));
if (i < l - 1) {
$dummy.append( $("<a>", {href: "", text: searchTerm}) );
}
}
return $dummy.contents();
})
});
It splits the value of the text node at searchTerm and re-joins the parts as a sequence of either new text nodes or <a> elements. The nodes created this way replace the respective text node.
This way all text values keep their original meaning, which cannot be guaranteed when you call replace() on them and feed them to .html() (think of text that contains HTML special characters).
See jsFiddle: http://jsfiddle.net/Tomalak/rGcxw/
I don't know jQuery very much but I think you can't just say .indexOf('hashtag'), you have to iterate through the text itself. Let's say with substring. Probably there's an jQuery function that will do this for you, but that might be your problem for finding 'hashtag'.
I am using a 'contenteditable' <div/> and enabling PASTE.
It is amazing the amount of markup code that gets pasted in from a clipboard copy from Microsoft Word. I am battling this, and have gotten about 1/2 way there using Prototypes' stripTags() function (which unfortunately does not seem to enable me to keep some tags).
However, even after that, I wind up with a mind-blowing amount of unneeded markup code.
So my question is, is there some function (using JavaScript), or approach I can use that will clean up the majority of this unneeded markup?
Here is the function I wound up writing that does the job fairly well (as far as I can tell anyway).
I am certainly open for improvement suggestions if anyone has any. Thanks.
function cleanWordPaste( in_word_text ) {
var tmp = document.createElement("DIV");
tmp.innerHTML = in_word_text;
var newString = tmp.textContent||tmp.innerText;
// this next piece converts line breaks into break tags
// and removes the seemingly endless crap code
newString = newString.replace(/\n\n/g, "<br />").replace(/.*<!--.*-->/g,"");
// this next piece removes any break tags (up to 10) at beginning
for ( i=0; i<10; i++ ) {
if ( newString.substr(0,6)=="<br />" ) {
newString = newString.replace("<br />", "");
}
}
return newString;
}
Hope this is helpful to some of you.
You can either use the full CKEditor which cleans on paste, or look at the source.
I am using this:
$(body_doc).find('body').bind('paste',function(e){
var rte = $(this);
_activeRTEData = $(rte).html();
beginLen = $.trim($(rte).html()).length;
setTimeout(function(){
var text = $(rte).html();
var newLen = $.trim(text).length;
//identify the first char that changed to determine caret location
caret = 0;
for(i=0;i < newLen; i++){
if(_activeRTEData[i] != text[i]){
caret = i-1;
break;
}
}
var origText = text.slice(0,caret);
var newText = text.slice(caret, newLen - beginLen + caret + 4);
var tailText = text.slice(newLen - beginLen + caret + 4, newLen);
var newText = newText.replace(/(.*(?:endif-->))|([ ]?<[^>]*>[ ]?)|( )|([^}]*})/g,'');
newText = newText.replace(/[ยท]/g,'');
$(rte).html(origText + newText + tailText);
$(rte).contents().last().focus();
},100);
});
body_doc is the editable iframe, if you are using an editable div you could drop out the .find('body') part. Basically it detects a paste event, checks the location cleans the new text and then places the cleaned text back where it was pasted. (Sounds confusing... but it's not really as bad as it sounds.
The setTimeout is needed because you can't grab the text until it is actually pasted into the element, paste events fire as soon as the paste begins.
How about having a "paste as plain text" button which displays a <textarea>, allowing the user to paste the text in there? that way, all tags will be stripped for you. That's what I do with my CMS; I gave up trying to clean up Word's mess.
You can do it with regex
Remove head tag
Remove script tags
Remove styles tag
let clipboardData = event.clipboardData || window.clipboardData;
let pastedText = clipboardData.getData('text/html');
pastedText = pastedText.replace(/\<head[^>]*\>([^]*)\<\/head/g, '');
pastedText = pastedText.replace(/\<script[^>]*\>([^]*)\<\/script/g, '');
pastedText = pastedText.replace(/\<style[^>]*\>([^]*)\<\/style/g, '');
// pastedText = pastedText.replace(/<(?!(\/\s*)?(b|i|u)[>,\s])([^>])*>/g, '');
here the sample : https://stackblitz.com/edit/angular-u9vprc
I did something like that long ago, where i totally cleaned up the stuff in a rich text editor and converted font tags to styles, brs to p's, etc, to keep it consistant between browsers and prevent certain ugly things from getting in via paste. I took my recursive function and ripped out most of it except for the core logic, this might be a good starting point ("result" is an object that accumulates the result, which probably takes a second pass to convert to a string), if that is what you need:
var cleanDom = function(result, n) {
var nn = n.nodeName;
if(nn=="#text") {
var text = n.nodeValue;
}
else {
if(nn=="A" && n.href)
...;
else if(nn=="IMG" & n.src) {
....
}
else if(nn=="DIV") {
if(n.className=="indent")
...
}
else if(nn=="FONT") {
}
else if(nn=="BR") {
}
if(!UNSUPPORTED_ELEMENTS[nn]) {
if(n.childNodes.length > 0)
for(var i=0; i<n.childNodes.length; i++)
cleanDom(result, n.childNodes[i]);
}
}
}
This works great to remove any comments from HTML text, including those from Word:
function CleanWordPastedHTML(sTextHTML) {
var sStartComment = "<!--", sEndComment = "-->";
while (true) {
var iStart = sTextHTML.indexOf(sStartComment);
if (iStart == -1) break;
var iEnd = sTextHTML.indexOf(sEndComment, iStart);
if (iEnd == -1) break;
sTextHTML = sTextHTML.substring(0, iStart) + sTextHTML.substring(iEnd + sEndComment.length);
}
return sTextHTML;
}
Had a similar issue with line-breaks being counted as characters and I had to remove them.
$(document).ready(function(){
$(".section-overview textarea").bind({
paste : function(){
setTimeout(function(){
//textarea
var text = $(".section-overview textarea").val();
// look for any "\n" occurences and replace them
var newString = text.replace(/\n/g, '');
// print new string
$(".section-overview textarea").val(newString);
},100);
}
});
});
Could you paste to a hidden textarea, copy from same textarea, and paste to your target?
Hate to say it, but I eventually gave up making TinyMCE handle Word crap the way I want. Now I just have an email sent to me every time a user's input contains certain HTML (look for <span lang="en-US"> for example) and I correct it manually.