
Motivation
- 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 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 some aspects about the settings that are 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 customer-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.
Showdown vs. Showdown
<!-- 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>
Demo App
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 convention. 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.
Editor Defaults
Inline Layout
The layout of each is going to be the first problem we'll tackle. In some applications, there's the 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 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 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 JS 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 JS addons to help with this, but we're in TMS WEB Core so that's 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 setup 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;

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
Summernote Scenario
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");
procedure TForm1.SummernoteSave(HTML: WideString); begin console.log('User wants to save an HTML file that has '+IntToStr(Length(HTML))+' bytes'); end;


SunEditor Scenario
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>'
},
});


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
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;
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";

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

Styling
Download
Other Notes
- It is my understanding that the HTML Editor included in the 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.