@mdefy/ngx-markdown-editor
Version:
An Angular Markdown Editor in WYSIWYG style with extensive functionality, high customizability and an integrated material theme.
1,233 lines (1,227 loc) • 72.2 kB
JavaScript
import { ɵɵdefineInjectable, ɵɵinject, Injectable, EventEmitter, SimpleChange, Component, ViewEncapsulation, ElementRef, Host, Input, Output, ViewChild, HostBinding, SecurityContext, NgModule } from '@angular/core';
import { MatIconRegistry, MatIconModule } from '@angular/material/icon';
import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipModule } from '@angular/material/tooltip';
import { EventManager, DomSanitizer, BrowserModule } from '@angular/platform-browser';
import { MarkdownEditor, DEFAULT_OPTIONS } from '@mdefy/markdown-editor-core';
import { MarkdownService, SECURITY_CONTEXT, MarkdownModule } from 'ngx-markdown';
import { Observable, Subject } from 'rxjs';
import { startWith, map, takeUntil } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
/**
* An injectable hotkeys service to add keybindings.
*/
class Keybindings {
constructor(eventManager) {
this.eventManager = eventManager;
}
/**
* Adds an _keydown_ event listener to the Angular `EventManager` with the specified
* `keys`. The listener is applied to specified `element`. Returns an RxJS observable
* of the keyboard event.
*
* @param element The HTML element to which the keybinding shall be applied to.
* @param keys The key combination which shall trigger the event.
*/
addKeybinding(element, keys) {
const event = `keydown.${keys}`;
return new Observable((observer) => {
const handler = (e) => {
e.preventDefault();
observer.next(e);
};
const dispose = this.eventManager.addEventListener(element, event, handler);
return () => {
dispose();
};
});
}
}
Keybindings.ɵprov = ɵɵdefineInjectable({ factory: function Keybindings_Factory() { return new Keybindings(ɵɵinject(EventManager)); }, token: Keybindings, providedIn: "root" });
Keybindings.decorators = [
{ type: Injectable, args: [{ providedIn: 'root' },] }
];
Keybindings.ctorParameters = () => [
{ type: EventManager }
];
/**
* Transforms a _CodeMirror_ event to an RxJS observable.
*
* @param cm the `CodeMirror.Editor` instance of which the event shall be observed
* @param eventName the name of a _CodeMirror_ event
*/
function fromCmEvent(cm, eventName) {
return new Observable((subscriber) => {
let handler;
switch (eventName) {
case 'change':
handler = (...args) => subscriber.next({ instance: args[0], changeObj: args[1] });
break;
case 'changes':
handler = (...args) => subscriber.next({ instance: args[0], changes: args[1] });
break;
case 'beforeChange':
handler = (...args) => subscriber.next({ instance: args[0], changeObj: args[1] });
break;
case 'cursorActivity':
handler = (...args) => subscriber.next({ instance: args[0] });
break;
case 'keyHandled':
handler = (...args) => subscriber.next({ instance: args[0], name: args[1], event: args[2] });
break;
case 'inputRead':
handler = (...args) => subscriber.next({ instance: args[0], changeObj: args[1] });
break;
case 'electricInput':
handler = (...args) => subscriber.next({ instance: args[0], line: args[1] });
break;
case 'beforeSelectionChange':
handler = (...args) => subscriber.next({ instance: args[0], obj: args[1] });
break;
case 'viewportChange':
handler = (...args) => subscriber.next({ instance: args[0], from: args[1], to: args[2] });
break;
case 'swapDoc':
handler = (...args) => subscriber.next({ instance: args[0], oldDoc: args[1] });
break;
case 'gutterClick':
handler = (...args) => subscriber.next({ instance: args[0], line: args[1], gutter: args[2], clickEvent: args[3] });
break;
case 'gutterContextMenu':
handler = (...args) => subscriber.next({ instance: args[0], line: args[1], gutter: args[2], contextMenu: args[3] });
break;
case 'focus':
handler = (...args) => subscriber.next({ instance: args[0], event: args[1] });
break;
case 'blur':
handler = (...args) => subscriber.next({ instance: args[0], event: args[1] });
break;
case 'scroll':
handler = (...args) => subscriber.next({ instance: args[0] });
break;
case 'refresh':
handler = (...args) => subscriber.next({ instance: args[0] });
break;
case 'optionChange':
handler = (...args) => subscriber.next({ instance: args[0], option: args[1] });
break;
case 'scrollCursorIntoView':
handler = (...args) => subscriber.next({ instance: args[0], event: args[1] });
break;
case 'update':
handler = (...args) => subscriber.next({ instance: args[0] });
break;
case 'renderLine':
handler = (...args) => subscriber.next({ instance: args[0], line: args[1], element: args[2] });
break;
case 'overwriteToggle':
handler = (...args) => subscriber.next({ instance: args[0], overwrite: args[1] });
break;
default:
handler = (...args) => subscriber.next({ instance: args[0], event: args[1] });
}
if (cm) {
cm.on(eventName, handler);
return () => cm.off(eventName, handler);
}
subscriber.error(new Error('CodeMirror instance is undefined'));
return;
});
}
class StatusbarService {
constructor() {
/**
* The default statusbar setup.
*/
this.DEFAULT_STATUSBAR = ['wordCount', 'characterCount', '|', 'cursorPosition'];
}
/**
* The default configurations of all items.
*/
get DEFAULT_ITEMS() {
return this._defaultItems;
}
/**
* Returns the default configuration of the item with the specified name.
* Returns `undefined`, if no item with the specified name can be found.
*/
getDefaultItem(itemName) {
return this.DEFAULT_ITEMS.find((i) => i.name === itemName);
}
/**
* Defines the default statusbar items.
* Cannot be done statically as the values depend on the `MarkdownEditor` instance.
*/
defineDefaultItems(mde) {
const defaultItems = [
{
name: 'wordCount',
value: fromCmEvent(mde.cm, 'changes').pipe(startWith(true), map(() => 'Words: ' + mde.getWordCount().toString())),
},
{
name: 'characterCount',
value: fromCmEvent(mde.cm, 'changes').pipe(startWith(true), map(() => 'Characters: ' + mde.getCharacterCount().toString())),
},
{
name: 'cursorPosition',
value: fromCmEvent(mde.cm, 'cursorActivity').pipe(startWith(true), map(() => {
const pos = mde.getCursorPos();
return `${pos.line}:${pos.ch}`;
})),
},
// Normalize separator item to reduce type complexity in template.
// Effectively, only the `name` property is needed.
{
name: '|',
value: new Observable(),
},
];
this._defaultItems = defaultItems;
}
}
StatusbarService.decorators = [
{ type: Injectable }
];
class ToolbarService {
constructor() {
/**
* The default toolbar setup.
*/
this.DEFAULT_TOOLBAR = [
'setHeadingLevel',
'toggleHeadingLevel',
'increaseHeadingLevel',
'decreaseHeadingLevel',
'toggleBold',
'toggleItalic',
'toggleStrikethrough',
'|',
'toggleUnorderedList',
'toggleOrderedList',
'toggleCheckList',
'|',
'toggleQuote',
'toggleInlineCode',
'insertCodeBlock',
'|',
'insertLink',
'insertImageLink',
'insertTable',
'insertHorizontalRule',
'|',
'toggleRichTextMode',
'formatContent',
'|',
'downloadAsFile',
'importFromFile',
'|',
'togglePreview',
'toggleSideBySidePreview',
'|',
'undo',
'redo',
'|',
'openMarkdownGuide',
];
}
/**
* The default configurations of all items
*/
get DEFAULT_ITEMS() {
return this._defaultItems;
}
/**
* Returns the default configuration of the item with the specified name.
* Returns `undefined`, if no item with the specified name can be found.
*/
getDefaultItem(itemName) {
return this.DEFAULT_ITEMS.find((i) => i.name === itemName);
}
/**
* Defines the default toolbar items.
* Cannot be done statically as the actions depend on the `MarkdownEditorComponent` instance.
*/
defineDefaultItems(ngxMde) {
const defaultItems = [
{
name: 'setHeadingLevel',
action: (level) => ngxMde.mde.setHeadingLevel(level),
shortcut: 'Shift-Ctrl-Alt-H',
isActive: () => {
if (!ngxMde.mde.hasTokenAtCursorPos('header'))
return 0;
const token = ngxMde.mde.cm.getTokenAt(ngxMde.mde.getCursorPos());
return token.state.base.header;
},
tooltip: 'Set Heading Level',
icon: {
format: 'svgString',
iconName: 'format_heading',
svgHtmlString: FORMAT_HEADING,
},
disableOnPreview: true,
},
{
name: 'toggleHeadingLevel',
action: () => ngxMde.mde.increaseHeadingLevel(),
tooltip: 'Heading',
shortcut: 'Alt-H',
icon: {
format: 'svgString',
iconName: 'format_heading',
svgHtmlString: FORMAT_HEADING,
},
disableOnPreview: true,
},
{
name: 'increaseHeadingLevel',
action: () => ngxMde.mde.increaseHeadingLevel(),
tooltip: 'Smaller Heading',
icon: {
format: 'svgString',
iconName: 'format_heading_decrease',
svgHtmlString: FORMAT_HEADING_SMALLER,
},
disableOnPreview: true,
},
{
name: 'decreaseHeadingLevel',
action: () => ngxMde.mde.decreaseHeadingLevel(),
tooltip: 'Bigger Heading',
icon: {
format: 'svgString',
iconName: 'format_heading_increase',
svgHtmlString: FORMAT_HEADING_BIGGER,
},
disableOnPreview: true,
},
{
name: 'toggleBold',
action: () => ngxMde.mde.toggleBold(),
isActive: () => ngxMde.mde.hasTokenAtCursorPos('strong'),
tooltip: 'Toggle Bold',
icon: {
format: 'material',
iconName: 'format_bold',
},
disableOnPreview: true,
},
{
name: 'toggleItalic',
action: () => ngxMde.mde.toggleItalic(),
isActive: () => ngxMde.mde.hasTokenAtCursorPos('em'),
tooltip: 'Toggle Italic',
icon: {
format: 'material',
iconName: 'format_italic',
},
disableOnPreview: true,
},
{
name: 'toggleStrikethrough',
action: () => ngxMde.mde.toggleStrikethrough(),
isActive: () => ngxMde.mde.hasTokenAtCursorPos('strikethrough'),
tooltip: 'Toggle Strikethrough',
icon: {
format: 'material',
iconName: 'format_strikethrough',
},
disableOnPreview: true,
},
{
name: 'toggleUnorderedList',
action: () => ngxMde.mde.toggleUnorderedList(),
isActive: () => this.isListTypeActive(ngxMde, 'unordered'),
tooltip: 'Toggle Unordered List',
icon: {
format: 'material',
iconName: 'format_list_bulleted',
},
disableOnPreview: true,
},
{
name: 'toggleOrderedList',
action: () => ngxMde.mde.toggleOrderedList(),
isActive: () => this.isListTypeActive(ngxMde, 'ordered'),
tooltip: 'Toggle Ordered List',
icon: {
format: 'material',
iconName: 'format_list_numbered',
},
disableOnPreview: true,
},
{
name: 'toggleCheckList',
action: () => ngxMde.mde.toggleCheckList(),
isActive: () => this.isListTypeActive(ngxMde, 'check'),
tooltip: 'Toggle Checklist',
icon: {
format: 'material',
iconName: 'check_box',
},
disableOnPreview: true,
},
{
name: 'toggleQuote',
action: () => ngxMde.mde.toggleQuote(),
isActive: () => ngxMde.mde.hasTokenAtCursorPos('quote'),
tooltip: 'Toggle Quotation',
icon: {
format: 'material',
iconName: 'format_quote',
},
disableOnPreview: true,
},
{
name: 'toggleInlineCode',
action: () => ngxMde.mde.toggleInlineCode(),
isActive: () => this.isCodeTypeActive(ngxMde, 'inline'),
tooltip: 'Toggle Inline Code',
icon: {
format: 'material',
iconName: 'code',
},
disableOnPreview: true,
},
{
name: 'insertCodeBlock',
action: () => ngxMde.mde.insertCodeBlock(),
isActive: () => this.isCodeTypeActive(ngxMde, 'block'),
tooltip: 'Insert Code Block',
icon: {
format: 'svgString',
iconName: 'file_code',
svgHtmlString: FILE_CODE,
},
disableOnPreview: true,
},
{
name: 'insertLink',
action: () => ngxMde.mde.insertLink(),
isActive: () => (ngxMde.mde.hasTokenAtCursorPos('link-text') || ngxMde.mde.hasTokenAtCursorPos('link')) &&
!ngxMde.mde.hasTokenAtCursorPos('image'),
tooltip: 'Insert Link',
icon: {
format: 'material',
iconName: 'insert_link',
},
disableOnPreview: true,
},
{
name: 'insertImageLink',
action: () => ngxMde.mde.insertImageLink(),
isActive: () => ngxMde.mde.hasTokenAtCursorPos('image'),
tooltip: 'Insert Image Link',
icon: {
format: 'material',
iconName: 'image',
},
disableOnPreview: true,
},
{
name: 'insertTable',
action: () => ngxMde.mde.insertTable(),
tooltip: 'Insert Table',
icon: {
format: 'material',
iconName: 'table_chart',
},
disableOnPreview: true,
},
{
name: 'insertHorizontalRule',
action: () => ngxMde.mde.insertHorizontalRule(),
isActive: () => ngxMde.mde.hasTokenAtCursorPos('hr'),
tooltip: 'Insert Horizontal Rule',
icon: {
format: 'material',
iconName: 'horizontal_rule',
},
disableOnPreview: true,
},
{
name: 'toggleRichTextMode',
action: () => ngxMde.mde.toggleRichTextMode(),
isActive: () => {
const mode = ngxMde.mde.cm.getOption('mode');
return mode === 'gfm' || mode.name === 'gfm';
},
tooltip: 'Toggle Rich-Text Mode',
icon: {
format: 'material',
iconName: 'highlight',
},
disableOnPreview: true,
},
{
name: 'formatContent',
action: () => ngxMde.mde.formatContent(),
tooltip: 'Format Content',
icon: {
format: 'material',
iconName: 'format_paint',
},
disableOnPreview: true,
},
{
name: 'downloadAsFile',
action: () => ngxMde.mde.downloadAsFile(),
tooltip: 'Download As File',
icon: {
format: 'material',
iconName: 'get_app',
},
disableOnPreview: true,
},
{
name: 'importFromFile',
action: () => ngxMde.mde.importFromFile(),
tooltip: 'Import From File',
icon: {
format: 'svgString',
iconName: 'upload',
svgHtmlString: UPLOAD,
},
disableOnPreview: true,
},
{
name: 'togglePreview',
action: () => ngxMde.togglePreview(),
shortcut: 'Alt-P',
isActive: () => ngxMde.showPreview,
tooltip: 'Toggle Preview',
icon: {
format: 'material',
iconName: 'preview',
},
disableOnPreview: false,
},
{
name: 'toggleSideBySidePreview',
action: () => ngxMde.toggleSideBySidePreview(),
shortcut: 'Shift-Alt-P',
isActive: () => ngxMde.showSideBySidePreview,
tooltip: 'Toggle Side-by-Side Preview',
icon: {
format: 'svgString',
iconName: 'column',
svgHtmlString: COLUMN,
},
disableOnPreview: false,
},
{
name: 'undo',
action: () => ngxMde.mde.undo(),
tooltip: 'Undo',
icon: {
format: 'material',
iconName: 'undo',
},
disableOnPreview: true,
},
{
name: 'redo',
action: () => ngxMde.mde.redo(),
shortcut: 'Ctrl-S',
tooltip: 'Redo',
icon: {
format: 'material',
iconName: 'redo',
},
disableOnPreview: true,
},
{
name: 'openMarkdownGuide',
action: () => ngxMde.mde.openMarkdownGuide(),
tooltip: 'Open Markdown Guide',
icon: {
format: 'material',
iconName: 'help',
},
disableOnPreview: false,
},
// Normalize separator item to reduce type complexity in template.
// Effectively, only the `name` property is needed.
{
name: '|',
action: () => { },
tooltip: '',
icon: { format: 'material', iconName: '' },
disableOnPreview: false,
},
];
this._defaultItems = defaultItems;
}
isListTypeActive(ngxMde, listType) {
const isList = ngxMde.mde.hasTokenAtCursorPos('list');
if (!isList)
return false;
const selections = ngxMde.mde.cm.listSelections();
let isListType = false;
if (selections === null || selections === void 0 ? void 0 : selections.length) {
const lineNumber = selections[selections.length - 1].from().line;
isListType = ngxMde.mde.getListTypeOfLine(lineNumber) === listType;
}
return isListType;
}
isCodeTypeActive(ngxMde, codeType) {
const isCode = ngxMde.mde.hasTokenAtCursorPos('code');
if (!isCode)
return false;
const token = ngxMde.mde.cm.getTokenAt(ngxMde.mde.getCursorPos());
if (codeType === 'block') {
return token.state.overlay.codeBlock;
}
else {
return token.state.overlay.code;
}
}
}
ToolbarService.decorators = [
{ type: Injectable }
];
/* eslint-disable max-len */
const COLUMN = `
<!-- Icon from Font Awesome: https://fontawesome.com/icons/columns?style=solid; License: https://fontawesome.com/license -->
<svg
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="columns"
class="svg-inline--fa fa-columns fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
licenseUrl="https://fontawesome.com/license"
>
<path
fill="currentColor"
d="M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM224 416H64V160h160v256zm224 0H288V160h160v256z"
></path>
</svg>
`;
const FILE_CODE = `
<!-- Icon from Font Awesome: https://fontawesome.com/icons/file-code?style=solid; License: https://fontawesome.com/license -->
<svg
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="file-code"
class="svg-inline--fa fa-file-code fa-w-12"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
licenseUrl="https://fontawesome.com/license"
>
<path
fill="currentColor"
d="M384 121.941V128H256V0h6.059c6.365 0 12.47 2.529 16.971 7.029l97.941 97.941A24.005 24.005 0 0 1 384 121.941zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zM123.206 400.505a5.4 5.4 0 0 1-7.633.246l-64.866-60.812a5.4 5.4 0 0 1 0-7.879l64.866-60.812a5.4 5.4 0 0 1 7.633.246l19.579 20.885a5.4 5.4 0 0 1-.372 7.747L101.65 336l40.763 35.874a5.4 5.4 0 0 1 .372 7.747l-19.579 20.884zm51.295 50.479l-27.453-7.97a5.402 5.402 0 0 1-3.681-6.692l61.44-211.626a5.402 5.402 0 0 1 6.692-3.681l27.452 7.97a5.4 5.4 0 0 1 3.68 6.692l-61.44 211.626a5.397 5.397 0 0 1-6.69 3.681zm160.792-111.045l-64.866 60.812a5.4 5.4 0 0 1-7.633-.246l-19.58-20.885a5.4 5.4 0 0 1 .372-7.747L284.35 336l-40.763-35.874a5.4 5.4 0 0 1-.372-7.747l19.58-20.885a5.4 5.4 0 0 1 7.633-.246l64.866 60.812a5.4 5.4 0 0 1-.001 7.879z"
></path>
</svg>
`;
const FORMAT_HEADING = `
<!-- Icon from Font Awesome: https://fontawesome.com/icons/heading?style=solid; License: https://fontawesome.com/license -->
<svg
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="heading"
class="svg-inline--fa fa-heading fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 512 512"
licenseUrl="https://fontawesome.com/license"
>
<path
fill="currentColor"
d="M448 96v320h32a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H320a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h32V288H160v128h32a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H32a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h32V96H32a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h160a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16h-32v128h192V96h-32a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h160a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16z"
></path>
</svg>
`;
const FORMAT_HEADING_BIGGER = `
<!-- Icon from Font Awesome: https://fontawesome.com/icons/heading?style=solid; License: https://fontawesome.com/license -->
<svg
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="heading"
class="svg-inline--fa fa-heading fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 720 720"
licenseUrl="https://fontawesome.com/license"
>
<path
fill="currentColor"
d="M448 200v320h32a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H320a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h32V392H160v128h32a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H32a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h32V200H32a16 16 0 0 1-16-16V152a16 16 0 0 1 16-16h160a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16h-32v128h192V200h-32a16 16 0 0 1-16-16V152a16 16 0 0 1 16-16h160a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16z"
></path>
<path
fill="currentColor"
d="M620 285 l87 150 h-174 z"
></path>
</svg>
`;
const FORMAT_HEADING_SMALLER = `
<!-- Icon from Font Awesome: https://fontawesome.com/icons/heading?style=solid; License: https://fontawesome.com/license -->
<svg
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="heading"
class="svg-inline--fa fa-heading fa-w-16"
role="img"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 720 720"
licenseUrl="https://fontawesome.com/license"
>
<path
fill="currentColor"
d="M448 200v320h32a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H320a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h32V392H160v128h32a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H32a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h32V200H32a16 16 0 0 1-16-16V152a16 16 0 0 1 16-16h160a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16h-32v128h192V200h-32a16 16 0 0 1-16-16V152a16 16 0 0 1 16-16h160a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16z"
></path>
<path
fill="currentColor"
d="M620 435 l87 -150 h-174 z"
></path>
</svg>
`;
const UPLOAD = `
<svg
focusable="false"
data-icon="upload"
role="img"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="2 2 20 20"
>
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
</svg>
`;
/**
* An event emitter that is able to transform an RxJS observable
* into an Angular event.
*/
class ObservableEmitter extends EventEmitter {
/**
* Subscribes to the specified RxJS observable and emits an event
* containing the observed value. Unsubscribes the passed observable
* when the event is unsubscribed.
*/
emitObservable(o) {
const subscription = o.subscribe((x) => this.emit(x));
this.subscribe(() => { }, undefined, () => subscription.unsubscribe());
}
}
const markdownEditorTooltipDefaults = {
showDelay: 1000,
hideDelay: 0,
touchendHideDelay: 1000,
};
const ɵ0 = markdownEditorTooltipDefaults;
class MarkdownEditorComponent {
constructor(iconRegistry, domSanitizer, hotkeys, hostElement, markdownService, toolbarService, statusbarService) {
this.iconRegistry = iconRegistry;
this.domSanitizer = domSanitizer;
this.hotkeys = hotkeys;
this.hostElement = hostElement;
this.markdownService = markdownService;
this.toolbarService = toolbarService;
this.statusbarService = statusbarService;
/**
* Data string to set as content of the editor.
*/
this.data = '';
/**
* Options to configure _Ngx Markdown Editor_.
*
* Basically `MarkdownEditorOptions` from _Markdown Editor Core_ are forwarded,
* including some adjustments and extensions.
*/
this.options = {};
/**
* Custom set of toolbar items.
*/
this.toolbar = [];
/**
* Custom set of statusbar items.
*/
this.statusbar = [];
/**
* The current language applied to internationalized items.
*/
this.language = 'en';
/**
* Specifies whether the editor is a required form field. An asterisk will be appended to the label.
*/
this.required = false;
/**
* Specifies whether and which Angular Material style is used.
*/
this.materialStyle = false;
/**
* Specifies whether the editor is disabled.
*/
this.disabled = false;
/**
* Specifies whether the toolbar is rendered.
*/
this.showToolbar = true;
/**
* Specifies whether the statusbar is rendered.
*/
this.showStatusbar = true;
/**
* Specifies whether tooltips are shown for toolbar items.
*/
this.showTooltips = true;
/**
* Specifies whether the key combination is included in the tooltip.
*/
this.shortcutsInTooltips = true;
/**
* Emits when the editor's content changes.
*/
this.contentChange = new ObservableEmitter();
/**
* Emits when the editor's cursor is moved.
*/
this.cursorActivity = new ObservableEmitter();
/**
* Emits when the editor receives focus.
*/
this.editorFocus = new ObservableEmitter();
/**
* Emits when the editor loses focus.
*/
this.editorBlur = new ObservableEmitter();
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.normalizedToolbarItems = [];
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.activeToolbarItems = [];
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.toolbarItemTooltips = [];
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.normalizedStatusbarItems = [];
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.showPreview = false;
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.showSideBySidePreview = false;
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.blockBlur = false;
/**
* _Not intended to be used outside the component. Only made public for access inside template._
*/
this.focused = false;
this.shortcutResetter = new Subject();
// Render checkbox dummies which can be replaced by an `<input type="checkbox"> later,
// because the checkboxes rendered by marked.js inside ngx-markdown are removed by Angular sanitizer.
this.markdownService.renderer.checkbox = (checked) => (checked ? '[x] ' : '[ ] ');
}
get disabledStyle() {
return this.disabled;
}
get default() {
return !this.options.editorThemes && !this.materialStyle;
}
get material() {
return this.materialStyle;
}
get class() {
return this.options.editorThemes;
}
get appearanceStandard() {
return this.materialStyle === true || this.materialStyle === 'standard';
}
get appearanceFill() {
return this.materialStyle === 'fill';
}
get appearanceLegacy() {
return this.materialStyle === 'legacy';
}
get focusedStyle() {
return this.focused;
}
/**
* @inheritdoc
*/
ngOnInit() {
const wrapper = this.hostElement.nativeElement.querySelector('.ngx-markdown-editor-text-editor');
this.mde = new MarkdownEditor(wrapper, this.mapOptions(this.options));
this.contentChange.emitObservable(fromCmEvent(this.mde.cm, 'changes'));
this.cursorActivity.emitObservable(fromCmEvent(this.mde.cm, 'cursorActivity'));
this.editorFocus.emitObservable(fromCmEvent(this.mde.cm, 'focus'));
this.editorBlur.emitObservable(fromCmEvent(this.mde.cm, 'blur'));
this.toolbarService.defineDefaultItems(this);
this.statusbarService.defineDefaultItems(this.mde);
// Necessary to apply `this.mde` instance to default toolbar items
// as `ngOnChanges()` is executed before `ngOnInit()`.
this.ngOnChanges({ data: new SimpleChange(undefined, this.data, true) });
this.mde.cm.clearHistory();
fromCmEvent(this.mde.cm, 'focus').subscribe(() => {
this.focused = true;
});
fromCmEvent(this.mde.cm, 'blur').subscribe(() => {
if (!this.blockBlur)
this.focused = false;
});
fromCmEvent(this.mde.cm, 'cursorActivity').subscribe(() => {
this.determineActiveButtons();
});
}
/**
* @inheritdoc
*/
ngOnChanges(changes) {
if (this.mde) {
if (this.showToolbar) {
this.applyToolbarItems();
}
if (this.showStatusbar) {
this.applyStatusbarItems();
}
this.applyDisabled();
this.mde.setOptions(this.mapOptions(this.options));
if (changes.data) {
this.mde.setContent(changes.data.currentValue);
}
this.createTooltips();
this.determineActiveButtons();
this.setCodeMirrorClasses();
this.applyMaterialStyle();
}
}
/**
* @inheritdoc
*/
ngOnDestroy() {
this.shortcutResetter.next();
this.shortcutResetter.complete();
}
/**
* Toggles the full-size preview.
*/
togglePreview() {
this.showPreview = !this.showPreview;
this.showSideBySidePreview = false;
if (this.showPreview) {
// Necessary to wait until Angular change detector has finished
setTimeout(() => { var _a; return (_a = this.hostElement.nativeElement.querySelector('.ngx-markdown-editor-wrapper')) === null || _a === void 0 ? void 0 : _a.focus(); }, 100);
}
else {
// Necessary to wait until Angular change detector has finished
setTimeout(() => this.mde.focus(), 100);
}
}
/**
* Toggles the side-by-side preview.
*/
toggleSideBySidePreview() {
this.showSideBySidePreview = !this.showSideBySidePreview;
this.showPreview = false;
// Timeout necessary until Angular change detector has finished
setTimeout(() => this.mde.focus(), 100);
}
/**
* Triggered when a toolbar button is clicked.
*
* _Not intended to be used outside the component. Only made public for access inside template._
*/
onButtonClick(item) {
item.action();
this.mde.focus();
this.determineActiveButtons();
}
/**
* Resolves the shortcut for the specified item and appends it to the item's tooltip text,
* if `shortcutsInTooltips` is enabled.
*
* _Not intended to be used outside the component. Only made public for access inside template._
*/
createTooltip(item) {
let shortcut = this.mde.getShortcuts()[item.name] || item.shortcut;
if (item.name === 'undo')
shortcut = 'Ctrl-Z';
else if (item.name === 'redo')
shortcut = 'Shift-Ctrl-Z';
if (/Mac/.test(navigator.platform))
shortcut = shortcut === null || shortcut === void 0 ? void 0 : shortcut.replace(/Ctrl/gi, 'Cmd');
const shortcutString = this.shortcutsInTooltips && shortcut ? ' (' + shortcut + ')' : '';
return item.tooltip + shortcutString;
}
/**
* Replaces the checkbox dummies rendered inside the preview with actual checkboxes (also see constructor).
*
* _Not intended to be used outside the component. Only made public for access inside template._
*/
replaceCheckboxDummies() {
var _a;
(_a = this.markdown) === null || _a === void 0 ? void 0 : _a.element.nativeElement.querySelectorAll('li').forEach((el) => el.childNodes.forEach((node) => {
var _a, _b;
if (node.nodeType === 3) {
if (/^\[ \] /.test(node.nodeValue || '')) {
const input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('disabled', '');
el.insertBefore(input, node);
node.nodeValue = ((_a = node.nodeValue) === null || _a === void 0 ? void 0 : _a.replace(/^\[ \]/, '')) || null;
}
else if (/^\[x\] /.test(node.nodeValue || '')) {
const input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('disabled', '');
input.setAttribute('checked', '');
el.insertBefore(input, node);
node.nodeValue = ((_b = node.nodeValue) === null || _b === void 0 ? void 0 : _b.replace(/^\[x\]/, '')) || null;
}
}
}));
}
/**
* Maps `NgxMdeOptions` to `MarkdownEditorOptions`.
*/
mapOptions(options) {
if (!options) {
return undefined;
}
const getMarkdownGuideUrl = (url) => {
if (!url)
return undefined;
if (typeof url === 'string') {
return url;
}
else {
return url[this.language] || url.default;
}
};
const markupTheme = options.markupThemes || [];
let editorThemes = options.editorThemes || [];
if (this.materialStyle) {
editorThemes = editorThemes ? editorThemes.concat('mde-material') : ['mde-material'];
}
else {
if (editorThemes) {
const index = editorThemes.findIndex((t) => t === 'mde-material');
if (index > -1)
editorThemes.splice(index, 1);
}
}
const shortcuts = {};
for (const actionName in DEFAULT_OPTIONS.shortcuts) {
if (options.shortcuts) {
shortcuts[actionName] = options.shortcuts[actionName];
}
}
return Object.assign(Object.assign({}, options), { shortcuts, disabled: this.disabled, themes: editorThemes.concat(markupTheme), markdownGuideUrl: getMarkdownGuideUrl(options.markdownGuideUrl) });
}
/**
* Applies the custom toolbar or the default toolbar as fallback.
*/
applyToolbarItems() {
let items;
if (this.toolbar.length) {
items = this.toolbar;
}
else {
items = this.toolbarService.DEFAULT_TOOLBAR;
}
this.normalizedToolbarItems = [];
for (const toolbarItem of items) {
const item = this.getNormalizedItem(toolbarItem);
if (!item) {
console.warn(`No default item defined for name "${toolbarItem}"`);
continue;
}
this.addSvgIcon(item);
this.normalizedToolbarItems.push(item);
}
this.applyShortcuts(this.normalizedToolbarItems);
}
/**
* Returns a complete item for all combinations of how a toolbar item can be specified and
* resolves the current value of internationalized properties. Only returns `undefined` for
* items specified by name and no such item can be found.
*
* In detail, item normalization means (in addition to i18n resolution):
* - For built-in items specified by name string, resolves the default item.
* - For built-in items specified partly, completes the object with default values for the missing properties.
* - For custom items specified partly, completes the object with empty values for the missing properties.
* - For custom items specified fully, returns as is.
* - For unknown items specified by name string, returns `undefined`.
*/
getNormalizedItem(toolbarItem) {
const getTooltip = (tooltip) => {
if (typeof tooltip === 'string') {
return tooltip;
}
else {
return tooltip[this.language] || tooltip.default;
}
};
const getIcon = (icon) => {
if ('format' in icon) {
return icon;
}
else {
return icon[this.language] || icon.default;
}
};
if (typeof toolbarItem === 'string') {
return this.toolbarService.getDefaultItem(toolbarItem);
}
else {
let defaultItem = this.toolbarService.getDefaultItem(toolbarItem.name);
if (!defaultItem) {
defaultItem = {
name: '',
action: () => { },
tooltip: '',
icon: { format: 'material', iconName: '' },
disableOnPreview: false,
};
}
return {
name: toolbarItem.name,
action: toolbarItem.action || defaultItem.action,
shortcut: toolbarItem.shortcut || defaultItem.shortcut,
isActive: toolbarItem.isActive || defaultItem.isActive,
tooltip: (toolbarItem.tooltip && getTooltip(toolbarItem.tooltip)) || defaultItem.tooltip,
icon: (toolbarItem.icon && getIcon(toolbarItem.icon)) || defaultItem.icon,
disableOnPreview: toolbarItem.disableOnPreview || (defaultItem === null || defaultItem === void 0 ? void 0 : defaultItem.disableOnPreview),
};
}
}
/**
* Creates tooltips for all configured toolbar items and stores them in `this.toolbarItemTooltips`.
*/
createTooltips() {
this.toolbarItemTooltips = new Array(this.normalizedToolbarItems.length);
for (let i = 0; i < this.normalizedToolbarItems.length; i++) {
const item = this.normalizedToolbarItems[i];
this.toolbarItemTooltips[i] = this.showTooltips ? this.createTooltip(item) : '';
}
}
/**
* Applies custom shortcuts.
*
* For items, whose actions originate in _Markdown Editor Core_, `options.shortcuts` is
* modified. For items that are specific to _Ngx Markdown Editor_ keybindings are applied to
* the `<ngx-markdown-editor>` element.
*/
applyShortcuts(items) {
var _a, _b, _c, _d;
if (this.options.shortcutsEnabled === 'none') {
return;
}
const applySetHeadingLevelShortcut = (shortcut) => {
const s = shortcut.replace(/(\w)-/gi, '$1.').replace(/Ctrl/gi, 'Control').replace(/Cmd/gi, 'Meta');
return this.hotkeys
.addKeybinding(this.hostElement.nativeElement, s)
.pipe(takeUntil(this.shortcutResetter))
.subscribe(() => {
this.blockBlur = true;
this.setHeadingLevelDropdown.open();
this.setHeadingLevelDropdown.focus();
});
};
const applyShortcut = (shortcut, action) => {
const s = shortcut.replace(/(\w)-/gi, '$1.').replace(/Ctrl/gi, 'Control').replace(/Cmd/gi, 'Meta');
return this.hotkeys
.addKeybinding(this.hostElement.nativeElement, s)
.pipe(takeUntil(this.shortcutResetter))
.subscribe(() => {
action();
this.determineActiveButtons();
});
};
this.shortcutResetter.next();
const shortcuts = {};
const appliedNgxMdeShortcuts = {};
if (this.options.shortcutsEnabled !== 'customOnly') {
const previewItem = this.toolbarService.getDefaultItem('togglePreview');
if (previewItem === null || previewItem === void 0 ? void 0 : previewItem.shortcut) {
const subscription = applyShortcut(previewItem.shortcut, previewItem.action);
appliedNgxMdeShortcuts[previewItem.name] = subscription;
}
const sideBySidePreviewItem = this.toolbarService.getDefaultItem('toggleSideBySidePreview');
if (sideBySidePreviewItem === null || sideBySidePreviewItem === void 0 ? void 0 : sideBySidePreviewItem.shortcut) {
const subscription = applyShortcut(sideBySidePreviewItem.shortcut, sideBySidePreviewItem.action);
appliedNgxMdeShortcuts[sideBySidePreviewItem.name] = subscription;
}
}
for (const item of items) {
if (item.name === 'setHeadingLevel' && item.shortcut) {
const subscription = applySetHeadingLevelShortcut(item.shortcut);
appliedNgxMdeShortcuts[item.name] = subscription;
}
else if (item.name in DEFAULT_OPTIONS.shortcuts) {
shortcuts[item.name] = item.shortcut;
}
else if (item.shortcut) {
(_a = appliedNgxMdeShortcuts[item.name]) === null || _a === void 0 ? void 0 : _a.unsubscribe();
const subscription = applyShortcut(item.shortcut, item.action);
appliedNgxMdeShortcuts[item.name] = subscription;
}
}
for (const actionName in this.options.shortcuts) {
if (this.options.shortcuts[actionName]) {
const shortcut = this.options.shortcuts[actionName];
if (actionName === 'setHeadingLevel') {
const item = items.find((i) => i.name === actionName);
if (item) {
(_b = appliedNgxMdeShortcuts[actionName]) === null || _b === void 0 ? void 0 : _b.unsubscribe();
applySetHeadingLevelShortcut(shortcut);
item.shortcut = shortcut;
}
}
else if (actionName in DEFAULT_OPTIONS.shortcuts) {
shortcuts[actionName] = shortcut;
}
else {
const item = items.find((i) => i.name === actionName);
const defaultItem = this.toolbarService.getDefaultItem(actionName);
if (item) {
(_c = appliedNgxMdeShortcuts[actionName]) === null || _c === void 0 ? void 0 : _c.unsubscribe();
applyShortcut(shortcut, item.action);
item.shortcut = shortcut;
}
else if (defaultItem) {
(_d = appliedNgxMdeShortcuts[actionName]) === null || _d === void 0 ? void 0 : _d.unsubscribe();
applyShortcut(shortcut, defaultItem.action);
}
}
}
}
this.options.shortcuts = shortcuts;
}
/**
* Adds the SVG specified inside `item.icon` to the injected `MatIconRegistry` instance.
*/
addSvgIcon(item) {
switch (item.icon.format) {
case 'svgString':
this.iconRegistry.addSvgIconLiteral(item.icon.iconName, this.domSanitizer.bypassSecurityTrustHtml(item.icon.svgHtmlString));
break