TMS Web Core and SunEditor

This post is about how I've managed to very successfully make use of the SunEditor JS library in my TMS Web Core project.

Motivation: I was looking for a simple HTML editor to allow users to create custom pseudo-pages with things like tables and images, ordered and unordered lists, and the ability to edit the source code directly to make finer adjustments, etc. There are a lot of HTML editors out there, many very solid products. However, I also didn't want to pay any per-user type licensing fees, and ideally no fees of any kind as my project currently hasn't generated any revenue. This of course narrows the list considerably.

Side note: I set this up before TMS released their HTML component. I don't know how it compares to SunEditor in terms of features.

So. SunEditor. This fits the bill nicely. And is available on github and the developer is quite responsive and even implemented an improvement I had requested. The SunEditor website has a pretty good demo showing its main features so no need to go into them here. Of particular interest is that it integrates CodeMirror for its source editing and it also integrates with Katex for handling math formatting.

So to get started, a bunch of entries need to be added to the Project.html file as per usual. These can be linked directly from a CDN or incorporated more directly, as usual.

    <!-- CodeMirror -->
    <script src="https://cdn.jsdelivr.net/npm/codemirror@latest/lib/codemirror.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/codemirror@latest/mode/htmlmixed/htmlmixed.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/codemirror@latest/lib/codemirror.min.css" rel="stylesheet"/>
    <!-- Katex -->
    <link href="https://cdn.jsdelivr.net/npm/katex@latest/dist/katex.min.css" rel="stylesheet"/>
    <script src="https://cdn.jsdelivr.net/npm/katex@latest/dist/katex.min.js"></script>
    <!-- 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"/>

Next up, drop a TWebHTMLDiv component and assign it an ElementID like HTMLEditor. Also a good idea to define a reference variable so you can get at it later. Getting started is then as simple as this.

// form variable
var sunEditor: JSValue;

// WebFormCreate or wherever you need it
asm 
  this.sunEditor = SUNEDITOR.create('HTMLEditor',{
    lang: SUNEDITOR_LANG.en,
    codeMirror: {
      src: CodeMirror
    },
    katex: katex
  });
end;

But that's just getting started. There are a lot of other things that can be done, and an endless amount of customization. In particular, you can customize the editor toolbar location, icons, and the rest of it. In my project, I'm a big fan of FontAwesome's Duo icons so I wanted to use those. And I wanted the editor to be constrained within a DIV that is resized separately from SunEditor. This was a bit tricky as the toolbar and the resizing bar at the bottom default to a sceario where it is just in a free-flowing page. So here's what it looks like in a fully fleshed out implementation. Some extra TWebHTMLDiv's were added to hold the toolbar and resizing bar so they could be moved independently.

// form variable
var sunEditor: JSValue;

