import { AfterViewInit, Component, ElementRef, EventEmitter, forwardRef, Inject, Input, NgZone, OnChanges, OnDestroy, Output, PLATFORM_ID, Renderer2, SecurityContext, SimpleChanges, ViewEncapsulation, HostBinding } from '@angular/core'; import { isPlatformServer, DOCUMENT} from '@angular/common'; import { DomSanitizer } from '@angular/platform-browser'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms'; import Quill from 'quill'; import { Convert2HtmlEditorToolbars, RichTextEditorToolbars } from '@farris/ui-common'; import { defaultModules, toolbarOptions } from './html-editor-defaults'; import { QUILL_CONFIG_TOKEN, QuillConfig, QuillFormat, QuillModules } from './html-editor.interfaces'; import { ImageResize } from './image-resize-module/ImageResize'; import { ImageDrop } from './image-drop-module/image-drop'; // 拖动加载图片组件。 import { Tooltip } from './tool-tip-module/index'; // 修改默认字体 const Font = Quill.import('attributors/style/font'); Font.whitelist = ['Microsoft-YaHei', 'SimSun', 'SimHei', 'KaiTi', 'FangSong', 'Arial', 'Times', 'sans-serif']; Quill.register(Font, true); // 注册图片拖拽和图片大小调整 Quill.register('modules/imageDrop', ImageDrop); Quill.register('modules/imageResize', ImageResize); Quill.register('modules/tooltip', Tooltip); // Because quill uses `document` directly, we cannot `import` during SSR // instead, we load dynamically via `require('quill')` in `ngAfterViewInit()` // declare var require: any // let Quill: any = null export interface CustomOption { import: string; whitelist: any[]; } export interface Range { index: number; length: number; } const getFormat = (format?: QuillFormat, configFormat?: QuillFormat): QuillFormat => { const passedFormat = format || configFormat; return passedFormat || 'html'; }; @Component({ selector: 'farris-html-editor', template: ` `, styleUrls: ['./html-editor.component.css'], encapsulation: ViewEncapsulation.None, providers: [ { multi: true, provide: NG_VALUE_ACCESSOR, // 自定义组件的数据绑定 useExisting: forwardRef(() => HtmlEditorComponent) }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => HtmlEditorComponent), multi: true } ] }) export class HtmlEditorComponent implements AfterViewInit, ControlValueAccessor, OnChanges, OnDestroy { quillEditor: any; editorElem: HTMLElement | undefined; content: any; private _disabled = false; // used to store initial value before ViewInit @HostBinding('class') rootClass = 'farris-html-editor'; @Input() @HostBinding('class.farris-html-editor-readonly') readonly?: boolean; @Input() @HostBinding('class.farris-html-editor-disabled') disabled = false; @Input() @HostBinding('class.farris-html-editor-border') showBorder = true; @Input() format?: 'object' | 'html' | 'text' | 'json'; @Input() theme?: string; @Input() modules?: QuillModules = { imageDrop: true, imageResize: true, tooltip: true }; @Input() debug?: 'warn' | 'log' | 'error' | false; @Input() placeholder?: string; @Input() maxLength?: number; @Input() minLength?: number; @Input() required = false; @Input() formats?: string[] | null; @Input() customToolbarPosition: 'top' | 'bottom' = 'top'; @Input() sanitize = false; // 净化dom @Input() styles: any = null; @Input() strict = true; @Input() scrollingContainer?: HTMLElement | string | null; @Input() bounds?: HTMLElement | string; @Input() customOptions: CustomOption[] = []; @Input() trackChanges?: 'user' | 'all'; @Input() preserveWhitespace = false; // 自定义toobar @Input() customToolbar = false; // 自定义功能按钮 @Input() toolbar: RichTextEditorToolbars; // 文字样式 @Input() fontStyleBar: string[] | boolean = true; // 代码块 @Input() BlockBar: string[] | boolean = false; // false; // 标题 @Input() headerBar: { header: any[] } | boolean = true; // 字号 @Input() sizeBar: { size: any[] } | boolean = true; // 列表 @Input() listBar: any[] | boolean = true; // 字体 @Input() fontBar: { font: string[] } | boolean = true; // 上标和下标 @Input() superAndSubscriptBar: any[] | boolean = true; // false; // 缩进 @Input() indentBar: any[] | boolean = true; // 文字颜色 @Input() colorBar: any[] | boolean = true; // false; // 媒体 @Input() mediaBar: string | boolean = true; // false; // 图片 @Input() imageBar: string | boolean = false; // 视频 @Input() videoBar: string | boolean = false; // false; // 清除 @Input() cleanBar: string | boolean = true; @Input() alignBar: string | boolean = true; @Output() onEditorCreated = new EventEmitter(); @Output() onContentChanged: EventEmitter<{ content: any; delta: any; editor: any; html: string | null; oldDelta: any; source: string; text: string; }> = new EventEmitter(); @Output() onSelectionChanged: EventEmitter<{ editor: any; oldRange: Range | null; range: Range | null; source: string; }> = new EventEmitter(); @Output() onFocus: EventEmitter<{ editor: any; source: string; }> = new EventEmitter(); @Output() onBlur: EventEmitter<{ editor: any; source: string; }> = new EventEmitter(); @Input() imageUpload: () => void; @Input() valueGetter = (quillEditor: any, editorElement: HTMLElement): string | any => { // tslint:disable-next-line: no-non-null-assertion let html: string | null = editorElement.querySelector('.ql-editor')!.innerHTML; if (html === '


