Khalid Askia Khalid Askia - 3 years ago 120
HTML Question

How can I know if an user deleted an element in a contenteditable div?

Hi I am using jquery to detect if an @ symbol has been inserted into a contenteditable div. When this is detected an ajax request is fired to the server to retrieve a list of users. A user would then click on the username (the function adduser will fire) and it will create a disabled textarea in the div.
Then I create an input dynamically with the id of the user selected as value (to notify him)

The problem I have is that if the users delete a textarea (a username) with the keyboard or even the mouse from the contenteditable div, How do I know wich textarea it is and so remove too the input wich has the user id as value?

My script:



function adduser(fname, lname, id, q)
{
var fname = fname;
var lname = lname;
var id = id;
var word = '@' + q;
var iid = 'id' + id;

var old=$("#contentbox").html();
var content=old.replace(word,"");

$("#contentbox").html(content);

var E="<textarea class='textareamention' name="+id+" id="+iid+" disabled >"+fname+"</textarea>";

$("#contentbox").append(E);

$('<input/>').attr({ type: 'text', name: 'mention[]', id: id, value: id}).appendTo('#post');
}

Answer Source

OK, here goes ...

A way ahead - maybe not the only one - is to use Mutation Observers, more particularly the Mutation-summary, library - to watch your #contentbox for changes. The Mutation-summary documentation even mentions this particular usage case - see last bullet point under What is it useful for?.

The advantage over event handling is that you don't need to predict every type of event that might cause deletion of a textarea.

A few warnings though :

  • Mutation Observers are currently supported only by the major browsers - Google Chrome, Firefox, Safari, Opera and IE11. "In the future it will be implemented in other browsers", we are told.
  • Mutation-summary hasn't been updated for some 3 years.
  • I have not actually used Mutation-summary in anger - I've only read the documentation.

Once a mutation is observed, there must be a number of ways to handle removal of a corresponding element.

One way, as I suggested in the comments above, would be to loop through all the inputs and remove any of them for which its corresponding textarea no longer exists.

However, I think there's a cleaner way.

First, install the Mutation-summary, library on the page (a <script src="..."> tag, like jQuery).

Then, modify adduser() to include a custom EventListener, removeRelatedElements, attached to your <textarea> elements :

function adduser(fname, lname, id, q) {
    $('<textarea class="textareamention" name=' + id + ' id=id' + id + ' disabled>' + fname + '</textarea>')
    .on('removeRelatedElements', function() {
        $input.remove(); // $input is held in a closure
    })
    .appendTo($("#contentbox").html($("#contentbox").html().replace('@' + q, '')));

    var $input = $('<input/>')
    .attr({
        'type': 'text',
        'name': 'mention[]',
        'id': id,
        'value': id
    })
    .appendTo('#post');
}

Now, you should be able to use Mutation-summary to observe changes to #contentbox and, when a textArea is removed, trigger its custom removeRelatedElements event, causing the corresponding <input> element also to be removed.

var observer = new MutationSummary({
    queries: [
        { element: '#contentbox' } // jQuery-like selector
    ],
    callback: function(summaries) {
        summaries[0].removed.forEach(function(removedEl) {
            // Here, you should be able to write POJS or jQuery, or a mixture
            if(removedEl.tagName.toUpperCase() === 'TEXTAREA') {
                $(removedEl).trigger('removeRelatedElements').off('removeRelatedElements'); // remove the corresponding input element and detatch the custom handler to keep things clean
            }
        });
    }
});

That's the gist of it anyway. You may well need to do some debugging.

EDIT:

OK, the above attempt doesn't work due to $("#contentbox").html().replace('@' + q, ''), which replaces the DOM hierarchy within #contentbox. In the process, our .on('removeRelatedElements', ...) event handlers get irrevocably blitzed and thereafter .trigger('removeRelatedElements') fails silently.

We have to revert to "plan A". At each mutation of .textareamention, you need to find and remove any input for which there is no corresponding textarea.

It's tempting to think you can simply work with summaries[0].removed as before, but no. The problem there summaries[0].removed includes references to the textareas that were removed during the replace('@' + q, '') process. Consequently all corresponding input elements get removed, including the one that was just added! The nett effect is that the expected input never appears.

Fortunately, the workaround is simple. We can completely ignore summaries and work with $("#contentbox textarea.textareamention") as a start point at each observed mutation. Don't worry if you can't grasp this point - it's not obvious. I discovered it by trial and error.

Here's the code :

jQuery(function($) {
    var counter = 0; // integer, for associating each <textarea> with its <input>

    $('#button').click(function() {
        var q = " ";

        $('<textarea class="textareamention ' + counter + '" disabled></textarea>') // include `counter` as a className
        .text(counter)  // for demo purposes
        .appendTo($("#contentbox").html($("#contentbox").html().replace('@' + q, '')));

        var $input = $('<input/>').attr({
            'type': 'text',
            'name': 'mention[]',
            'value': counter // for demo purposes
        })
        .data('counter', counter) // bind the current counter value to this <input>
        .appendTo('#post');
        counter++;
    });

    var observer = new MutationSummary({
        'queries': [
            { element: '.textareamention' }
        ],
        'callback': function(summaries) {
            // Remove all inputs for which there is no corresponding textarea.
            var $textareas = $("#contentbox textarea.textareamention"); // all textareas
            $("#post input").filter(function(index, element) {
                return $textareas.filter('.' + $(element).data('counter')).length === 0;
            }) // filter to find any input for which there is no corresponding textarea ...
            .remove(); // ... and remove it/them.
        }
    });
});

DEMO. Note: in the fiddle, all the above code appears at the bottom of the javascript frame.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download