#TinyMCE: Add Character Count

TinyMCE has a built in word counter but it does not have a way to count characters.  We have a requirement that limits the users to 5000 characters per textarea field.  This count is purely to keep the users from writing a book each time they enter data.  We only need to could the actual characters that the user sees, all html and hidden characters are ignored.

To do this I created a character count plugin.  It ended up pretty straight forward in the end.  I tried many different ways of pulling and counting the visible characters but many of them were pretty inaccurate.  This version is the closest I could get to the Microsoft Word character counts.

Factoids:

  • I started the plugin with a generic base similar to the existing TinyMCE word count plugin.
  • I layered on this Stack Overflow answer to decode the raw HTML in a way that gave me the most accurate character count.
  • I used this Stack Overflow answer to hid the the path in the status bar of the editor.
  • I also added a generic “Over 5000 characters” validation message that I turn on and off based on the validation done in the “on change” event.
  • The last piece was to validate the character count in the “on submit” event.

TinyMCE Initialization

    $('textarea.tinymce').tinymce({
      script_url: '/scripts/tinymce/4.0.25/tinymce.min.js',
      theme: "modern",
      plugins: "customcharactercount",
      toolbar: "bold italic | link | bullist numlist ",
      menubar: false,
      statusbar: true,
      setup: function (editor) {
        editor.on('change', function(e) {
          var count = this.plugins["fmgcharactercount"].getCount();
          if (count > 5000)
            $('#invalidContentHtml').show();
          else
            $('#invalidContentHtml').hide();
        });
      },
      init_instance_callback: function (editor) {
        $('.mce-tinymce').show('fast');
        $(editor.getContainer()).find(".mce-path").css("display", "none");
      }
    });

Character Count Plugin:

tinymce.PluginManager.add('customcharactercount', function (editor) {
  var self = this;

  function update() {
    editor.theme.panel.find('#charactercount').text(['Characters: {0}', self.getCount()]);
  }

  editor.on('init', function () {
    var statusbar = editor.theme.panel && editor.theme.panel.find('#statusbar')[0];

    if (statusbar) {
      window.setTimeout(function () {
        statusbar.insert({
          type: 'label',
          name: 'charactercount',
          text: ['Characters: {0}', self.getCount()],
          classes: 'charactercount',
          disabled: editor.settings.readonly
        }, 0);

        editor.on('setcontent beforeaddundo', update);

        editor.on('keyup', function (e) {
            update();
        });
      }, 0);
    }
  });

  self.getCount = function () {
    var tx = editor.getContent({ format: 'raw' });
    var decoded = decodeHtml(tx);
    var decodedStripped = decoded.replace(/(<([^>]+)>)/ig, "").trim();
    var tc = decodedStripped.length;
    return tc;
  };

  function decodeHtml(html) {
    var txt = document.createElement("textarea");
    txt.innerHTML = html;
    return txt.value;
  }
});

On Submit Code:

var charcnt = tinyMCE.editors["{TextAreaId}"].plugins["customcharactercount"].getCount() <= 5000;

Book Review: #wycwyc


While I love books, I don’t often get excited for them before they are even published. For years now I have been following Roni Noone and her journey to healthy living and a balanced lifestyle. She inspires people to be a slightly better version of themselves by making small changes on a regular basis.  She coined her method of doing this “wycwyc” which means “what you can when you can”. I find that this way of living meshes very well with my life and philosophy.  Shortly here the book that Roni and Carla (another great healthy living blogger) wrote about wycwycing will be release to the public.  I already have mine pre-ordered!

XML Date, Convert to TimeZone

Dates are complicated no matter which language you use.  To solve some of our problems we save all dates as Universal time, typically using (DateTime.UtcNow).  Then when we display it we convert to a specific timezone.

For this specific project we save the Utc date as an XML string.  When we parse it out for display we read the date, convert to the proper timezone, then put it back in the XML be parsed for display. In this system we show all our dates as Central Standard Time.   We were doing a TryParse but got an error stating that the date was the wrong “Kind”.  Finally I hit on the right syntax and, as with so many other little tidbits, don’t want to lose it.


.......

XElement cElement = data.Element("CreatedDate");

cElement.ReplaceWith(GetDateFormattedCst(cElement, "{0:g} CST"));

.......
 private XElement GetDateFormattedCst(XElement element, string format)
    {
      if(element == null || element.IsEmpty)
         return null;
      XmlReader reader = element.CreateReader();
      reader.MoveToContent();
      string dateValue = reader.ReadInnerXml();

      DateTime dateTime;
      CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");
      DateTimeStyles styles = DateTimeStyles.AdjustToUniversal;

      if (DateTime.TryParse(dateValue, culture, styles, out dateTime))
      {
        dateTime = dateTime.ToCentralStandardTime();
      }
      else
      {
        return null;
      }

      dateValue = String.Format(format, dateTime);
      dateValue = string.Format("<{0}>{1}</{0}>", element.Name, dateValue);

      return XElement.Parse(dateValue);
    }