' || html === '

') { html = null; } let modelValue = html; const format = getFormat(this.format, this.config.format); if (format === 'text') { modelValue = quillEditor.getText(); } else if (format === 'object') { modelValue = quillEditor.getContents(); } else if (format === 'json') { try { modelValue = JSON.stringify(quillEditor.getContents()); } catch (e) { modelValue = quillEditor.getText(); } } return modelValue; } @Input() valueSetter = (quillEditor: any, value: any): any => { const format = getFormat(this.format, this.config.format); if (format === 'html') { if (this.sanitize) { value = this.domSanitizer.sanitize(SecurityContext.HTML, value); } value = value ? (value + '').replace(/\s\s/g, '  ') : ''; return quillEditor.clipboard.convert(value); } else if (format === 'json') { try { return JSON.parse(value); } catch (e) { return [{ insert: value }]; } } return value; } constructor( private elementRef: ElementRef, private domSanitizer: DomSanitizer, @Inject(DOCUMENT) private doc: any, // tslint:disable-next-line:ban-types @Inject(PLATFORM_ID) private platformId: Object, private renderer: Renderer2, private zone: NgZone, @Inject(QUILL_CONFIG_TOKEN) private config: QuillConfig ) { } // tslint:disable-next-line:no-empty onModelChange(_modelValue?: any) { } // tslint:disable-next-line:no-empty onModelTouched() { } ngAfterViewInit(): void { this.editorCreate(); } setToolbar(toolbar) { console.log(toolbar); } private editorCreate() { if (isPlatformServer(this.platformId)) { return; } this.elementRef.nativeElement.insertAdjacentHTML( this.customToolbarPosition === 'top' ? 'beforeend' : 'afterbegin', this.preserveWhitespace ? '
' : '
' ); this.editorElem = this.elementRef.nativeElement.querySelector('[html-editor-element]'); const toolbarElem = this.elementRef.nativeElement.querySelector('[html-editor-toolbar]'); const modules = this.modules || this.config.modules || defaultModules; if (modules.toolbar === undefined) { modules.toolbar = defaultModules.toolbar; } if (this.customToolbar) { if (this.toolbar && this.toolbar.length) { modules.toolbar = Convert2HtmlEditorToolbars('concise', this.toolbar); } else { modules.toolbar = this.buildToolbar(toolbarOptions); } } if (modules.tooltip === undefined) { modules.tooltip = defaultModules.tooltip; } if (modules.imageDrop === undefined) { modules.imageDrop = defaultModules.imageDrop; } if (modules.imageResize === undefined) { modules.imageResize = defaultModules.imageResize; } let placeholder = this.placeholder !== undefined ? this.placeholder : this.config.placeholder; if (placeholder === undefined) { placeholder = ''; } if (toolbarElem) { // tslint:disable-next-line:no-string-literal modules['toolbar'] = toolbarElem; } if (this.styles) { Object.keys(this.styles).forEach((key: string) => { this.renderer.setStyle(this.editorElem, key, this.styles[key]); }); } this.customOptions.forEach(customOption => { const newCustomOption = Quill.import(customOption.import); newCustomOption.whitelist = customOption.whitelist; Quill.register(newCustomOption, true); }); let bounds = this.bounds && this.bounds === 'self' ? this.editorElem : this.bounds; if (!bounds) { bounds = this.config.bounds ? this.config.bounds : this.doc.body; } let debug = this.debug; if (!debug && debug !== false && this.config.debug) { debug = this.config.debug; } let readOnly = this.readonly; if (!readOnly && this.readonly !== false) { readOnly = this.config.readOnly !== undefined ? this.config.readOnly : false; } let scrollingContainer = this.scrollingContainer; if (!scrollingContainer && this.scrollingContainer !== null) { scrollingContainer = this.config.scrollingContainer === null || this.config.scrollingContainer ? this.config.scrollingContainer : null; } let formats = this.formats; if (!formats && formats === undefined) { formats = this.config.formats || this.config.formats === null ? this.config.formats : undefined; } // 初始化quill this.quillEditor = new Quill(this.editorElem, { bounds, debug, formats, modules, placeholder, readOnly, scrollingContainer, strict: this.strict, theme: this.theme || (this.config.theme ? this.config.theme : 'snow') }); if (this.content) { const format = getFormat(this.format, this.config.format); if (format === 'object') { this.quillEditor.setContents(this.content, 'silent'); } else if (format === 'text') { this.quillEditor.setText(this.content, 'silent'); } else if (format === 'json') { try { this.quillEditor.setContents(JSON.parse(this.content), 'silent'); } catch (e) { this.quillEditor.setText(this.content, 'silent'); } } else { if (this.sanitize) { this.content = this.domSanitizer.sanitize(SecurityContext.HTML, this.content); } const contents = this.quillEditor.clipboard.convert(this.content); this.quillEditor.setContents(contents, 'silent'); } this.quillEditor.history.clear(); } // initialize _disabled status based on this._disabled as default value // this.setDisabledState(this.disabled); this.setStatusEditor(this.readonly, this.disabled); this.onEditorCreated.emit(this.quillEditor); // mark model as touched if editor lost focus this.quillEditor.on('selection-change', this.selectionChangeHandler); // update model if text changes this.quillEditor.on('text-change', this.textChangeHandler); } random_id() { return Math.random().toString(36).slice(2); } selectionChangeHandler = (range: Range | null, oldRange: Range | null, source: string) => { this.zone.run(() => { if (range === null) { this.onBlur.emit({ editor: this.quillEditor, source }); } else if (oldRange === null) { this.onFocus.emit({ editor: this.quillEditor, source }); } this.onSelectionChanged.emit({ editor: this.quillEditor, oldRange, range, source }); if (!range && this.onModelTouched) { this.onModelTouched(); } }); } textChangeHandler = (delta: any, oldDelta: any, source: string): void => { // only emit changes emitted by user interactions const text = this.quillEditor.getText(); const content = this.quillEditor.getContents(); // tslint:disable-next-line: no-non-null-assertion let html: string | null = this.editorElem!.querySelector('.ql-editor')!.innerHTML; if (html === '


