Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
Summernote vs. SunEditor Showdown

Bookmarks: 

Tuesday, June 7, 2022

Photo of Andrew Simard

Last time out, we had a good look at CodeMirror 5, a JavaScript text editor that, among many other things, can be used to edit native HTML. This time out, we're going to tackle the general topic of editing HTML in TMS WEB Core projects using two very similar JavaScript libraries, where both provide a WYSIWYG HTML editing interface - Summernote and SunEditor. And if neither of those are to your liking, there's also a section at the end about how to convert Markdown text into HTML, using Showdown. Three different ways to tackle the same problem.


Motivation.

Why might you need a WYSIWYG HTML editor in the first place? There are at least a handful of places I can think of where it might (or has) come up in TMS WEB Core projects.

  • RichEditor. As a replacement for any needs that have historically been addressed using typical Windows RichEdit controls. Places where you want users to enter information, but where you also want them to be able to use different text colors, bold or italic text, lists, tables, images, links, and so on. While such RichEdit controls have included support for some or all of these kinds of features, a WYSIWYG HTML control can typically do all of this and more, and usually, you're turning off things rather than trying to add editing functionality.
  • Forums. If you have an area within your project where users can discuss things, like in a forum-type environment, this is sometimes a useful thing to have. Markdown is also popular for this kind of thing and is used to varying degrees in the TMS Support Center and on GitHub, keeping the UI as simple as possible.
  • Documentation. For example, on a settings form, there might be a header at the top that describes aspects of the settings that may be specific to a client. This description is stored as an HTML object in the client's database and is displayed, if available, instead of the description that might be used by default. Naturally, access to editing these descriptions would be restricted to appropriate people. Or even if it isn't client-specific, sometimes storing the documentation in a database like this makes it possible to update the documentation that might be static within the application, but without having to release a new code update to make changes.
  • Landing Pages. Customized web pages that cater to particular customers or marketing events or advertising campaigns. These are just web pages that you track to see how much traffic is being generated by a particular campaign. Or maybe you use them to A/B test what gets more conversions for a particular product. Or something like that. By being able to edit HTML directly within the application, these pages can all be managed from within your application - no need to worry about file access or server configurations or anything like that.
  • Other Content. Sometimes applications have a 'news feed' or an 'about' page or a 'contact us page' or maybe corporate profiles, that kind of thing. Having a built-in HTML editor makes it easy to keep this kind of content up-to-date and also secured in a way that would likely be more difficult if people were editing HTML files in some other application and then uploading them to your server, for example.

There are likely many other examples of how a WYSIWYG HTML editor could be useful. If you've ever run across a different use case, by all means, please post a comment about it. Would be great if we could generate more discussions at this kind of level - building better TMS WEB Core applications is what we're all trying to do here, after all.


Showdown vs. Showdown.

A curious use of words! I had long ago planned on titling this post "Showdown: Summernote vs. SunEditor" and having it just be about the comparison between Summernote and SunEditor. But as luck would have it, the JavaScript library that converts Markdown to HTML is actually called Showdown as well. How convenient! Getting started with all three involves the same steps we've covered many times before, tracking down a CDN link and adding it to your project.html. Or include it in your project directly. Or using the Manage JavaScript Libraries interface. And while you're likely never going to use all three in the same project, we're going to do just that. 


<!-- jQuery -->
<script src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"></script>
<!-- FontAwesome 5 -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.0/css/all.min.css" rel="stylesheet"/>
<!-- CodeMirror -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/mode/markdown/markdown.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/mode/css/css.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/mode/xml/xml.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/mode/htmlmixed/htmlmixed.js"></script>
<link href="https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.css" rel="stylesheet"/>
<!-- Katex -->
<script src="https://cdn.jsdelivr.net/npm/katex@latest/dist/katex.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/katex@latest/dist/katex.min.css" rel="stylesheet"/>
<!-- SunEditor -->
<script src="https://cdn.jsdelivr.net/npm/suneditor@latest/dist/suneditor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/suneditor@latest/src/lang/en.js"></script>
<link href="https://cdn.jsdelivr.net/npm/suneditor@latest/dist/css/suneditor.min.css" rel="stylesheet"/>
<!-- Summernote w/Bootstrap5 Support -->
<script src="https://cdn.jsdelivr.net/npm/summernote@latest/dist/summernote-bs5.min.js"></script>
<link  href="https://cdn.jsdelivr.net/npm/summernote@latest/dist/summernote-bs5.min.css" rel="stylesheet"/>
<!--  Showdown -->
<script src="https://cdn.jsdelivr.net/npm/showdown@latest/dist/showdown.min.js"></script>

