Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enforce LI formatting in a contenteditable UL

I'm trying to allow users to edit a list (UL). In my attempts, it appears that contenteditable doesn't do anything special (like enforcing behind-the-scenes markup) -- it just gives the user a window into the innerHTML.

This is causing issues in that if there is not already a LI, and the user adds something, it doesn't get LI-ized. Similarly, if there are list items, but the user deletes them, then the LI gets deleted, and any new text is added without LI's. See http://jsfiddle.net/JTWSC/ . I've also found that it's sometimes possible for the cursor to "get outside" of an LI that does exist, but I can't reproduce consistently.

I have to include code, so this is what the "result" looks like:

<ul>whatever the user typed in</ul>

How do I fix this? I started down the path of a $('ul').keyup() handler that checks the html and wraps as necessary, but I was running into a handful of gotchas, like timing, losing focus on the element, having to refocus in the right place, etc. I'm sure it's possible if I work at it, but I'm hoping for an easier solution.

like image 648
James S Avatar asked Nov 05 '12 21:11

James S


1 Answers

I built the following keyup/down handler in order to make my contenteditable <UL>s idiot proof.*

It does two things:

  1. Adds <LI>s to the <UL> when the <UL> is empty. I use some code I found on SO (from Tim Down) to place the caret in the expected place
  2. Cleans up all non-LI / non-BR tags. This is basically a quick-and-dirty paste-cleaner.

This is pushing my comfort-level on jquery and DOM manipulation, so there are probably a few things I could do better, but it works pretty well as-is.

//keyup prevented the user from deleting the bullet (by adding one back right after delete), but didn't add in li's on empty ul's, thus keydown added to check
$('ul').on('keyup keydown', function() {
  var $this = $(this);
    if (! $this.html()) {
        var $li = $('&lt;li&gt;&lt;/li&gt;');
        var sel = window.getSelection();
       var range = sel.getRangeAt(0);
        range.collapse(false);
        range.insertNode($li.get(0));
        range = range.cloneRange();
        range.selectNodeContents($li.get(0));
        range.collapse(false);
        sel.removeAllRanges();
        sel.addRange(range);

    } else {
        //are there any tags that AREN'T LIs?
        //this should only occur on a paste
        var $nonLI = $this.find(':not(li, br)');

        if ($nonLI.length) {
            $this.contents().replaceWith(function() {
    //we create a fake div, add the text, then get the html in order to strip out html code. we then clean up a bit by replacing nbsp's with real spaces
return '&lt;li&gt;' + $('&lt;div /&gt;').text($(this).text()).html().replace(/&nbsp;/g, ' ') + '</li>';
            });
            //we could make this better by putting the caret at the end of the last LI, or something similar
        }                   
    }
});

jsfiddle at http://jsfiddle.net/aVuEk/5/

*I respectfully disagree with Diodeus that training is the best / easiest solution in all cases. In my situation, I have several contenteditable <UL>s on a page that are very in-line WYSIWYG (ie, not a lot of room for tinymce-style chrome) and used by casual, first-time, non-advanced users.

like image 166
James S Avatar answered Nov 15 '22 10:11

James S