How to remove H1 formatting within ContentEditable (wysiwyg) - javascript

With the exception of using Undo, I don't think there's a way to remove h1 and h2 tags in content editable. The expected behavior is clicking the H1 button again should toggle it off, but it does not. There's also a "remove formatting" button, but it only works on items that are bold, italic, etc. Is there a way to do this through javascript?
Edit: Result must remove the opening and closing H1 tag, and not replace it with anything else.
Please see the simplified test case here:
http://jsfiddle.net/kthornbloom/GSnbb/1/
<div id="editor" contenteditable="true">
<h1>This is a heading one</h1>
How can I remove the header styling if I want to?
</div>

I decided to implement the approach I outlined in my comment to my other answer: traversing nodes within the selected range and removing particular nodes (in this case, based on tag name).
Here's the full demo. It won't work in IE <= 8 (which lacks DOM Range and Selection support) but will in everything other major current browser. One problem is that the selection isn't always preserved, but that isn't too hard to achieve.
http://jsfiddle.net/gF3sa/1/
This example includes modified range traversal code from elsewhere on SO.
function nextNode(node) {
if (node.hasChildNodes()) {
return node.firstChild;
} else {
while (node && !node.nextSibling) {
node = node.parentNode;
}
if (!node) {
return null;
}
return node.nextSibling;
}
}
function getRangeSelectedNodes(range, includePartiallySelectedContainers) {
var node = range.startContainer;
var endNode = range.endContainer;
var rangeNodes = [];
// Special case for a range that is contained within a single node
if (node == endNode) {
rangeNodes = [node];
} else {
// Iterate nodes until we hit the end container
while (node && node != endNode) {
rangeNodes.push( node = nextNode(node) );
}
// Add partially selected nodes at the start of the range
node = range.startContainer;
while (node && node != range.commonAncestorContainer) {
rangeNodes.unshift(node);
node = node.parentNode;
}
}
// Add ancestors of the range container, if required
if (includePartiallySelectedContainers) {
node = range.commonAncestorContainer;
while (node) {
rangeNodes.push(node);
node = node.parentNode;
}
}
return rangeNodes;
}
function getSelectedNodes() {
var nodes = [];
if (window.getSelection) {
var sel = window.getSelection();
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
nodes.push.apply(nodes, getRangeSelectedNodes(sel.getRangeAt(i), true));
}
}
return nodes;
}
function replaceWithOwnChildren(el) {
var parent = el.parentNode;
while (el.hasChildNodes()) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
function removeSelectedElements(tagNames) {
var tagNamesArray = tagNames.toLowerCase().split(",");
getSelectedNodes().forEach(function(node) {
if (node.nodeType == 1 &&
tagNamesArray.indexOf(node.tagName.toLowerCase()) > -1) {
// Remove the node and replace it with its children
replaceWithOwnChildren(node);
}
});
}
removeSelectedElements("h1,h2,h3,h4,h5,h6");

This may not exactly meet your needs, but you could do it by using the FormatBlock command and passing in "div" or "pre" as the final parameter:
document.execCommand('formatBlock', false, 'p');
Demo: http://jsfiddle.net/GSnbb/2/ [jsFiddle has been deleted]
EDIT: Yes, this doesn't answer the question as it is now. However, it pre-dates the edit to the question about not replacing the <h1> element and was a reasonable answer to the original question.

It is feasible with javascript, logic is the following:
get the selected text and its position (cf.
Get the Highlighted/Selected text
and
javascript - Getting selected text position)
remove all the <h1> and </h1> from the selected text
s = s.replace(/<h1>/g, '');
s = s.replace(/<\/h1>/g,'');
Insert the corrected text in place of the original one
I have drafted a solution based on your JSFiddle, but it requires some tweaking.
works: removing <h1> and </h1> from selected text on Gecko and webKit based browsers
not developed: IE support - cf. links in the jsfiddle, should not be difficult
broken:
replacement of incomplete selections (containing only one of <h1> and </h1>) - easy to fix
removal of <h1> when it is right at the beginning of the selected text - you will need to play around a bit more with selections and ranges to sort that out.
P.S. Have you considered using an existing text editor plugin instead of creating it by yourself ?

Related

How to know if Ckeditor anchor is encapsulated within span?

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;
}
});

Wrap a tag around multiple instances of a string using Javascript