Note: The jQuery dependency comes from Summernote. 


FontAwesome is used to first create some nicer buttons to use with CodeMirror, and later to show how the entire set of icons for both Summernote and SunEditor can easily be replaced.  

A (somewhat) basic CodeMirror setup is included, and we'll get to the implementation details of that shortly. Note that the CodeMirror modes included are needed for regular "markdown" editing, and also those that we'll be using when it is used as the HTML code editor for both Summernote and SunEditor. If we were just using CodeMirror without those, we could get by with just the markdown JavaScript file. And as we covered last time, we're specifically referencing codemirror@5 from the CDN here so that we don't get into trouble if the CodeMirror folks release their CodeMirror 6 via a CDN link at some future point.

Katex is a library for handling mathematical equations, which is (optionally) used by SunEditor. Note that in terms of ordering, CodeMirror and Katex should come before the others so that Summernote and SunEditor are aware of them when their scripts are initialized. If you add Katex after SunEditor, for example, Katex support will not be available.

For SunEditor, the only extra bit is the language file used. There are a fair number of additional languages available.  

For Summernote, there are a handful of different distributions. For example, lite vs. regular vs. bootstrap variations.  Bootstrap support until recently was a little less than spectacular, but we'll give it a shot now and see how things turn out. Previously, I've used the lite version without problems. So if you run into any hiccups with any other variants, that might be a good choice. Note also that the jQuery dependency is due to Summernote and not anyone else.

And finally, for Showdown, there's not really much to do but include a reference to the library.


Demo App.

For demo purposes, I'm using a fresh TMS WEB Bootstrap Application template with three TWebHTMLDiv components that will hold each of the editors to start with. We'll expand on that shortly, but to get started we can drop the three TWebHTMLDiv components on a TWebForm and give them a name and ElementID of divSummernote, divSunEditor, and divCodeMirror. Then in the form's WebFormCreate procedure, we can do some basic initialization for each editor like this.


unit Unit1;

interface uses System.SysUtils, System.Classes, JS, Web,
WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, WEBLib.WebCtrls;

type
  TForm1 = class(TWebForm)
    divSummernote: TWebHTMLDiv;
    divSunEditor: TWebHTMLDiv;
    divCodeMirror: TWebHTMLDiv;
    procedure WebFormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.WebFormCreate(Sender: TObject);
begin
  asm
    $('#divSummernote').summernote();
    SUNEDITOR.create('divSunEditor',{});
    CodeMirror(document.getElementById('divCodeMirror'), {mode: "markdown"});
  end;
end;

end.

They each have their own particular format. Summernote uses jQuery, so the link to an element is done using the normal jQuery conventions. SunEditor is a little different, but passing it an ElementID gets it on its way easily enough. And CodeMirror does the lookup explicitly but works the same way. Each has a host of options that can be passed in right at the outset, but we're just using the defaults here, with the exception that in the case of CodeMirror, we want to specify Markdown as its mode.

The result is that we've got a page with three different editors, each with its own preferences about layout (for example, only CodeMirror has any respect for the div that it is contained by), default icons, and so on. Kind of a mess to start with, but we'll get it cleaned up in no time at all.  Here's what they look like with no options specified.

TMS Software Delphi  Components

Editor Defaults.

Inline Layout.

The layout of each is going to be the first problem we'll tackle. In some applications, there's a need for an "inline" version of this kind of editor control. For example, if you're replying to a comment in a forum thread, you most likely expect to see the editor appear in place, with the buttons just above where you're editing, along with the option of expanding the editing area by dragging a corner or an edge to give yourself more room for whatever it is that you're editing. 

This is the default mode for Summernote, and you can see the little draggable icon in the bottom center of the control. Dragging this up or down will automatically move the SunEditor section below it up or down as well. And the Summernote control takes up the full width of the page rather than the full width of the divSummernote container, ignoring it entirely for the most part, as it creates a root-level element to do its work by default.

