UNPKG

@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
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