#WebEssentials & Zen Coding

One of the plug-ins that I use the most in Visual Studio is Web Essentials.  It has a large set of features designed to make life easier for developers to get their jobs done.   Today I read the article by Visual Studio Time Saver – Faster HTML Coding by Susan Ibach where she talks about the Zen Coding feature in Web Essentials.  For as many years I have used Web Essentials this is not a feature that I am familiar with.  It’s incredible!  Here is a list of links to different places to get additional information on Web Essentials and tips on how to use Zen Coding.

As a bonus check out vstips in Twitter.  There some great suggestions in there for making Visual Studio easier to use.

Here is a link to the video Susan referenced that shows how to use the Zen Coding functionality.

Book Review: #SoftSkills

A few weeks back I came across a book called Soft Skills.  I’m a sucker for these kinds of books and before I realized what I was doing I had splurged and purchased it.  It is a fantastic book by John Sonmez of Simple Programmer.  With these kinds of books I usually skip around reading what interests me that day.  I make a note on the chapter to indicate that I have already read it.  After a week when flipping through it to find an unread chapter I realized that I was already more than half done with it, which is impressive since it is a 500+ page book.  It is very refreshing to read a book like this that talks to me as an in-the-trenches software developer with no aspirations to become great.  So many of these books talk about how to become an expert in your career and take things to the next level.  This book has some of that but more of it is just tips and tricks on how I can refine what I am doing now to be just that little bit better than I was before.  It’s definitely worth picking up a copy.

MVC: Check if Is Dirty via #JavaScript

I had a need to do some fancy work on a MVC Razor page enabling and disabling buttons based on various criteria.  To do this I needed to check to see if the user had changed any data on the page I ended up using the following structure to do it.

First set up your page with a form and all your input fields.

@using(Html.BeginForm("actionname", "controllername", new { area = "areaname" }, 
       FormMethod.Post, new { id = "saveForm", @class = "disable-submit-form" }))
{
     /* lots of page code */
}

This method serializes the data into a Name, Value array of objects  and hashes it.

     // Grab all the data fields in the code and serialize them, then hash them
     // @Html.Checkbox and CheckBoxFor need to be handled as a separate case 
     function GetSerializedItems() {
        var serializedArray = $(".disable-submit-form").serializeArray()
                                 .filter(function (item) { 
                                           return item.name != 'CheckboxItem1' 
                                               && item.name != 'CheckboxItem2'; 
                                         });
        var serializedCheckboxItems = $('input:checkbox').map(
                                        function () { 
                                           return { name: this.name, 
                                                    value: this.checked ? this.value 
                                                                        : "false" }; 
                                        });
        for (var index = 0; index < serializedCheckboxItems.length; ++index) {
           serializedArray.push(serializedCheckboxItems[index]);
        }
        return convertSerializedArrayToHash(serializedArray);
      }

      $(document).ready(function () {
        // Get the initial state of the data on the page.
        var startItems = GetSerializedItems(); 
       
        // On each field change recheck the current button state
        $('.disable-submit-form').on('change', function () {
           showhidebuttons();
        });

      function showhidebuttons() {
        var $form = $(".disable-submit-form");
        var currentItems = GetSerializedItems();
        var dirty = hashDiff(startItems, currentItems);
        var isDirty = countProperties(dirty) > 0;
        var isValid = $form.valid();
        var isDraft = '@Model.IsDraft' === 'True';

        $form.find(".submitform").attr('disabled', 'disabled');

        if (isValid && isDirty) {
          $form.find(".submitform").removeAttr('disabled');
        } else if (isDraft && isValid) {
          $form.find("#PreviewId").removeAttr('disabled');
          $form.find("#PublishId").removeAttr('disabled');
        }
      }
     }
     
     //Call ShowHideButtons to set the default state
     showhidebuttons();

Here are the javascript helpers I used.

 function convertSerializedArrayToHash(a) {
 var r = {};
 for (var i = 0; i < a.length; i++) {
 r[a[i].name] = a[i].value;
 }
 return r;
 }

 function hashDiff(h1, h2) {
 var d = {};
 for (k in h2) {
 if (h1[k] !== h2[k]) d[k] = h2[k];
 }
 return d;
 }

 function countProperties(obj) {
 var count = 0;

 for (var prop in obj) {
 if (obj.hasOwnProperty(prop))
 ++count;
 }

 return count;
 }