To get SunEditor to (mis)behave in this same way, we can pass it a couple of options. It already creates its own root-level element outside of its divSunEditor container. We can help it get the rest of the way by adding something to tell it to take up the full width and to set a default starting height. This is enough to enable its resizingBar to act in the same way as the Summernote bar, albeit without the icon in the middle.

    SUNEDITOR.create('divSunEditor',{
      width: 'auto',
      height: 200
    });

For CodeMirror, it does respect the divCodeMirror that it is in, so we just need to set some options on that to have it behave in a similar way. And a bit of CSS can be used to help that out, or it can be set directly in the code. 

    var cm = document.getElementById('divCodeMirror');
    CodeMirror(document.getElementById('divCodeMirror'), {
      mode: "markdown"
    });
    document.getElementById('divCodeMirror').classList.add('w-100');
    document.getElementById('divCodeMirror').firstElementChild.style.minHeight = "200px";
    document.getElementById('divCodeMirror').firstElementChild.style.resize = "vertical";
    document.getElementById('divCodeMirror').firstElementChild.style.overflow = "auto !important";

We'd also like to have a similar set of buttons, even if we have to write the code ourselves. There are other JavaScript libraries that provide an out-of-the-box CodeMirror editor experience, but we'll do the legwork here to show how it is done. Not too difficult. We'll add the first of our extra elements here to hold the buttons for CodeMirror, divCodeMirrorPanel, just as another TWebHTMLDiv above the existing divCodeMirror div. CodeMirror has some additional JavaScript add-ons to help with this, but using TMS WEB Core we can do the same thing without much effort, so those are not really needed. We can add a panel and some buttons, no trouble at all. The trick is having the buttons do something. The panel itself can be set up using a bunch of different Bootstrap classes added to its ElementClassName property, like this:


    d-flex flex-row m-0 p-1 ps-2  bg-light w-100 border border-black-50

And the individual buttons can be set using a FontAwesome icon by using a TWebButton with a Caption property like these:   

    <i class="fas fa-bold"></i>
    <i class="fas fa-underline"></i>
    <i class="fas fa-italic"></i>


And to make the buttons look a bit nicer, they all have the same ElementClassName property, easily adjusted to suit your personal preferences:


    btn bg-white border border-secondary  mx-1

For the buttons to work, we'll need to add some basic code to them. We're not trying to be too fancy here, just replacing selected text with the Markdown equivalent in case someone is new to Markdown or has forgotten what characters to use. For Bold, Underline, and Italic, we can do it like this, which is a modified version of the CodeMirror Buttons GitHub project.

procedure TForm1.btnCodeMirrorBoldClick(Sender: TObject);
begin
  asm
    var selection = this.cmeditor.getSelection();
    this.cmeditor.replaceSelection('**' + selection + '**');
    if (!selection) {
      var cursorPos = this.cmeditor.getCursor();
      this.cmeditor.setCursor(cursorPos.line, cursorPos.ch - 2);
    }
  end;
end;

procedure TForm1.btnCodeMirrorItalicClick(Sender: TObject);
begin
  asm
    var selection = this.cmeditor.getSelection();
    this.cmeditor.replaceSelection('*' + selection + '*');
    if (!selection) {
      var cursorPos = this.cmeditor.getCursor();
      this.cmeditor.setCursor(cursorPos.line, cursorPos.ch - 2);
    }
  end;
end;

procedure TForm1.btnCodeMirrorUnderlineClick(Sender: TObject);
begin
  asm
    var selection = this.cmeditor.getSelection();
    this.cmeditor.replaceSelection('<span style="text-decoration: underline">' + selection + '</span>');
    if (!selection) {
      var cursorPos = this.cmeditor.getCursor();
      this.cmeditor.setCursor(cursorPos.line, cursorPos.ch - 2);
    }
  end;
end;

Note that Markdown doesn't really have an underline equivalent, so here we're just jamming something in that will eventually get converted into HTML easily enough. Just including it here as an example of what you could do when trying to keep pace with the different editors we're dealing with. But with those out of the way, we now have things working a little better, with each editor being resizable and flowing nicely down the page.  


TMS Software Delphi  Components
More Consistent Editor Presentation.

