/ngx-markdown-editor

Angular WYSIWYG Markdown editor, especially intended for users unfamiliar with the Markdown syntax.

Primary LanguageTypeScriptMIT LicenseMIT

Ngx Markdown Editor

Ngx Markdown Editor is a Angular library providing a WYSIWYG markdown editor, which is especially intended for users unfamiliar with the Markdown syntax. However, it is also well-suited for advanced users as it provides efficient ways to write Markdown, e.g. by using shortcuts or utilizing a preview-like markup theme to get immediate visual response of how the result will look like.

In addition, this markdown editor provides high extensibility and customizability as well as broad and simple options for internationalization.

Last but not least, by containing an opt-in material theme, this component will perfectly fit into your Angular Material application. If you do not use Angular Material you can easily integrate your own theme.

Demo available @ https://mdefy.github.io/ngx-markdown-editor/

Table of contents

Dependencies

This library depends on Markdown Editor Core and Ngx Markdown.

Markdown Editor Core is a JS library based on CodeMirror and was developed together with Ngx Markdown Editor. It provides the text editor and an extensive API for markdown-related actions and everything required to interact conveniently with the editor.

Ngx Markdown is used to provide a preview feature, that renders the Markdown text written in the editor.

Installation

Run

npm i @mdefy/ngx-markdown-editor

or

yarn add @mdefy/ngx-markdown-editor

Getting started

Include MarkdownEditorModule into your Angular module and include <ngx-markdown-editor></ngx-markdown-editor> into your HTML template.

Make sure to load Material Font, e.g. the header of your index.html file:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />

In order to use the material theme of Ngx Markdown Editor in combination with your global material theme (especially Angular Material), import the theme file into your styles.scss and include the material mixin, where you should pass your app's primary color. You can select from different material styles like in Angular Material's MatFormField using the materialStyle input property.

@import '~ngx-markdown-editor/themes/material';

@include mde-material(mat-color($your-primary-color));

In order to set specific dimension for Ngx Markdown Editor, simply apply any of the dimensional CSS properties to the component. Example:

ngx-markdown-editor {
  min-height: 100px;
  max-height: 500px;
}

Module configuration

In general, all configuration of the editor can be done dynamically through input bindings. However, as Ngx Markdown Editor utilizes the Ngx Markdown library for its preview feature and as the latter can be configured statically via NgModule import, MarkdownEditorModule also implements Angular's forRoot() and forChild() paradigma to make Ngx Markdown's configuration options available. For this, import MarkdownEditorModule as follows:

@NgModule({
  imports: [
    NgxMarkdownEditorModule.forRoot({
      previewConfig: {
        sanitize: ...
        markedOptions: ...
      }
    })
  ]
})

For detailed instructions how to configure Ngx Markdown, visit its dedicated documentation section on GitHub. Note, that we do not forward the loader option of MarkdownModuleConfig, as the [src] property of <ngx-markdown> is irrelevant for us.

If you like to use the same configuration for other MarkdownEditorComponent instances in sub modules, import the module with

@NgModule({
  imports: [ NgxMarkdownEditorModule.forChild() ]
})