I’m trying to wrap multiple instances of a string found in html around a tag (span or abbr) using pure JS. I have found a way to do it by using the code:
function wrapString() {
document.body.innerHTML = document.body.innerHTML.replace(/string/g, ‘<tag>string</tag>');
};
but using this code messes with a link’s href or an input’s value so I want to exclude certain tags (A, INPUT, TEXTAREA etc.).
I have tried this:
function wrapString() {
var allElements = document.getElementsByTagName('*');
for (var i=0;i<allElements.length;i++){
if (allElements[i].tagName != "SCRIPT" && allElements[i].tagName != "A" && allElements[i].tagName != "INPUT" && allElements[i].tagName != "TEXTAREA") {
allElements[i].innerHTML = allElements[i].innerHTML.replace(/string/g, ‘<span>string</span>');
}
}
}
but it didn’t work as it gets ALL the elements containing my string (HTML, BODY, parent DIV etc.), plus it kept crushing my browser. I even tried with JQuery's ":containing" Selector but I face the same problem as I do not know what the string's container is beforehand to add it to the selector.
I want to use pure JavaScript to do that as I was planning on using it as a bookmark for quick access to any site but I welcome all answers regarding JQuery and other frameworks as well.
P.S. If something like that has already been answered I couldn't find it...
This is a quite complicated problem actually (you can read this detailed blog post about it).
You need to:
recurse on the dom tree
find all text nodes
do your replace on its data
make the modified data into dom nodes
insert the dom nodes to the tree, before the original text node
remove the original text node
Here is a demo fiddle.
And if you still need tagName based exclusions, look at this fiddle
The code:
function wrapInElement(element, replaceFrom, replaceTo) {
var index, textData, wrapData, tempDiv;
// recursion for the child nodes
if (element.childNodes.length > 0) {
for (index = 0; index < element.childNodes.length; index++) {
wrapInElement(element.childNodes[index], replaceFrom, replaceTo);
}
}
// non empty text node?
if (element.nodeType == Node.TEXT_NODE && /\S/.test(element.data)) {
// replace
textData = element.data;
wrapData = textData.replace(replaceFrom, replaceTo);
if (wrapData !== textData) {
// create a div
tempDiv = document.createElement('div');
tempDiv.innerHTML = wrapData;
// insert
while (tempDiv.firstChild) {
element.parentNode.insertBefore(tempDiv.firstChild, element);
}
// remove text node
element.parentNode.removeChild(element);
}
}
}
function wrapthis() {
var body = document.getElementsByTagName('body')[0];
wrapInElement(body, "this", "<span class='wrap'>this</span>");
}

Get HTML of selection in a specific div

I have found a code snippet (can't remember where), and it's working fine - almost :-)
The problem is, that it copies the selection no matter where the selection is made on the entire website, and it must only copy the selection if it is in a specific div - but how is that done?
function getHTMLOfSelection () {
var range;
if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
return range.htmlText;
}
else if (window.getSelection) {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0);
var clonedSelection = range.cloneContents();
var div = document.createElement('div');
div.appendChild(clonedSelection);
return div.innerHTML;
} else {
return '';
}
} else {
return '';
}
}
$(document).ready(function() {
$("#test").click(function() {
var kopitekst = document.getElementById("replytekst");
var kopitjek=getHTMLOfSelection(kopitekst);
if (kopitjek=='')
{
alert("Please select some content");
}
else
{
alert(kopitjek);
}
});
});
I have made a Jsfiddle
This is my first post here. Hopefully I done it right :-)
That's because it checks the entire document with:
if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
return range.htmlText;
}
Not a specific section. If you want to check specific sections for selected text, you need to identify that you are searching for them in the search selection, something that nails your range down to a particular div:
range = $('#replytekst');
Specify a particular DOM element instead of using document object.
var oDiv = document.getElementById( 'selDiv' );
then use
if ( oDiv.selection && oDiv.selection.createRange ) {
range = oDiv.selection.createRange();
return range.htmlText;
}
You need to check if the section contains the selection. This is separate from getting the selection. There is a method for doing this in this answer: How to know if selected text is inside a specific div
I've updated your fiddle
Basically you need to check the id of the parent/ascendant of the selected text node.
selection.baseNode.parentElement.id or selection.baseNode.parentElement.parentElement.id will give you that.
Edit: I've thought of another, somewhat hack-y, way of doing it.
If
kopitekst.innerHTML.indexOf(kopitjek) !== -1
gives true, you've selected the right text.
DEMO1
DEMO2
(these work in Chrome and Firefox, but you might want to restructure the getHTMLOfSelection function a little)
If it possible for you I recommend to use rangy framework. Then your code might look like this:
// get the selection
var sel = rangy.getSelection();
var ranges = sel.getAllRanges();
if (!sel.toString() || !sel.toString().length)
return;
// create range for element, where selection is allowed
var cutRange = rangy.createRange();
cutRange.selectNode(document.getElementById("replytekst"));
// make an array of intersections of current selection ranges and the cutRange
var goodRanges = [];
$.each(ranges, function(j, tr) {
var rr = cutRange.intersection(tr);
if (rr)
goodRanges.push(rr);
});
sel.setRanges(goodRanges);
// do what you want with corrected selection
alert(sel.toString());
// release
sel.detach();
In this code if text was selected in your specific div then it will be kept, if there was selection where other elements take part too, these selection ranges will be cut off.

Rangy: How can I get the span element that is created using the Highlighter module?

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";
});

Javascript/Jquery - How do I get the element that the user has selected with their cursor?

If the user highlights the text within an <h1> with their cursor, how do I get that <h1> object? Or if they selected text within an <li>, how do i get that <li>?
You can get the selection on Document as,
dd = window.getSelection();
desiredElement = dd.focusNode.parentNode; // h1 or li or other
desiredTag = desiredElement.tagName; // its tagname
Happy Coding.
You need to deal with window.getSelection().
See
See Here
Here
and Here
$('h1').click(function(){
alert(this); // `this` is the <h1> object clicked.
});
is there some tricky part I missed in your question?
You can get the parent element of a selection in all modern mainstream browsers as follows. Bear in mind that Firefox allows multiple selections by default these days; this code will use only the first.
See also my answer here: How can I get the DOM element which contains the current selection?
function getSelectionContainerElement() {
var sel, el;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt) {
if (sel.rangeCount) {
el = sel.getRangeAt(0).commonAncestorContainer;
return (el.nodeType == 3) ? el.parentNode : el;
}
} else {
// This happens in old versions of Safari. A workaround
// exists, if you need it
}
} else if (document.selection && document.selection.createRange) {
return document.selection.createRange().parentElement();
}
}

Categories