The other 'gotcha' that will come up in all three editors at some point, is that when you call Delphi functions from JavaScript event handlers or other callback functions, the context that JavaScript is in at that stage isn't where it normally is - it forgets that you're on a form, and things like 'this.' don't work anymore. This comes up when defining new hotkeys for CodeMirror, for example, where the Delphi code doesn't know what this.cdmeditor is, so we instead reference it more explicitly. After all that, we've now got the following code to round out this section.

unit Unit1;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, WEBLib.WebCtrls,
  Vcl.StdCtrls, WEBLib.StdCtrls;

type
  TForm1 = class(TWebForm)
    divSummernote: TWebHTMLDiv;
    divSunEditor: TWebHTMLDiv;
    divCodeMirror: TWebHTMLDiv;
    divCodeMirrorPanel: TWebHTMLDiv;
    btnCodeMirrorBold: TWebButton;
    btnCodeMirrorUnderline: TWebButton;
    btnCodeMirrorItalic: TWebButton;
    procedure WebFormCreate(Sender: TObject);
    procedure btnCodeMirrorBoldClick(Sender: TObject);
    procedure btnCodeMirrorUnderlineClick(Sender: TObject);
    procedure btnCodeMirrorItalicClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    cmeditor: JSValue;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btnCodeMirrorBoldClick(Sender: TObject);
var
  cm: JSValue;
begin
  cm := Form1.cmeditor;
  asm
    var selection = cm.getSelection();
    cm.replaceSelection('**' + selection + '**');
    if (!selection) {
      var cursorPos =cm.getCursor();
      cm.setCursor(cursorPos.line, cursorPos.ch - 2);
    }
  end;
end;

procedure TForm1.btnCodeMirrorItalicClick(Sender: TObject);
var
  cm: JSValue;
begin
  cm := Form1.cmeditor;
  asm
    var selection = cm.getSelection();
    cm.replaceSelection('*' + selection + '*');
    if (!selection) {
      var cursorPos =cm.getCursor();
      cm.setCursor(cursorPos.line, cursorPos.ch - 2);
    }
  end;
end;

procedure TForm1.btnCodeMirrorUnderlineClick(Sender: TObject);
var
  cm: JSValue;
begin
  cm := Form1.cmeditor;
  asm
    var selection = cm.getSelection();
    cm.replaceSelection('<span style="text-decoration: underline">' + selection + '</span>');
    if (!selection) {
      var cursorPos =cm.getCursor();
      cm.setCursor(cursorPos.line, cursorPos.ch - 2);
    }
  end;
end;

procedure TForm1.WebFormCreate(Sender: TObject);
begin
  asm

    $('#divSummernote').summernote({
      height: 200,
      minHeight: 100
    });

    SUNEDITOR.create('divSunEditor',{
      width: 'auto',
      height: 200,
      minHeight: 100
    });

    this.cmeditor = CodeMirror(divCodeMirror, {
      mode: "markdown",
      lineNumbers: true
    });

    var boldclick = this.btnCodeMirrorBoldClick;
    this.cmeditor.addKeyMap({"Ctrl-B":function (cm) { boldclick() }});
    var underlineclick = this.btnCodeMirrorBoldClick;
    this.cmeditor.addKeyMap({"Ctrl-I":function (cm) { underlineclick() }});
    var italicclick = this.btnCodeMirrorBoldClick;
    this.cmeditor.addKeyMap({"Ctrl-U":function (cm) { italicclick() }});

    divCodeMirror.classList.add('w-100');
    divCodeMirror.firstElementChild.style.height = "200px";
    divCodeMirror.firstElementChild.style.minHeight = "100px";
    divCodeMirror.firstElementChild.style.resize = "vertical";
    divCodeMirror.firstElementChild.style.overflow = "auto !important";
  end;
end;

end.

More Buttons.

Next up, let's talk about buttons, generally. We've just seen what was necessary to get buttons for CodeMirror to behave the way we'd like, but fortunately, with Summernote and SunEditor, there are a ton of buttons already defined. And there are even more buttons possible, all easily customizable. They both work pretty much the same way, with a few minor differences. And, in both cases, they call all be replaced with other icons. I'm a big fan of FontAwesome DuoTone icons for example, as they make things look a little nicer. But regular FontAwesome icons work just as well. Both also support button groups, so similar buttons can be collected together. And with a little bit of effort, they will also wrap when the available space is less than the width of all of the buttons.

