mirror of https://github.com/Squidex/squidex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
9.8 KiB
298 lines
9.8 KiB
/*
|
|
* Squidex Headless CMS
|
|
*
|
|
* @license
|
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
|
|
*/
|
|
|
|
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core';
|
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
import { ApiUrlConfig, AssetDto, AssetUploaderState, DialogModel, ResourceLoaderService, StatefulControlComponent, Types, UploadCanceled } from '@app/shared/internal';
|
|
import { marked } from 'marked';
|
|
|
|
declare const SimpleMDE: any;
|
|
|
|
export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
|
|
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true,
|
|
};
|
|
|
|
interface State {
|
|
// True, when the editor is shown as fullscreen.
|
|
isFullscreen: boolean;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'sqx-markdown-editor',
|
|
styleUrls: ['./markdown-editor.component.scss'],
|
|
templateUrl: './markdown-editor.component.html',
|
|
providers: [
|
|
SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR,
|
|
],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class MarkdownEditorComponent extends StatefulControlComponent<State, string> implements AfterViewInit {
|
|
private simplemde: any;
|
|
private value: string;
|
|
|
|
@Input()
|
|
public folderId?: string;
|
|
|
|
@Input()
|
|
public set disabled(value: boolean | undefined | null) {
|
|
this.setDisabledState(value === true);
|
|
}
|
|
|
|
@ViewChild('editor', { static: false })
|
|
public editor: ElementRef;
|
|
|
|
@ViewChild('container', { static: false })
|
|
public container: ElementRef;
|
|
|
|
@ViewChild('inner', { static: false })
|
|
public inner: ElementRef;
|
|
|
|
public assetsDialog = new DialogModel();
|
|
|
|
constructor(changeDetector: ChangeDetectorRef,
|
|
private readonly apiUrl: ApiUrlConfig,
|
|
private readonly assetUploader: AssetUploaderState,
|
|
private readonly renderer: Renderer2,
|
|
private readonly resourceLoader: ResourceLoaderService,
|
|
) {
|
|
super(changeDetector, {
|
|
isFullscreen: false,
|
|
});
|
|
}
|
|
|
|
public writeValue(obj: any) {
|
|
this.value = Types.isString(obj) ? obj : '';
|
|
|
|
if (this.simplemde) {
|
|
this.simplemde.value(this.value);
|
|
}
|
|
}
|
|
|
|
public onDisabled(isDisabled: boolean) {
|
|
if (this.simplemde) {
|
|
this.simplemde.codemirror.setOption('readOnly', isDisabled);
|
|
}
|
|
}
|
|
|
|
private showSelector = () => {
|
|
if (this.snapshot.isDisabled) {
|
|
return;
|
|
}
|
|
|
|
this.assetsDialog.show();
|
|
};
|
|
|
|
public ngAfterViewInit() {
|
|
Promise.all([
|
|
this.resourceLoader.loadLocalStyle('dependencies/simplemde/simplemde.min.css'),
|
|
this.resourceLoader.loadLocalStyle('dependencies/font-awesome/css/font-awesome.min.css'),
|
|
this.resourceLoader.loadLocalScript('dependencies/simplemde/simplemde.min.js'),
|
|
]).then(() => {
|
|
this.simplemde = new SimpleMDE({
|
|
previewRender: (text: string) => {
|
|
return marked(text, { pedantic: true });
|
|
},
|
|
autoDownloadFontAwesome: true,
|
|
spellChecker: false,
|
|
status: ['lines', 'words', 'cursor'],
|
|
toolbar: [
|
|
{
|
|
name: 'bold',
|
|
action: SimpleMDE.toggleBold,
|
|
className: 'fa fa-bold',
|
|
title: 'Bold',
|
|
}, {
|
|
name: 'italic',
|
|
action: SimpleMDE.toggleItalic,
|
|
className: 'fa fa-italic',
|
|
title: 'Italic',
|
|
}, {
|
|
name: 'heading',
|
|
action: SimpleMDE.toggleHeadingSmaller,
|
|
className: 'fa fa-header',
|
|
title: 'Heading',
|
|
}, {
|
|
name: 'quote',
|
|
action: SimpleMDE.toggleBlockquote,
|
|
className: 'fa fa-quote-left',
|
|
title: 'Quote',
|
|
}, {
|
|
name: 'unordered-list',
|
|
action: SimpleMDE.toggleUnorderedList,
|
|
className: 'fa fa-list-ul',
|
|
title: 'Generic List',
|
|
}, {
|
|
name: 'ordered-list',
|
|
action: SimpleMDE.toggleOrderedList,
|
|
className: 'fa fa-list-ol',
|
|
title: 'Numbered List',
|
|
},
|
|
'|',
|
|
{
|
|
name: 'link',
|
|
action: SimpleMDE.drawLink,
|
|
className: 'fa fa-link',
|
|
title: 'Create Link',
|
|
}, {
|
|
name: 'image',
|
|
action: SimpleMDE.drawImage,
|
|
className: 'fa fa-picture-o',
|
|
title: 'Insert Image',
|
|
},
|
|
'|',
|
|
{
|
|
name: 'preview',
|
|
action: SimpleMDE.togglePreview,
|
|
className: 'fa fa-eye no-disable',
|
|
title: 'Toggle Preview',
|
|
}, {
|
|
name: 'fullscreen',
|
|
action: SimpleMDE.toggleFullScreen,
|
|
className: 'fa fa-arrows-alt no-disable no-mobile',
|
|
title: 'Toggle Fullscreen',
|
|
}, {
|
|
name: 'side-by-side',
|
|
action: SimpleMDE.toggleSideBySide,
|
|
className: 'fa fa-columns no-disable no-mobile',
|
|
title: 'Toggle Side by Side',
|
|
},
|
|
'|',
|
|
{
|
|
name: 'guide',
|
|
action: 'https://simplemde.com/markdown-guide',
|
|
className: 'fa fa-question-circle',
|
|
title: 'Markdown Guide',
|
|
},
|
|
'|',
|
|
{
|
|
name: 'assets',
|
|
action: this.showSelector,
|
|
className: 'icon-assets icon-bold',
|
|
title: 'Insert Assets',
|
|
},
|
|
],
|
|
element: this.editor.nativeElement,
|
|
});
|
|
|
|
this.simplemde.value(this.value || '');
|
|
this.simplemde.codemirror.setOption('readOnly', this.snapshot.isDisabled);
|
|
|
|
this.simplemde.codemirror.on('change', () => {
|
|
const value = this.simplemde.value();
|
|
|
|
if (this.value !== value) {
|
|
this.value = value;
|
|
|
|
this.callChange(value);
|
|
}
|
|
});
|
|
|
|
this.simplemde.codemirror.on('refresh', () => {
|
|
const isFullscreen = this.simplemde.isFullscreenActive();
|
|
|
|
this.toggleFullscreen(isFullscreen);
|
|
});
|
|
|
|
this.simplemde.codemirror.on('blur', () => {
|
|
this.callTouched();
|
|
});
|
|
});
|
|
}
|
|
|
|
public insertAssets(assets: ReadonlyArray<AssetDto>) {
|
|
const content = this.buildMarkups(assets);
|
|
|
|
if (content.length > 0) {
|
|
this.simplemde.codemirror.replaceSelection(content);
|
|
}
|
|
|
|
this.assetsDialog.hide();
|
|
}
|
|
|
|
public insertFiles(files: ReadonlyArray<File>) {
|
|
const doc = this.simplemde.codemirror.getDoc();
|
|
|
|
for (const file of files) {
|
|
this.uploadFile(doc, file);
|
|
}
|
|
}
|
|
|
|
private uploadFile(doc: any, file: File) {
|
|
if (this.snapshot.isDisabled) {
|
|
return;
|
|
}
|
|
|
|
const uploadCursor = doc.getCursor();
|
|
const uploadText = `![Uploading file...${new Date()}]()`;
|
|
|
|
doc.replaceSelection(uploadText);
|
|
|
|
const replaceText = (replacement: string) => {
|
|
const cursor = doc.getCursor();
|
|
|
|
const text = doc.getValue().replace(uploadText, replacement);
|
|
|
|
doc.setValue(text);
|
|
|
|
if (uploadCursor && uploadCursor.line === cursor.line) {
|
|
const offset = replacement.length - uploadText.length;
|
|
|
|
doc.setCursor({ line: cursor.line, ch: cursor.ch + offset });
|
|
} else {
|
|
doc.setCursor(cursor);
|
|
}
|
|
};
|
|
|
|
this.assetUploader.uploadFile(file, undefined, this.folderId)
|
|
.subscribe({
|
|
next: asset => {
|
|
if (Types.is(asset, AssetDto)) {
|
|
replaceText(this.buildMarkup(asset));
|
|
}
|
|
},
|
|
error: error => {
|
|
if (!Types.is(error, UploadCanceled)) {
|
|
replaceText('FAILED');
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
private toggleFullscreen(isFullscreen: boolean) {
|
|
let target = this.container.nativeElement;
|
|
|
|
if (isFullscreen) {
|
|
target = document.body;
|
|
}
|
|
|
|
this.renderer.appendChild(target, this.inner.nativeElement);
|
|
|
|
this.next({ isFullscreen });
|
|
}
|
|
|
|
private buildMarkups(assets: readonly AssetDto[]) {
|
|
let content = '';
|
|
|
|
for (const asset of assets) {
|
|
content += this.buildMarkup(asset);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
private buildMarkup(asset: AssetDto) {
|
|
const name = asset.fileNameWithoutExtension;
|
|
|
|
if (asset.type === 'Image' || asset.mimeType === 'image/svg+xml' || asset.fileName.endsWith('.svg')) {
|
|
return `} '${name}')`;
|
|
} else if (asset.type === 'Video') {
|
|
return `[${name}](${asset.fullUrl(this.apiUrl)}')`;
|
|
} else {
|
|
return `[${name}](${asset.fullUrl(this.apiUrl)}')`;
|
|
}
|
|
}
|
|
}
|
|
|