Custom Image Upload Modal with Bootstrap-Wysihtml5

My current project is, among other things, a custom CMS.

In several places, we needed a Wysiwyg editor with an image uploader that would pull from our application’s media library.
We started off with bootstrap-wysihtml5. This project is a nice Bootstrap wrapper on wysihtml5 which is an open source html5 rich text editor project. Out of the box, the bootstrap-wysihtml5 works nicely. But it’s image insert functionality simply asks for an URL which it wraps in an image tag. We wanted it to pull that URL from our in-application library of images.

To achieve this, I modified the editor to allow inserting custom templates for the modal, and hooked up an Ajax call (at /attachable_images) that would pull the images (as json) allowing the dialog for inserting images to display and choose from our image library. The changes that allow this customization have been merged into the project (http://jhollingworth.github.com/bootstrap-wysihtml5/). To enable this, we added the following on our side:

First, we setup the customTemplates that we want to use for the image modal:

// override options
var wysiwygOptions = {
  customTemplates: {
    image: function(locale) {
      return "<li>" +
        "<div class='bootstrap-wysihtml5-insert-image-modal modal hide fade'>" +
        "<div class='modal-header'>" +
        "<a class='close' data-dismiss='modal'>&times;</a>" +
        "<h3>" + locale.image.insert + "</h3>" +
        "</div>" +
        "<div class='modal-body'>" +
        "<div class='chooser_wrapper'>" +
        "<table class='image_chooser images'></table>" +
        "</div>" +
        "</div>" +
        "<div class='modal-footer'>" +
        "<a href='#' class='btn' data-dismiss='modal'>" + locale.image.cancel + "</a>" +
        "</div>" +
        "</div>" +
        "<a class='btn' data-wysihtml5-command='insertImage' title='" + locale.image.insert + "'><i class='icon-picture'></i></a>" +
        "</li>";
    }
  }
};

These options are used when you fire up the editor:

$(function() {
 $('textarea.wysiwyg').each(function() {
    $(this).wysihtml5($.extend(wysiwygOptions, {html:true, color:false});
  });
});

Then we override the insertImage functions defined in the editor.

var xhrFetchingImages;

wysiHelpers = {
  getImageTemplate: function() {
    /* this is what goes in the wysiwyg content after the image has been chosen */
    var tmpl;
    var imgEntry = "<img src='<%= url %>' alt='<%= caption %>'>";
    tmpl = _.template("<div class='shrink_wrap'>" +
                      imgEntry +
                      "</div>" +
                      "<p class='credit'><%= caption %></p>" +
                      "<hr>");
    return tmpl;
  }
}
bootWysiOverrides: {
  initInsertImage: function(toolbar) {
    var self = this;
    var insertImageModal = toolbar.find('.bootstrap-wysihtml5-insert-image-modal');
    var urlInput = insertImageModal.find('.bootstrap-wysihtml5-insert-image-url');
    var insertButton = insertImageModal.find('a.btn-primary');
    var initialValue = urlInput.val();
  
    var chooser = insertImageModal.find('.image_chooser.images'),
    /* this is the template we put in the image dialog */
    var optionTemplate = _.template(
      "<tr><td data-type='image' data-caption='<%= title %>' data-url='<%= urls.content %>'>" +
        "<img src='<%= urls.icon %>'>"+
        "<div class='type'>Image</div>" +
        "<div class='title'><%= title %></div>" + 
        "<div class='caption'><%= caption %></div>" + 
        "</td></tr>");

    var helpers = wysiHelpers;
    
    // populate chooser
    // TODO: this get's called once for each wysiwyg on the page.  we could 
    //       be smarter and cache the results after call 1 and use them later.
    if (!xhrFetchingImages) {
      $.ajax({
        url:'/attachable_images',
        success: function(data) {
          xhrFetchingImages = false;
          // populate dropdowns
          _.each(data, function(img) {
            chooser.append(optionTemplate(img));
          });
        }
      });
    }

    var insertImage = function(imageData) {
      if(imageData.url) {
        var clz = 'image_container';
        var doc = self.editor.composer.doc;
        var tmpl = helpers.getImageTemplate(!!imageData.caption);
        var chunk = tmpl(imageData);
        self.editor.composer.commands.exec("insertHTML", chunk);
      }
    };
    
    chooser.on('click', 'td', function(ev) {
      var $row = $(ev.currentTarget);
      insertImage($row.data());
      insertImageModal.modal('hide');
    });
    
    insertImageModal.on('hide', function() {
      self.editor.currentView.element.focus();
    });
    
    toolbar.find('a[data-wysihtml5-command=insertImage]').click(function() {
      var activeButton = $(this).hasClass("wysihtml5-command-active");
      
      if (!activeButton) {
        insertImageModal.modal('show');
        insertImageModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) {
          e.stopPropagation();
        });
        return false;
      }
      else {
        return true;
      }
    });
  }
};