Beyond the standard buttons, there's also generally a need to add custom buttons. For example, both Summernote and SunEditor have undo/redo capabilities. I also tend to use these with a database backend, so being able to "save to database" is a natural thing to want to do, but only if the content has changed. So having some custom functionality can be pretty important, along with some custom icons to personalize things a little bit.

Summernote Scenario.

For Summernote, this is what a more fully defined solution might look like. Custom buttons are defined, along with some extra functionality for changing the color of the save icon when the document is edited. Also, there are some weird shenanigans to do with carets in the final result, and some super-sketchy code to address it. It works, what can I say? In any event, here's the asm block needed. 
 
    var undoButton = function (context) {
      var ui = $.summernote.ui;
      var button = ui.button({
        contents: '<i class="fas fa-undo"/> Undo',
        tooltip: 'Undo'
      });
      return button.render();
    };

    var redoButton = function (context) {
      var ui = $.summernote.ui;
      var button = ui.button({
        contents: '<i class="fas fa-redo"/> Redo',
        tooltip: 'Redo',
      });
      return button.render();
    };

    var SummernoteSave = this.SummernoteSave;
    var saveButton = function (context) {
      var ui = $.summernote.ui;
      var button = ui.button({
        contents: '<div id="SummernoteSave"><i class="fas fa-check"></div>',
        tooltip: 'Save Changes',
        id: "SummernoteSave",
        container: $('.note-editor.note-frame'),
        click: function () {
          var HTML = $('#divSummernote').summernote('code');
            SummernoteSave(HTML);
        }
      });
      return button.render();
    };

    $('#divSummernote').summernote({
      height: 200,
      minHeight: 100,
      buttons: {
        undo: undoButton,
        redo: redoButton,
        save: saveButton,
      },
      toolbar: [
        ['edit', ['undo','save','redo']],
        ['style', ['style']],
        ['font', ['bold', 'underline', 'clear']],
        ['fontname', ['fontname']],
        ['color', ['color']],
        ['para', ['ul', 'ol', 'paragraph']],
        ['table', ['table']],
        ['insert', ['link', 'picture', 'video']],
        ['view', ['fullscreen', 'codeview', 'help']]
      ],
      icons: {
        align:         'fas fa-align',
        alignCenter:   'fas fa-align-center',
        alignJustify:  'fas fa-align-justify',
        alignLeft:     'fas fa-align-left',
        alignRight:    'fas fa-align-right',
        indent:        'fas fa-indent',
        outdent:       'fas fa-outdent',
        arrowsAlt:     'fas fa-expand',
        bold:          'fas fa-bold',
        caret:         'fas fa-caret-down',
        circle:        'fas fa-circle',
        close:         'fas fa fa-close',
        code:          'fas fa-code',
        eraser:        'fas fa-eraser',
        font:          'fas fa-font',
        italic:        'fas fa-italic',
        link:          'fas fa-link',
        unlink:        'fas fa-chain-broken',
        magic:         'fas fa-magic',
        menuCheck:     'fas fa-check',
        minus:         'fas fa-minus',
        orderedlist:   'fas fa-list-ol',
        pencil:        'fas fa-pencil',
        picture:       'fas fa-image',
        question:      'fas fa-question',
        redo:          'fas fa-redo',
        square:        'fas fa-square',
        strikethrough: 'fas fa-strikethrough',
        subscript:     'fas fa-subscript',
        superscript:   'fas fa-superscript',
        table:         'fas fa-table',
        textHeight:    'fas fa-text-height',
        trash:         'fas fa-trash',
        underline:     'fas fa-underline',
        undo:          'fas fa-undo',
        unorderedlist: 'fas fa-list-ul',
        video:         'fas fa-video-camera'
      },
    });

    $('#divSummernote').on('summernote.change', function(we, contents, $editable) {
      document.getElementById('SummernoteSave').style.color = "green";
    });

    // Fixes the duplicate carets issue
    var styleEle = $("style#fixed");
    if (styleEle.length == 0)
      $("<style id=\"fixed\">.note-editor .dropdown-toggle::after { all: unset; } .note-editor .note-dropdown-menu { box-sizing: content-box; } .note-editor .note-modal-footer { box-sizing: content-box; }</style>")
      .prependTo("body");

