8

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>
kthornbloom
  • 3,660
  • 2
  • 31
  • 46

3 Answers3

5

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");
Community
  • 1
  • 1
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • This is looking very good. One question if you don't mind- it seems that you need to select more than the heading text in order to remove it. I assume that's because technically the cursor ends up before the closing H1 tag. Is that correct? Any conceivable way to fix that issue? – kthornbloom Sep 03 '13 at 13:01
  • @kthornbloom: Ah, that's an oversight by me. I'll fix it. – Tim Down Sep 03 '13 at 13:48
  • Perfect, still works, thanks @TimDown!! I had to rewrite 'replaceWithOwnChildren' though, because it gave 'el.parentNode is null', for unknown reasons.This worked for me: let $contents = $(el).html(); $(el).replaceWith($contents); Also had to wrap an 'if(node)' on the line after 'getSelectedNodes().forEach(function(node)' since it gave an error 'node is null' on some occasions. – IT-Girl Jan 05 '20 at 13:43
1

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.

Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • That would almost work. I'm really trying to build something that doesn't produce terrible code though. If execCommand won't help, does it even sound feasible to use javascript to save whatever is selected, strip html, and then return it to the editing area? – kthornbloom Aug 21 '13 at 13:13
  • @CtotheDoubleS: Yes: getting selected block elements and removing them shouldn't too hard, although I don't have an easy example to hand. – Tim Down Aug 22 '13 at 08:29
  • Looking in the DOM inspector, this does not produce terrible code like nested `

    No heading!

    ` or anything, so I'd say this solves your problem :)
    – asontu Aug 31 '13 at 13:09
  • jsfiddle link is broken :( – Stepan Suvorov Nov 19 '14 at 14:35
  • @STEVER: So it is. I assume the OP, who owned that jsFiddle, has deleted it. I should have made my own and now it's lost in the mists of the internet. – Tim Down Nov 19 '14 at 17:41
0

It is feasible with javascript, logic is the following:

  1. get the selected text and its position (cf. Get the Highlighted/Selected text and javascript - Getting selected text position)
  2. remove all the <h1> and </h1> from the selected text

    s = s.replace(/<h1>/g, '');  
    s = s.replace(/<\/h1>/g,''); 
    
  3. 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 ?

Community
  • 1
  • 1
Mehdi
  • 7,204
  • 1
  • 32
  • 44
  • I did come across the selected text post. I just haven't been able to string it all together on a button press. 50 rep points to you if you know how! – kthornbloom Aug 31 '13 at 20:42
  • I'm not overly concerned with IE support, although I'm testing in Chrome 29 and it doesn't seem to be working there either. When I highlight an H1 title and click the button, nothing happens. This is a good start though, so an upvote for that. Are you having success with your demo in webkit? I've actually got a very good editor put together that has workarounds for most typical issues. This is just the last bug I need to squash! – kthornbloom Sep 02 '13 at 01:03
  • it works for me in Chrome, but you need to have your selection start before the `

    ` and finish after the `

    `. cf. the second 'broken' bullet point.
    – Mehdi Sep 02 '13 at 12:10