$.extend($.fn.wysihtml5.Constructor.prototype, bootWysiOverrides);

Now we’re ready to roll. This code will now populate the chooser table with a list of images that come from our system (by way of the Ajax call) and the optionTemplate template. When you click on the image, it’s inserted into the editor window using the DOM defined in the getImageTemplate (which is customized depending on whether or not the image has a caption).

Though the overrides may appear a bit complex, you’ll notice that they look a lot like the original bootstrap-wysihtml5 source where he handles inserting an image – the primary mod is populating the dialog with something more than a simple input box.

Another thing to notice, in the initial options, we add the customStyles and customTags entries. These are to prevent the HTML cleanser from ripping out styles and tags that we are using in our new image DOM. Without those definitions, your new elements will get cleansed – possibly to the point of deletion.

With these simple-ish modifications, we were able to get our application images into the content blocks with a very nice UI.

See it in action (and get the source code) here.
Much thanks to James Hollingworth for merging in my pull requests so I can share this enhancement with others.

UPDATE (4/11/2013): it looks like someone’s done one better (at first glance). You may want to check out http://mindmup.github.io/bootstrap-wysiwyg/. He’s got a nice image upload image picker right out of the gate.

25 thoughts on “Custom Image Upload Modal with Bootstrap-Wysihtml5

  1. I found this very interesting, is this part of a CMS? I have been looking for a simple semantic CMS with a more modern WYSIWYG editor.

    1. We’re using this in a custom built CMS, not something off the shelf. But I’m sure you could put something like this into an off the shelf CMS system. We looked into a couple different packages (Radiant , Comfortable Mexican Sofa, and Harmony ). Though these all solve the “content management system” bill, they didn’t quite fit into our system and timeline. Perhaps they’d be right for you.

  2. For some reason, I couldn’t get any of this ‘customTemplates’ stuff to work, but your post helped me to inject a button in the toolbar and then inject some HTML into the editor when I’d chosen the image, so thanks. I’ll refactor my code at a later date when new versions come out and I can see it working the ‘right’ way. I did something like:

    //1)Call code to inject buttons on editor load:
    load: function(){
    var container = that;
    var editor = $(‘#f’+def.uuid ).data(‘wysihtml5’).editor;
    injectCustomButtons(container, editor);
    }

    //2) In the injectCustomButtons function:
    container.find(‘.wysihtml5-toolbar’).append(”
    +’
    +”);

    And then once the image was chosen…

    editor.composer.commands.exec(“insertHTML”, html);
    editor.currentView.element.focus();

    Works for me – now I just need to get around the need for fully qualified URLs…

    1. It looks like your plan of attack is basically the same. If it wasn’t clear from the post, my code is overwriting a method in the wysiwyg object called initInsertImage. If you called that method something else, it won’t get put in the right place on the wysiwyg prototype and may not be called. I’d be happy to help out if, for no other reason, than to make this post clearer. Setup a gist or a jsfiddle, if you want, and we could try to hammer it out.

      cheers

    1. I should probably sync with his. I submitted a pull request and I think my codebase may have gotten out of date because of that. Thanks for noticing.

      1. it looks like that the jhollingworth repo has remained with the same version number (0.0.2) even though there have been updates. Additionally, in the jhollingworth has been updated but the dist/ files have not been rebuilt in his repo. I just pulled his latest, built the new dist files and updated my repo. My repo and the sample app (http://wysihtml5-images.herokuapp.com/) we should be up to date now.

    1. I’ll take a look. I like the idea of upload but before I’d roll that in, I want to understand better how the jQuery.upload system works – where do the assets go etc. More as it happens. Cheers.

      1. @rcode5 you can write your own handler to save the uploads (or not). Just like your at /attachable_images. I use (/)upload.php for this. Maybe this should be renamed to /upload.
        The end point return json status and file information.
        A multipart/form-data request is send to the handler like: Content-Disposition: form-data; name=”file1″; filename=”images.png” as input.

      2. Got it. Nice integration. I took what you’d done and merged it in. Added a fake upload endpoint that acts as if upload succeeded (though i’m throwing out the uploaded data). I also added a bit of code so the ‘success’ message is cleared when the modal is closed so you could upload another image later (if you wanted). I also added some thanks to you as a contributor and better instructions on how one might integrate this into their own app.

        I noticed, while doing this, there are several jQuery.upload implementations. This one you’ve chosen is a bit strange in that he pulls the JSON data out of an html body (instead of just processing the response as pure JSON). His code base is not on github or I’d submit a pull request. I also updated his code to use JSON.parse instead of window.eval (should be a bit safer). You may want to pull/compare my version of jqueryupload with yours.

        Thanks for the suggestion. Now that the upload feature is in there, I find it much more useful that I would have imagined.

        If you want to give it a whirl: http://wysihtml5-images.herokuapp.com/

        Cheers
        Mr Rogers

    2. Hi Mr Rogers,

      Well done! Thanks. Yes, i think this code will be useful too.
      I have changed the readme on github, see https://github.com/bassjobsen/wysihtml5-image-upload. Do you have any suggestion for it?

      About the ‘success’ message. You should add a removeClass cause you add a class every upload now.

      $(‘#uploadresult’).html(‘Upload successful’).removeClass().addClass(‘alert alert-success’);

      On your demo page, please could you change me name in the reference section? o instead of a. On this page you also link to sinatrarb.org this should be sinatrarb.com.

      Regards

      1. Super. The extras on the docs on your page look great.

        I’ll try to get the JS and docs issues cleaned up soon.

        I’d like to work on a little better distribution method for the whole package might be nice. Something for the future, i suppose.

        Cheers

  3. Please forgive me if I’m missing something, I’m still feeling my way around JSON and I don’t really grasp it yet. Is it possible to do what you’ve done — add a list of images rather than just the input window — whilst grabbing the images info from a MySQL database? The table has columns for tblImages.image_path and tblImages.image_name (you could call it a caption, I guess).
    I would love to sit down and get into JSON properly, however time is short for this project and I just need something to work. I don’t need (or want) upload functionality — the uploading of images is handled elsewhere in my app.

    Thanks in advance.

    1. Should be pretty straight forward. It depends how you get stuff out of the database and who’s building JSON for you. If you look at this https://github.com/rcode5/image-wysiwyg-sample/blob/master/app.rb#L16, you should see that I’m generating urls and captions and sending them back as JSON. I suspect that whatever language you are using to grab data from a database either understands JSON or has a library to do JSON parsing. Once you find that, take your SQL results, build an array of hashes

      # in ruby you might do something like this (given an array of returned database rows)
      images = []
      dbresult.each do |row|
      images << { file: row.image_path, caption: row.image_name }
      end
      images.to_json

      Now you have a list of images as JSON. Make a server endpoint that returns those images as JSON. The image upload form (javascript) picks it up right here https://github.com/rcode5/image-wysiwyg-sample/blob/master/static/custom_image_wysihtml5.js#L38 using the Ajax call to ‘/attachable_images’ which returns that JSON.

      Hope this helps

      1. Thank you very much for your help, and my apologies for my lack of knowledge.

        I’m using PHP (I don’t know ruby very well) and I set the URL (attachable_images) to

        url:'http://emailcms.com.au/fetch-images-json.php',

        .

        If you load that php in your browser, you’ll see the json_encode output.

  4. Thanks for the code.
    Sometimes we had issues, that the form.submit() did nothing. Turned out, it is, because forms should never be nested (the wysiwyg editor is part of a form, the modal wraps it’s own form inside the modal which is inside the textarea’s form).
    We had to append the modal code to the document’s body, which solved that issue.

  5. Great work!
    A few suggestions for improvements;
    Clear the file upload status before opening the dialog.
    Clear the file name from the dialog.

    by adding these two lines
    $(‘#uploadresult’).html(”).removeClass();
    $(“#file1”).val(“”);
    before this line.
    insertImageModal.modal(‘show’);

    Clearing and fetching the images prior to opening the dialog could also be useful so any other changes in the image library are reflected.

    1. For the upload piece, you need to build a backend of some kind that can store images for you. You might look at the use by Bass Jobsen here https://github.com/bassjobsen/wysihtml5-image-upload. In my sample app, I’m assuming that you’ve already got an image store somewhere and this widget provides a way to get those images into the Wysiwyg. They could be a static set of images but more likely is that you’ve built another part of your web app to either store or connect to a storage system to provide those images.

Leave a reply to Mick Cancel reply