The Delphi bit we need to add to finish this off is just the SummernoteSave procedure. SummernoteSave just takes the HTML produced in the editor and does something with it. Maybe save it to a database, for example. Note that this can get quite large if users copy and paste images directly into the document, as these can be rendered as Base64-encoded elements in the document itself. Having an HTML document that exceeds several megabytes is entirely possible.


procedure TForm1.SummernoteSave(HTML: WideString);
begin
  console.log('User wants to save an HTML file that has '+IntToStr(Length(HTML))+' bytes');
end;

If you're a FontAwesome Pro user, you can use DuoTone icons. Here's a comparison. Naturally, you can change all of these to anything you like.


TMS Software Delphi  Components

Summernote with FontAwesome 5 Solid Icons.

TMS Software Delphi  Components

Summernote with FontAwesome 6 DuoTone Icons.


SunEditor Scenario.

All the same kinds of things apply here to SunEditor as they do to Summernote, just expressed a little differently, and none of the caret shenanigans.

   var SunEditorSave = this.SunEditorSave;
    SUNEDITOR.create('divSunEditor',{
      width: 'auto',
      height: 200,
      minHeight: 100,
      katex: katex,
      callBackSave: function (contents, isChanged) {
        SunEditorSave(contents);
      },
      buttonList: [
        ['undo', 'save', 'redo'],
        ['font', 'fontSize', 'formatBlock'],
        ['paragraphStyle', 'blockquote','horizontalRule'],
        ['bold', 'underline', 'italic', 'strike', 'subscript', 'superscript', 'math'],
        ['fontColor', 'hiliteColor', 'textStyle', 'removeFormat'],
        ['list', 'outdent', 'indent', 'align', 'lineHeight'],
        ['table', 'link', 'image', 'video', 'audio'],
        ['fullScreen','showBlocks', 'codeView', 'print']
      ],
      icons: {
        undo: '<span><i class="fas fa-undo"></i></span>',
        save: '<span class="HTMLSave" style="margin-top: -2px;"><i class="fas fa-check fa-xl"></i></span>',
        redo: '<span><i class="fas fa-redo"></i></span>',
        paragraph_style: '<span><i class="fas fa-paragraph"></i></span>',
        blockquote: '<span><i class="fas fa-quote-left"></i></span>',
        horizontal_rule: '<span><i class="fas fa-horizontal-rule"></i></span>',
        bold: '<span><i class="fas fa-bold"></i></span>',
        underline: '<span><i class="fas fa-underline"></i></span>',
        italic: '<span><i class="fas fa-italic"></i></span>',
        strike: '<span><i class="fas fa-strikethrough"></i></span>',
        subscript: '<span><i class="fas fa-subscript"></i></span>',
        superscript: '<span><i class="fas fa-superscript"></i></span>',
        math: '<span><i class="fas fa-abacus"></i></span>',
        font_color: '<span><i class="fas fa-pen-nib"></i></span>',
        highlight_color: '<span><i class="fas fa-highlighter"></i></span>',
        text_style: '<span><i class="fas fa-text"></i></span>',
        erase: '<span><i class="fas fa-eraser"></i></span>',
        list_bullets: '<span><i class="fas fa-list"></i></span>',
        list_number: '<span><i class="fas fa-list-ol"></i></span>',
        outdent: '<span><i class="fas fa-indent"></i></span>',
        indent: '<span><i class="fas fa-outdent"></i></span>',
        align_left: '<span><i class="fas fa-align-left"></i></span>',
        align_right: '<span><i class="fas fa-align-right"></i></span>',
        align_justify: '<span><i class="fas fa-align-justify"></i></span>',
        align_center: '<span><i class="fas fa-align-center"></i></span>',
        line_height: '<span><i class="fas fa-text-height"></i></span>',
        table: '<span><i class="fas fa-table"></i></span>',
        link: '<span><i class="fas fa-link"></i></span>',
        image: '<span><i class="fas fa-image"></i></span>',
        video: '<span><i class="fas fa-video"></i></span>',
        audio: '<span><i class="fas fa-microphone"></i></span>',
        expansion: '<span><i class="fas fa-expand"></i></span>',
        reduction: '<span><i class="fas fa-compress"></i></span>',
        show_blocks: '<span><i class="fas fa-tasks-alt"></i></span>',
        code_view: '<span><i class="fas fa-code"></i></span>',
        print: '<span><i class="fas fa-print"></i></span>'
      },
    });

