Insert link in contenteditable element

26,480

Solution 1

document.execCommand() does this for you in all major browsers:

document.execCommand("CreateLink", false, "http://stackoverflow.com/");

To preserve the selection while your link dialog is displayed, you can use the following functions:

function saveSelection() {
    if (window.getSelection) {
        sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
            var ranges = [];
            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                ranges.push(sel.getRangeAt(i));
            }
            return ranges;
        }
    } else if (document.selection && document.selection.createRange) {
        return document.selection.createRange();
    }
    return null;
}

function restoreSelection(savedSel) {
    if (savedSel) {
        if (window.getSelection) {
            sel = window.getSelection();
            sel.removeAllRanges();
            for (var i = 0, len = savedSel.length; i < len; ++i) {
                sel.addRange(savedSel[i]);
            }
        } else if (document.selection && savedSel.select) {
            savedSel.select();
        }
    }
}

jsFiddle example: http://jsfiddle.net/JRKwH/1/

UPDATE

To get hold of the link(s) created (if any were created at all) is tricky. You could use my own Rangy library:

var sel = rangy.getSelection();
if (sel.rangeCount) {
    var links = sel.getRangeAt(0).getNodes([1], function(el) {
        return el.nodeName.toLowerCase() == "a";
    });
    alert(links.length);
}

... or something like the following:

function getLinksInSelection() {
    var selectedLinks = [];
    var range, containerEl, links, linkRange;
    if (window.getSelection) {
        sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
            linkRange = document.createRange();
            for (var r = 0; r < sel.rangeCount; ++r) {
                range = sel.getRangeAt(r);
                containerEl = range.commonAncestorContainer;
                if (containerEl.nodeType != 1) {
                    containerEl = containerEl.parentNode;
                }
                if (containerEl.nodeName.toLowerCase() == "a") {
                    selectedLinks.push(containerEl);
                } else {
                    links = containerEl.getElementsByTagName("a");
                    for (var i = 0; i < links.length; ++i) {
                        linkRange.selectNodeContents(links[i]);
                        if (linkRange.compareBoundaryPoints(range.END_TO_START, range) < 1 && linkRange.compareBoundaryPoints(range.START_TO_END, range) > -1) {
                            selectedLinks.push(links[i]);
                        }
                    }
                }
            }
            linkRange.detach();
        }
    } else if (document.selection && document.selection.type != "Control") {
        range = document.selection.createRange();
        containerEl = range.parentElement();
        if (containerEl.nodeName.toLowerCase() == "a") {
            selectedLinks.push(containerEl);
        } else {
            links = containerEl.getElementsByTagName("a");
            linkRange = document.body.createTextRange();
            for (var i = 0; i < links.length; ++i) {
                linkRange.moveToElementText(links[i]);
                if (linkRange.compareEndPoints("StartToEnd", range) > -1 && linkRange.compareEndPoints("EndToStart", range) < 1) {
                    selectedLinks.push(links[i]);
                } 
            }
        }
    }
    return selectedLinks;
}

jsFiddle: http://jsfiddle.net/JRKwH/3/

Solution 2

Better looking answer:

function link() {
  if (window.getSelection().toString()) {
    var a = document.createElement('a');
    a.href = 'http://www.google.com';
    a.title = 'GOOGLE';
    window.getSelection().getRangeAt(0).surroundContents(a);
  }
}
select some of text then click link button!
<button onclick='link()'>link text to google</button>

This method can be applied anywhere and does not require the element to be contenteidtable.

you can add any event or attributes to the new A element like other elements.

The window.getSelection().toString() checks if some text is actually selected. It works well in chrome, I don't have IE to test, anyway there are other methods to check it. But surroundContents() which is the key part is available in IE9 as suggested by MDN.

Finally I suggest to use an iFrame instead of contenteditable div so there will be no worry about preserving the selection.

Solution 3

As alfred said there are already well-developed editors, especially for the basic features. You can restrict it to use as few, or as many features, as you would like.

The difficult part in developing it from scratch, is that all browsers act slightly differently. The following should get you moving in the right direction in most browsers, other than IE:

var selected = document.getSelection();
document.execCommand("insertHTML",false,"<a href='"+href+"'>"+selected+"</a>");

Solution 4

EDIT It is not possible in IE in Execcommand, because we cannot insert quotes in 'href', we must do it in pure javascript with range :

// IN DIV IN ONE IFRAME

// Get the frame
var iframe = document.getElementById('myframe');

// Selection object in the frame
theSelection = iframe.contentWindow.getSelection();

// position of the selection to insert
theRange = theSelection.getRangeAt(0);

// get content inside the original selection (and delete content in)
var fragment = theRange.extractContents();

// Create a new link in frame
var newLink = iframe.contentWindow.document.createElement('a');

// Create a text element with the fragment to put in the link
var theText = document.createTextNode(fragment.textContent);

// URL 
theLink.href = '#';

// Title
theLink.title = 'title';

// Attribute 'onclick'
theLink.setAttribute('onclick', thelink);

// Target
theLink.target = '_blank';

// Add the text in the link
theLink.appendChild(theText);

// Insert the link at the range
theRange.insertNode(newLink);

// DIV WITHOUT FRAMES

// Selection object in the window
theSelection = window.getSelection();

// begin of the selection to insert
theRange = theSelection.getRangeAt(0);

// get content inside the original selection (and delete content in)
var fragment = theRange.extractContents();

// Create a new link in the document
var newLink = document.createElement('a');

// Create a text element with the fragment to put in the link
var theText = document.createTextNode(fragment.textContent);