Due to the fact that the MarkdownService Ngx Markdown is a singleton object, we cannot provide different preview configurations within one application so far. The most possible is to inject MarkdownService an make adjustments at run-time (see ngx-markdown#177). Apart from that, however, there are no dependencies between different Ngx Markdown Editor instances.

Component bindings

Inputs

Input Description Default value
data: string Data string to set as content of the editor. ''
options: Options Mainly options from Markdown Editor Core, including some adjustments. To update options at runtime, merge the old options object with the new options before applying the changes: this.options = { ...this.options, optionToUpdate: updateValue }. {}
toolbar: ToolbarItemDef[] Toolbar configuration. Can contain names of predefined items or objects of custom items. See toolbar section.
statusItems: StatusbarItemDef[] Statusbar configuration. Can contain names of predefined items or objects of custom items. See statusbar section.
showTooltips: boolean Specifies whether tooltips are shown for toolbar items. true
shortcutsInTooltips: boolean Specifies whether the applied keyboard shortcuts are included in the tooltips of toolbar items. true
materialStyle: boolean | 'standard' | 'fill' | 'legacy' Specifies whether or which Angular Material style is applied. false; for true, the default style is standard
label: string | undefined The label text for the editor component. The label area is hidden, if no label is specified. undefined
disabled: boolean Specifies whether the editor is disabled. In the disabled mode, the preview with rendered markdown is shown instead of the editor area. false
showToolbar: boolean Specifies whether the toolbar is rendered. true
showStatusbar: boolean Specifies whether the statusbar is rendered. true
required: boolean Specifies whether the editor component is a required field. If true, an asterisk (*) is added to the label. (Apart from that, this has no other effect.) false
language: LanguageTag Specifies the current language applied to all internationalizable properties. en

Outputs

The most important Codemirror events are transformed to Angular outputs to facilitate event binding.

Output Description
contentChange: ObservableEmitter<{ instance: Editor; changes: EditorChangeLinkedList[] }> Emits when the editor's content changes.
curserActivity: ObservableEmitter<{ instance: Editor }> Emits when the cursor is moved.
editorFocus: ObservableEmitter<{ instance: Editor; event: FocusEvent }> Emits when the editor receives focus.
editorBlur: ObservableEmitter<{ instance: Editor; event: FocusEvent }> Emits when the editor loses focus.

Toolbar

The toolbar is highly configurable and comes with many built-in items. You can simply

  • pick the ones you need,
  • define a custom order,
  • reconfigure existing items,
  • and even define whole new items (currently only buttons supported).

If you do not specify anything at all for the toolbar input property, the default toolbar setup is applied.

The toolbar input property in an array of type

type ToolbarItemDef = ToolbarItemName | ToolbarItem;

ToolbarItemName is a union type of all built-in item names. The interface ToolbarItem represents a full toolbar item.

interface ToolbarItem {
  name: string;
  action?: (...args: any[]) => void;
  shortcut?: string;
  isActive?: (...args: any[]) => boolean | number;
  tooltip?: OptionalI18n<string>;
  icon?: OptionalI18n<Icon>;
  disableOnPreview?: boolean;
}

For details about the OptionalI18n<T> type, see Internationalization section.

In the following, we always apply a JavaScript variable to the toolbar input property:

<ngx-markdown-editor [toolbar]="toolbar"></ngx-markdown-editor>

1. Construct a toolbar from existing items

To build a toolbar from existing items, simply create an array of type ToolbarItemName[] (or ToolbarItemDef[]) and specify the items by name. Additionally, there is a separator element, which you can insert at any position with '|'.

public toolbar: ToolbarItemName[] = ['toggleBold', 'toggleItalic', '|', 'insertLink', '|', 'openMarkdownGuide'];

The naming convention for items is to use the name of the function that is triggered by the item.

2. Configure an existing item

You can adjust built-in items for you needs by defining an item with a name included in ToolbarItemName. You do not have to specify all item properties, but can simply adjust only a subset of them, the rest will keep their default values.

For example, if you want to give the toggleBold item a new shortcut (default: Ctrl-B) as well as change the tooltip, then proceed as following:

const newToggleBoldItem: ToolbarItem = {
  name: 'toggleBold',
  shortcut: 'Alt-B',
  tooltip: 'Bold you shall be',
};

Then include this object into the toolbar item array (may be alongside ToolbarItemNames):

public toolbar: ToolbarItemDef[] = [newToggleBoldItem, 'toggleItalic', ...];

3. Create your own item

This is very similar to configuring an existing item. Example:

const myItem: ToolbarItem = {
  name: 'myCustomAction',
  action: () => myCustomAction()
  shortcut: 'Alt-B',
  tooltip: 'Hint: No need to hard code the shortcut here, see input `shortcutsInTooltips`',
  icon: {
    format: 'material'
    iconName: 'star'
  }
};

Again you only have to include the properties you want to explicitly specify (except the obligatory name property), all other item properties will be "as empty as possible" per default:

const defaultItem: ToolbarItem = {
  name: '',
  action: () => {},
  shortcut: undefined,
  isActive: undefined,
  tooltip: '',
  icon: { format: 'material', iconName: '' },
  disableOnPreview: false,
};

Note, that although there is the built-in setHeadingLevel dropdown item, so far only custom button items can be constructed in the described way (setHeadingLevel is implemented as a special case). This should satisfy most cases. If you require other items as well, you are welcome to fork this repo and/or make a pull request.

Shortcuts

The default keymap is as follows (on Mac "Ctrl" is replaced with "Cmd"):

Action Shortcut
setHeadingLevel Shift-Ctrl-Alt-H
toggleHeadingLevel Alt-H
increaseHeadingLevel Alt-H
decreaseHeadingLevel Shift-Alt-H
toggleBold Ctrl-B
toggleItalic Ctrl-I
toggleStrikethrough Ctrl-K
toggleUnorderedList Ctrl-L
toggleOrderedList Shift-Ctrl-L
toggleCheckList Shift-Ctrl-Alt-L
toggleQuote Ctrl-Q
toggleInlineCode Ctrl-7
insertCodeBlock Shift-Ctrl-7
insertLink Ctrl-M
insertImageLink Shift-Ctrl-M
insertTable Ctrl-Alt-T
insertHorizontalRule Shift-Ctrl--
toggleRichTextMode Alt-R
formatContent Alt-F
downloadAsFile Shift-Ctrl-S
importFromFile Ctrl-Alt-I
togglePreview Alt-p
toggleSideBySidePreview Shift-Alt-P
undo Ctrl-Z
redo Ctrl-Y, Shift-Ctrl-Z
openMarkdownGuide F1

For shortcuts that come built-in with CodeMirror, see CodeMirror documentation.

The primary to configure single shortcuts alongside with other item properties is to use the toolbar configuration as described in the toolbar section.

However, if you want to customize keyboard shortcuts of a lot of (built-in) items you may also do this inside the options: Options input property with options.shortcuts = {...}. This is a decent alternative as you can specify many keybindings in a single object. Attention: Shortcuts defined in options.shortcuts will override shortcuts specified in toolbar.

When specifying custom shortcuts, mind the correct order of special keys: Shift-Cmd-Ctrl-Alt (see here).

Disabling shortcuts

As per default, keyboard shortcuts are always functioning for all built-in toolbar items, even when they are not included into the visible toolbar, in order to enable users to efficiently write Markdown. However, you can configure this behavior inside the options: Options input property with options.shortcutsEnabled. You can either disable shortcuts completely (shortcutsEnabled: 'none') or only enable them for items included in the toolbar or specified in options.shortcuts (shortcutsEnabled: 'customOnly').

Icons

Icons can be specified in multiple ways. The easiest is to use an icon included in the Material icon font.

const item: Icon = {
  format: 'material',
  iconName: 'thumb_up',
};

But you can also use your own SVG icons by either specifying the icon's file path (location at runtime) or by including an SVG string into your TypeScript file:

const item: Icon = {
  format: 'svgFile',
  iconName: 'my_icon',
  runTimePath: './path/to/icon.svg',
};

or

const item: Icon = {
  format: 'svgFile',
  iconName: 'my_icon',
  svgHtmlString: '<svg viewBox="0 0 20 20"> <circle r="10" /> </svg>',
};

Depending on the format of your icon you might need to adjust the icon via CSS, e.g. similar to

.mat-button .mat-icon[data-mat-icon-name='my_icon'] {
  height: 16px;
  ...;
}

Statusbar

Configuring the statusbar is very similar to configuring the toolbar, only simpler as there are only two properties for an item.

The statusbar input property is an array of type

type StatusbarItemDef = StatusbarItemName | StatusbarItem;

StatusbarItemName is a union type of all built-in item names. The interface StatusbarItem represents a full statusbar item.

interface StatusbarItem {
  name: string;
  value: OptionalI18n<Observable<string>>;
}

The value of an item is a observable (or an internationalized version of it), which will be observed by the statusbar.

For details about the OptionalI18n<T> type, see Internationalization section.

In the following, we always apply a JavaScript variable to the toolbar input property:

<ngx-markdown-editor [toolbar]="toolbar"></ngx-markdown-editor>

1. Construct a statusbar from existing items

To build a statusbar from existing items, simply create an array of type StatusbarItemName[] (or StatusbarItemDef[]) and specify the items by name. Additionally, there is a separator element, which you can insert at any position with '|'.

public statusbar: ToolbarItemName[] = ['wordCount', 'characterCount', '|', 'cursorPosition'];

The naming convention for items is to use the name of the subject / value that is displayed.

2. Configure an existing item

You can adjust built-in items for you needs by defining an item with a name included in ToolbarItemName. However, this might only make sense if you want to keep an existing item name and implement internationalization for it, as this is almost the same as creating a new item. For this reason, we omit an example here and refer to the section below.

3. Create your own item

To create a custom statusbar item, simply define a new object of type StatusbarItem:

const myItem: ToolbarItem = {
  name: 'myValue',
  value: of('static string'),
};

Then include this object into the statusbar item array (maybe alongside StatusbarItemNames):

public toolbar: ToolbarItemDef[] = [newToggleBoldItem, 'toggleItalic', ...];

Internationalization

Ngx Markdown Editor provides opt-in internationalization for many objects like tooltips and even icons. To realize this, a generic type named OptionalI18n<T> was implemented:

type OptionalI18n<T> = T | ({ default: T } & { [lang in LanguageTag]?: T });

This enables you to specify either the same object for all languages (not using internationalization for this specific object) or apply an i18n object for only those language you need. If you use the internationalized version, you are required to define a default value, that is applied for all languages you do not specify explicitly.

To summarize: you can go exactly as far with internationalization as you want to with this component.

Theming

Theming is an important issue when using third party components to integrate them smoothly into your application. Therefore, Ngx Markdown Editor provides default themes as well as an easy way to apply your own theme.

You can style every element inside <ngx-markdown-editor> with CSS as the component does not use view encapsulation (ViewEncapsulation.None). You can also predefine different themes and apply them dynamically.

Editor styling

The default theme is named default. Alternatively you can use the predefined material theme and choose from different styles as described in the Getting started section.

To customize the editor's appearance for your needs, use options.editorThemes. The theme name specified here, will be applied as is to <ngx-markdown-editor> as well as be applied with a cm-s- prefix to the <div class="CodeMirror"> element. For further details on CodeMirror's theming, visit the dedicated section on CodeMirror.

To apply a customized theme with the name "example"

  • specify { editorThemes: ['example']} in the options input property,
  • define the CSS selector ngx-markdown-editor.example in a CSS file,
  • define the CSS class .cm-s-example to style the CodeMirror element, and
  • make sure to load the CSS file with your app.

If you only want to extend the default theme, you can either define new stylings for the classes ngx-markdown-editor.default and .cm-s-default and make sure that the "default" theme is applied or you can create your own additional theme and specify two themes in the options: { editorThemes: ['default', 'additional-theme'] }.

Markup styling

Again, the default theme is named default here, which applies the default markup styling from the gfm CodeMirror mode.

Additionally there is a predefined theme preview-like-markup which imitates the styling of Ngx Markdown's default styling. This makes the markup look as similar to the preview as possible.

To customize the markup styling, use options.markupThemes. The theme specified here is only applied to the CodeMirror element <div class="CodeMirror"></div>. For detailed instructions how to define your own markup styling, visit the section on Markdown Editor Core.

FAQs

How to set the editor's content programmatically

You can either set the content using the input property data (one-way binding, not like ngModel):

<ngx-markdown-editor [data]="'Content changes whenever this input changes'"></ngx-markdown-editor>

Or you can do it using the MarkdownEditorComponent instance:

@ViewChild(MarkdownEditorComponent) ngxMde: MarkdownEditorComponent;
this.ngxMde.mde.setContent('Any _Markdown_ **string**, where lines are separated with a new line character "\n".');

How to access the CodeMirror editor instance

The CodeMirror instance is accessible through the Markdown Editor Core instance, which is in turn publicly accessible in MarkdownEditorComponent.

@ViewChild(MarkdownEditorComponent) ngxMde: MarkdownEditorComponent;
const cm: CodeMirror.Editor = this.ngxMde.mde.cm;

How to listen to an CodeMirror event which is not emitted by MarkdownEditorComponent

With the utility function fromCmEvent, Ngx Markdown Editor provides a convenient way to convert a CodeMirror event to an RxJS Observable.

@ViewChild(MarkdownEditorComponent) ngxMde: MarkdownEditorComponent;
const eventObs: Observable<{...}> = fromCmEvent(this.ngxMde.mde.cm, 'mousedown');

eventObs.subscribe((instance, ...) => myEventHandler());

How to contribute

First of all, contributions in any way are very welcome! And a big thank you to all who decide to so!! :)