This perhaps looks a little more convoluted than Summernote at first glance, but it's actually a bit simpler as the redo/undo/save functions are already part of the mix, just needing a little push over the finish line. There are also more icons by default, in part because of how they're organized. The same customizations are possible using either FontAwesome set, or even other SVG icons entirely (as can be done with Summernote as well).

TMS Software Delphi  Components

SunEditor with FontAwesome 5 Solid Icons.

TMS Software Delphi  Components

SunEditor with FontAwesome 6 DuoTone Icons.


The save mechanism also works very much the same way. For our demo purposes, the Delphi function is basically identical.

procedure TForm1.SunEditorSave(HTML: WideString);
begin
  console.log('User wants to save an HTML file that has '+IntToStr(Length(HTML))+' bytes');
end;


Markdown/Showdown Scenario.

In this case, we'll need a button for saving, and the function it calls will need to do the conversion from Markdown to HTML. We'll use the same icon as with Summernote and SunEditor, but instead of doing replacements like with the other CodeMirror buttons, we'll do a conversion. And, just for fun, we'll update the other two controls with whatever we get from CodeMirror.

procedure TForm1.btnCodeMirrorSaveClick(Sender: TObject);
var
  cm: JSValue;  // CodeMirror Instance
  se: JSValue;  // SunEditor Instance;
begin
  cm := Form1.cmeditor;
  se := Form1.suneditor;
  asm
    var markdown = cm.getValue();
    var converter = new showdown.Converter();
    var html = converter.makeHtml(markdown);
    $("#divSummernote").summernote('code',html);
    se.setContents(html);
  end;
end;


The WebFormCreate CodeMirror creation code now looks like this.

    this.cmeditor = CodeMirror(divCodeMirror, {
      mode: "markdown",
      lineNumbers: true
    });
    var boldclick = this.btnCodeMirrorBoldClick;
    this.cmeditor.addKeyMap({"Ctrl-B":function (cm) { boldclick() }});
    var underlineclick = this.btnCodeMirrorBoldClick;
    this.cmeditor.addKeyMap({"Ctrl-I":function (cm) { underlineclick() }});
    var italicclick = this.btnCodeMirrorBoldClick;
    this.cmeditor.addKeyMap({"Ctrl-U":function (cm) { italicclick() }});
    var saveclick = this.btnCodeMirrorSaveClick;
    this.cmeditor.addKeyMap({"Ctrl-S":function (cm) { saveclick() }});
    divCodeMirror.classList.add('w-100');
    divCodeMirror.firstElementChild.style.height = "200px";
    divCodeMirror.firstElementChild.style.minHeight = "100px";
    divCodeMirror.firstElementChild.style.resize = "vertical";
    divCodeMirror.firstElementChild.style.overflow = "auto !important";

When adding some sample Markdown and then clicking the Markdown check, the contents of CodeMirror are converted to HTML and then added to SummerNote and SunEditor, which might look something like the following.

TMS Software Delphi  Components
Copying Markdown to HTML.

Note that in Markdown, you don't need to properly order an ordered list - it will figure out the numbers for you.  Also, the Showdown JavaScript library can convert from HTML back to Markdown as well, if you find that you need that feature.

Code View.

Both Summernote and SunEditor can make use of CodeMirror directly when viewing the underlying HTML code.  Line numbers, themes, and all the rest of CodeMirror can be used, simply by specifying its own options in the definition of the editor. Summernote by default has reasonable CodeMirror defaults. In SunEditor you can get the same effect using an extra definition, something like this.

   this.suneditor = SUNEDITOR.create('divSunEditor',{
      width: 'auto',
      height: 200,
      minHeight: 100,
      codeMirror: {
        src: CodeMirror,
        options: {
          lineNumbers: true
        }
      }
    });

Then, when you're looking at Code View for each editor, and then copy over the CodeMirror markdown, it might look like this:


TMS Software Delphi  Components

Using CodeMirror as the "code view" of both Summernote and SunEditor.

Styling.