// URL 
theLink.href = '#';

// Title
theLink.title = 'title';

// Attribute 'onclick'
theLink.setAttribute('onclick', thelink);

// Target
theLink.target = '_blank';

// Add the text in the link
theLink.appendChild(theText);

// Insert the link at the range
theRange.insertNode(newLink);
Share:
26,480

Related videos on Youtube

PeeHaa
Author by

PeeHaa

So long SO main o/ and thanks for all the fish Check out my personal website or check out one of the open source projects I'm currently working on: Jeeves - A headless chatbot for the PHP room GitHub Minifine - JS and CSS minifier GitHub Requestable - online webservice for testing and debugging HTTP / REST requests GitHub OpCacheGUI - a nice webinterface for PHP's OpCache GitHub EmailTester - an online emailaddress validation regex tester GitHub Commentar - an open source PHP5.4+ commenting system GitHub HexDump - an online hex viewer GitHub RichUploader - a private filehoster GitHub PHP OAuth library GitHub Proposal for the new beginners tutorial on php.net GitHub I've created a close-voting Chrome plugin available at GitHub to help clean up Stack Overflow. If you would like to see what other projects I'm working on please visit my GitHub or drop me a line in the PHP chat.

Updated on July 09, 2022

Comments

  • PeeHaa
    PeeHaa almost 2 years

    I'm working on a simple blog system and I'm using contenteditable so that users can format the text.

    Up to now everything works like a charm.

    Next thing i want is that users can add a hyperlink in the text.

    The user have to select (part of) the text and click on the link button. After that a popup opens where users should enter the link address.

    When the user clicks on the accept button I want to add the link to the text they selected in the contenteditable.

    How can I implement this functionality, since I have no clue how to do this?

    My site: http://82.170.147.49/blog/3/alpha-release

    jsFiddle of my site: http://jsfiddle.net/qhN9j/

  • PeeHaa
    PeeHaa about 13 years
    Thanks clmarquart. Works for most browsers. I didn't really expect it would work under IE by default. What else is new :)
  • PeeHaa
    PeeHaa about 13 years
    The only thing that happens now is that when I try to enter an URL the selection isn't set anymore on my text. So I cannot add the hyperlink. Is there a way to preserve / reset the selection before the execCommand?
  • clmarquart
    clmarquart about 13 years
    Since you are using a <p/> for your contentEditable area, you are using the same document element, so what happens is when you click anywhere you will lose the previous selection. Try using the Enter key as the way to submit, rather than clicking the check icon. Otherwise, you can get more in-depth by preserving the range, reference the Mozilla Docs
  • Tim Down
    Tim Down about 13 years
    @PeeHaa, @clmarquart: Using the "CreateLink" command of document.execCommand() does exactly what is required in all browsers. See my answer.
  • clmarquart
    clmarquart about 13 years
    @Tim, you're correct. Although, I've preferred to use insertHTML, even if it requires a little extra work for IE. This way you can directly control the HTML that is inserted, like adding a title or class attribute.
  • PeeHaa
    PeeHaa about 13 years
    Mr. Down. You're my all time hero! Finally I can get rid of that ugly javascript prompt window ;) Although I think clmarquart has a valid point. The next thing I want to do is create a link with a target (_blank) and I think I am going to need insertHTML (or something other) for this. Is it possible to make your code work with adding a target to the a tag? If you can you're not only my hero but I'll upgrade your status to God! :)
  • PeeHaa
    PeeHaa about 13 years
    @clmarquart sorry I need to make Tim's answer THE answer. Although you have made a good point I think with the extra attributes! +1 for that.
  • Tim Down
    Tim Down about 13 years
    @PeeHaa: Good question. It's going to be tricky because the command handles multiple cases. Sometimes no link will be created at all (if the selected content is already inside a link with the same URL) or only content not already inside another link will be linked. I will add a bit to my answer.
  • PeeHaa
    PeeHaa about 13 years
    Wow. You've just been promoted to God! :) Thanks yet again!
  • Neek
    Neek about 11 years
    The problem with using inserthtml with a full <a> tag's markup is that it may break up an existing tag that you're inserting into. I am battling with content of e.g. "<font face='whatever'>Content goes here</font>".. selecting 'goes' and using insertHTML of an anchor causes Chrome to break the <font> tag up, so I get "<font face='whatever'>Content </font><a href='wherever'>goes </a><font face='whatever'>here</font>". This means the <a> content is styled differently. This seems quite a serious problem, is there a way around this while still using inserthtml?
  • Ced
    Ced about 8 years
    @TimDown Would it be possible to modify saveSelection so if the user hasn't clicked in the editable area yet the carret just goes at the end ?
  • Ced
    Ced about 8 years
    nvm, i'll just checck if div has focus before saving
  • Alexander Dixon
    Alexander Dixon over 7 years
    This is simple and elegant. It should be the accept answer. Where can find a comprehensive list of all the .execCommand() interactions. They seem pretty awesome but I am hesitant to move from jQuery.
  • ZirkoViter
    ZirkoViter over 6 years
    Is this still up to date? What is document.selection?
  • lmiller1990
    lmiller1990 about 6 years
    @SerhiiKalaida still working great in 2018 for me. i thikn document.selection was supported in old browsers.
  • Tim Down
    Tim Down about 6 years
    @SerhiiKalaida document.selection is the old selection API in Internet Explorer (from version 4 or 5 to 10, I think). IE 9 and later have the standards-based window.getSelection().
  • Bogdan
    Bogdan almost 4 years
    It is a good example but there will be problems when you need to remove the link from that selection. :D