The code is neither perfect nor complete. If you have any suggestions, requirements or even just comments, please let me know and I will do my best do incorporate them! The even better (and probably faster) way for requesting code modifications, however, are pull requests. I am very happy about all code contributions as time is often rare around here... :)

Writing issues

Before you open an issue, please have one closer look if this is really an issue of Ngx Markdown Editor or if it rather belongs to Markdown Editor Core.

When writing issues, please give a clear description of the current state and what you are unhappy about. Then, if possible, propose your solution or at least leave a short statement of your thoughts about it.

Making pull requests

Recipe for making a pull request:

  1. Fork and checkout repo locally.
  2. Install Yarn, if you do not have it yet. For example via npm i yarn -g.
  3. Open a command line, move to the project directory and run yarn to install all dependencies.
  4. Make your code changes. (Please mind the style guidelines.)
  5. Run ng build ngx-markdown-editor --watch to build the library in watch mode.
  6. Run ng serve to test your changes in the demo app.
  7. Check the docs whether they need to be changed.
  8. Push the changes to your fork.
  9. Make a pull request to the master branch of this repo. Please provide a meaningful title for the PR and give a concise description.

Project setup

Package manager

This project uses Yarn as package manager. So you must use this one to install dependencies when contributing code. The scripts in package.json still work with npm, although it is recommended to always use yarn throughout the project.

Commit rules

We use Commitlint to guarantee structured commit messages. This means you must write commit messages that meet the rules of Commitlint. If you are not familiar with Commitlint, you can use the CLI tool Commitizen by running yarn run commit, which assists you to write conventional messages. You can also install Commitizen globally on your system, if you want to use the shorter cli commands cz or git cz.

Coding style guidelines

There are not many strict guidelines to keep in mind, but please adapt to the project's code style when contributing. Only one more thing shall be mentioned here:

We use Prettier to ensure consistent formatting. Therefore, you should install a Prettier plugin for your IDE. Further it is highly recommended to enable "Format on save", which is also set as the project's default for VSCode.

There is a pre-commit git hook for Prettier, which checks the formatting of all files. Occasionally it might happen that this hook fails although you have "Format on save" enabled. This is usually due to wrong line endings, e.g. caused by yarn add ... or some other file-writing script or tool. In this case, run yarn run format:write to let Prettier correct the wrong formatting and then try to commit again. Unfortunately, the format:write command cannot be set as a pre-commit hook as it is not known in general, which files need to be staged afterwards.