We covered quite a bit about CodeMirror styling last time out. Both Summernote and SunEditor can be styled as well, with endless CSS overrides possible. However, the bigger issue is about the CSS that is used, both to apply to the editor as you're using it, as well as to the HTML that is generated. As can be seen above, they default to different styles to begin with.

SunEditor even has options that can prohibit certain HTML tags from being used in the first place, which can be hugely beneficial but also bothersome if you happen to need those tags and aren't aware of what is being excluded.  Both editors are also likely to rewrite your HTML code when it decides there's a more optimal way to display it. So getting them ultimately to behave like the WYSIWYG editors they are sometimes can be a bit tricky. but entirely possible. 

If you have your own themes or CSS rules defined, you'll want to be sure to apply them to the editor of your choice so that when people are using it, they can have the same rules applied.

Download.

The source code of the project presented here can be downloaded here.


Other Notes.

One could probably write a book about all the various ins and outs of both of these editors, but here are some final thoughts to keep in mind.

  • The HTML Editor included in the TMS WX pack is indeed Summernote, ready to go as a TMS WEB Core component.
  • There are many other editors out there like CKEditor, TinyMCE, Froala, and so on. They used to be considerably less expensive.
  • Summernote and SunEditor have plenty of support for images, video, and so on. SunEditor even has a gallery plugin available.
  • SunEditor can make use of Katex for editing algebraic expressions.
  • Both can be used in a compact mode, where the toolbar buttons only appear when needed.
  • Both can be used in a more traditional mode, where the size of the editing window is fixed.


Which is best?

Tough call. I used SunEditor initially for a long time. I've recently started to use Summernote in a few projects. I tend to personally prefer SunEditor, but general users who are just editing documents seem to have less trouble with Summernote. Summernote might be more forgiving when editing certain kinds of advanced HTML blocks, but honestly, it is hard to say. From a developer perspective, I actually see a lot more development work going into SunEditor than Summernote, just based on the frequency of GitHub activity. And there's a big v3 release of SunEditor due in the next short while, whereas Summernote has been languishing around v0.8 for quite some time. Not that it matters if it works great already, just different things to think about.

Tabulator On Deck.

Next time out, we'll finally start to get into Tabulator. Definitely won't get through it in one post. Or even two or three. So stay tuned. And as always, I'd like to get more feedback on how things are going and what things you find interesting. Suggestions for future topics. Questions about past topics. Whatever you like!

Follow Andrew on 𝕏 at @WebCoreAndMore or join our
𝕏
Web Core and More Community.



Andrew Simard


Bookmarks: 

This blog post has received 4 comments.


1. Thursday, November 16, 2023 at 7:26:58 AM

Hey Andrew, great posting, may I ask how to get to save the html content? I know that there is a save button on the tool bar, but I would like to put a Delphi button outside of the divsummernote, that when clicked, it would save the html to variable or string. I have tried dvisummerset.html.text but it is always blank, even after I do a save? Also, how would I clear the html? I want to save the html like above, but when I open it a second time, I would like the html contents to be empty. I have tried divsummernote.HTML.Clear; but that fails to clear the contents. Thanks

Lawrence Green


2. Friday, November 17, 2023 at 2:08:17 PM

Give these a try? Summernote uses JQuery, so you can call functions from JavaScript as follows.

procedure TForm1.buttonClearClick(Sender: TObject);
begin
asm
$(''#divSummernote'').summernote(''reset'');
end;
end;

procedure TForm1.buttonSaveClick(Sender: TObject);
var
HTML: String;
begin
asm
HTML = $(''#divSummernote'').summernote(''code'');
end;
console.log(''User wants to save an HTML file that has ''+IntToStr(Length(HTML))+'' bytes'');
end;


Andrew Simard


3. Friday, November 17, 2023 at 4:35:02 PM

Hey Andrew, that worked great! Interesting the lines appears to be a double quote marks in your code copies to be 2 single quote marks, which fails. A double quote also fails, but if the 2 single quotes are changed to a single quote, then it works great. Thank You!

Green Lawrence N


4. Friday, November 17, 2023 at 4:37:40 PM

Glad to hear it! I think the quoting thing is just related to how comments here are transformed when posted here. Not really well suited for posting code. Even in regular comments, if I''m not careful, single quotes get converted, like that one just now!

Andrew Simard




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post