// WebFormCreate or wherever you need it
  
 asm 
    this.sunEditor = SUNEDITOR.create('HTMLEditor',{
      mode: "classic",
      lang: SUNEDITOR_LANG.en,
      toolbarContainer: "#HTMLEditorToolbar",
      resizingBar: true,
      resizingBarContainer: "#HTMLEditorResizingBar",
      charCounter: true,
      charCounterLabel: "Character Count: ",
      showPathLabel: true,
      fullScreenOffset: 67,
      codeMirror: {
        src: CodeMirror,
        options: {
          theme: "custom",
          scrollbarStyle: "overlay"
        }
      },
      katex: katex,
      callBackSave: function (contents, isChanged) {
        pas.CoreDataModule.DM1.SaveHTML(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="fad fa-paragraph"></i></span>',
        blockquote: '<span><i class="fas fa-quote-left"></i></span>',
        horizontal_rule: '<span><i class="fad fa-horizontal-rule"></i></span>',

        bold: '<span><i class="fad fa-bold"></i></span>',
        underline: '<span><i class="fad fa-underline"></i></span>',
        italic: '<span><i class="fad fa-italic"></i></span>',
        strike: '<span><i class="fad fa-strikethrough"></i></span>',
        subscript: '<span><i class="fad fa-subscript"></i></span>',
        superscript: '<span><i class="fad fa-superscript"></i></span>',
        math: '<span><i class="fad fa-abacus"></i></span>',

        font_color: '<span><i class="fad fa-pen-nib"></i></span>',
        highlight_color: '<span><i class="fad fa-highlighter"></i></span>',
        text_style: '<span><i class="fad fa-text"></i></span>',
        erase: '<span><i class="fad fa-eraser"></i></span>',

        list_bullets: '<span><i class="fad fa-list"></i></span>',
        list_number: '<span><i class="fad fa-list-ol"></i></span>',
        outdent: '<span><i class="fad fa-indent"></i></span>',
        indent: '<span><i class="fad fa-outdent"></i></span>',
        align_left: '<span><i class="fad fa-align-left"></i></span>',
        align_right: '<span><i class="fad fa-align-right"></i></span>',
        align_justify: '<span><i class="fad fa-align-justify"></i></span>',
        align_center: '<span><i class="fad fa-align-center"></i></span>',
        line_height: '<span><i class="fad fa-text-height"></i></span>',

        table: '<span><i class="fad fa-table"></i></span>',
        link: '<span><i class="fad fa-link"></i></span>',
        image: '<span><i class="fad fa-image"></i></span>',
        video: '<span><i class="fad fa-video"></i></span>',
        audio: '<span><i class="fad fa-microphone"></i></span>',

        expansion: '<span><i class="fad fa-expand"></i></span>',
        reduction: '<span><i class="fad fa-compress"></i></span>',
        show_blocks: '<span><i class="fad fa-tasks-alt"></i></span>',
        code_view: '<span><i class="fad fa-code"></i></span>',
        print: '<span><i class="fad fa-print"></i></span>'
      },
    });

end;

This covers most but not quite all of the functions supported. I don't have much need for image galleries for example, prefering instead just to use the inline image support that is there.

The callBackSave funciton is what I use to save the changes made to a page. The data is converted to Base64 and sent along to XData. The only caveat here is that I had trouble with the regular Base64 implementation somehow. But making use of another JS library seemed to fix it up. I posted about that here. Also be sure to protect the Delphi function that it calls from being optimized out of the final build if it is only called from within Javascript.

Loading data into the editor is simple as well. In my project, you can load up different HTML files, so the save button needs to be reset when this happens. It looks something like this.

      asm
        this.sunEditor.setContents(HTML);
        this.sunEditor.core.history.reset();
        this.sunEditor.core.context.tool.save.disabled = true;
      end;

Other notes.

  1. In my project the SunEditor is contained within a TWebHTML component which is itself contained within a TWebPageControl's TWebTabSheet. This works pretty well. I tried using the FNC equivalent and hit a roadblock where the context menus didn't work properly. Like there was a redraw needed between right-clicking and having the popup menu appear. I couldn't get it to work, but I had reverted to the non-FNC version so I didn't investigate further.
  2. The request I had made of the developer was related to having the bottom resizing bar appear in a different div so that it could be fixed at the bottom of the containing div rather than float with the content and disappear past the bottom on longer pages. The most recent update addressed this and it works well.

Are there any other (completely free) HTML editors that anyone has used successfully with TMS Web Core?

The one in TMS FNC WX Pack TMS FNC WX Pack Take advantage of proven extraordinary web technology from any type of Delphi or Object Pascal application is based on SummerNote https://summernote.org/
I know a customer has been working with TinyMCE : https://www.tiny.cloud/

Maybe the SunEditor can go to TMS Software | WEB Partners ? :wink:

interesting seeing someone working with TinyMCE. I have used that for years, it's very extensive - almost too much. I have been using Quill (https://quilljs.com/) which is very neat.

However, I have just started using the WX Pack (Summernote) in a desktop app and will probably use that in my webcore too.

Ah, very good. I had looked at SummerNote. Can't quite recall why SunEditor ended up ahead of it. Others like TinyMCE and CKEditor were also good candidates of course. Good to have alternatives.