' || html === '

') { html = null; } this.zone.run(() => { const trackChanges = this.trackChanges || this.config.trackChanges; if ((source === Quill.sources.USER || (trackChanges && trackChanges === 'all')) && this.onModelChange) { this.onModelChange(this.valueGetter(this.quillEditor, this.editorElem!)); } this.onContentChanged.emit({ content, delta, editor: this.quillEditor, html, oldDelta, source, text }); }); } endEdit() { this.onModelTouched(); } buildToolbar(options: any) { const rtn = []; Object.keys(options).forEach(e => { if (this[e] === true) { rtn.push(options[e]); } else if (this[e]) { rtn.push(this[e]); } }); return rtn; } ngOnDestroy() { if (this.quillEditor) { this.quillEditor.off('selection-change', this.selectionChangeHandler); this.quillEditor.off('text-change', this.textChangeHandler); } } ngOnChanges(changes: SimpleChanges): void { if (!this.quillEditor) { return; } if (changes['disabled']) { this.setStatusEditor(false, this.disabled); } // tslint:disable:no-string-literal if (changes['readonly']) { this.quillEditor.enable(!changes['readonly'].currentValue); this.disableToolbar(changes['readonly'].currentValue); } if (changes['placeholder']) { this.quillEditor.root.dataset.placeholder = changes['placeholder'].currentValue; } if (changes['styles']) { const currentStyling = changes['styles'].currentValue; const previousStyling = changes['styles'].previousValue; if (previousStyling) { Object.keys(previousStyling).forEach((key: string) => { this.renderer.removeStyle(this.editorElem, key); }); } if (currentStyling) { Object.keys(currentStyling).forEach((key: string) => { this.renderer.setStyle(this.editorElem, key, this.styles[key]); }); } } // tslint:enable:no-string-literal } writeValue(currentValue: any) { this.content = currentValue; const format = getFormat(this.format, this.config.format); if (this.quillEditor) { if (currentValue) { if (format === 'text') { this.quillEditor.setText(currentValue); } else { this.quillEditor.setContents(this.valueSetter(this.quillEditor, this.content)); } return; } this.quillEditor.setText(''); } } disableToolbar(readonly: boolean) { const toolbarElem = this.elementRef.nativeElement.querySelector('.ql-toolbar'); if (toolbarElem) { toolbarElem.style.display = readonly ? 'none' : 'block'; } } setStatusEditor(readonly: boolean = false, disable: boolean) { // if (disable || readonly) { // this.elementRef.nativeElement.style.border = 'none'; // } this.disableToolbar(disable || readonly); this.setDisabledState(disable); } setDisabledState(isDisabled: boolean = this._disabled): void { // store initial value to set appropriate _disabled status after ViewInit this._disabled = isDisabled; if (this.quillEditor) { if (isDisabled) { this.quillEditor.disable(); this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'disabled'); } else { if (!this.readonly) { this.quillEditor.enable(); } this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled'); } } } registerOnChange(fn: (modelValue: any) => void): void { this.onModelChange = fn; } registerOnTouched(fn: () => void): void { this.onModelTouched = fn; } validate() { return null; // if (!this.quillEditor) { // } // const err: { // minLengthError?: { // given: number; // minLength: number; // }; // maxLengthError?: { // given: number; // maxLength: number; // }; // requiredError?: { empty: boolean }; // } = {}; // let valid = true; // const textLength = this.quillEditor.getText().trim().length; // if (this.minLength && textLength && textLength < this.minLength) { // err.minLengthError = { // given: textLength, // minLength: this.minLength // }; // valid = false; // } // if (this.maxLength && textLength > this.maxLength) { // err.maxLengthError = { // given: textLength, // maxLength: this.maxLength // }; // valid = false; // } // if (this.required && !textLength) { // err.requiredError = { // empty: true // }; // valid = false; // } // return valid ? null : err; } }