Как интегрировать редактор Quill с Angular Материалом, затрудняющим пользовательский MatFormFieldControl - PullRequest
0 голосов
/ 29 января 2020

Я пытаюсь интегрировать редактор Quill с Angular Материал, затрудняющий пользовательский MatFormFieldControl здесь: https://github.com/sermicromegas/learning-management-system/tree/master/frontend/src/app/shared/ui/quill-material

Вы можете увидеть рабочую демонстрацию здесь (см. Поле "description") ): https://ser-learning-management-system.herokuapp.com/course/edit/5e2089a2c4fea300172bca3e

Вот руководство по созданию настраиваемого MatFormFieldControl: https://material.angular.io/guide/creating-a-custom-form-field-control

Это работает, но не так, как оно должно быть. Например (go к пустой форме: https://ser-learning-management-system.herokuapp.com/course/create):

  1. Если вы щелкнете по полю «Заголовок», оно все еще будет действительным, снова щелкните за пределами поля, это становится недействительным. Но если вы нажмете на Описание, оно сразу станет недействительным
  2. В quill-material.component.ts, метод writeValue () я бы не стал делать что-то вроде this.editor.root.innerHTML = contents; для записи исходного содержимого в редактор, но я не знаю, как это сделать

Может ли кто-нибудь помочь мне улучшить quill-material.component.ts? Не стесняйтесь проверить мой код github, если хотите поиграть с ним.

Большое спасибо

1 Ответ

1 голос
/ 30 января 2020

Если кому-то может помочь, это исходный код компонента, протестированный на Angular 9. Я назвал его quill-material, чтобы вы могли использовать его так:

<mat-form-field>
    <quill-material
      formControlName="your_control_name"
      placeholder="Type your text here..."
      required
    ></quill-material>
    <mat-error *ngIf="formGroup.get('your_control_name').hasError('required')">
      Field is <strong>required</strong>
    </mat-error>
</mat-form-field>

import {
  Component,
  Input,
  OnInit,
  ElementRef,
  ViewChild,
  forwardRef,
  OnDestroy,
  Injector,
  DoCheck,
  HostBinding
} from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, NgControl } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material';
import { Subject } from 'rxjs';
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import Quill from 'quill';
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html';

const SELECTOR = 'quill-material';

@Component({
  selector: SELECTOR,
  template: `<div class="quill-material-container" #container>
     <div class="editor" (click)="onTouched()" [ngStyle]="{'height': '200px'}"></div>
   </div>`,
  styles: [`img {
      position: relative;
    }`],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => QuillMaterialComponent),
    multi: true
  },
  {
    provide: MatFormFieldControl,
    useExisting: QuillMaterialComponent
  }],
  host: {
    '[id]': 'id',
    '[attr.aria-describedby]': 'describedBy'
  }
})
export class QuillMaterialComponent implements OnInit, DoCheck, OnDestroy, ControlValueAccessor, MatFormFieldControl<any> {
  static nextId = 0;
  @HostBinding() id = `quill-material-${QuillMaterialComponent.nextId++}`;

  @ViewChild('container', { read: ElementRef, static: true }) container: ElementRef;

  stateChanges = new Subject<void>();

  quill: any = Quill;
  editor: any;
  controlType = 'quill-material';
  errorState = false;
  ngControl: any;
  touched = false;
  focused = false;

  _value: any;

  get value(): any {
    return this._value;
  }
  set value(value) {
    this._value = value;
    this.editor.setContents(this._value);
    this.onChange(value);
    this.stateChanges.next();
  }

  @Input()
  get placeholder() {
    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  public _placeholder: string;

  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  public _required = false;

  @Input()
  get disabled() {
    return this._disabled;
  }
  set disabled(disabled) {
    this._disabled = coerceBooleanProperty(disabled);
    this.stateChanges.next();
  }
  public _disabled = false;

  get empty() {
    const text = this.editor.getText().trim();
    return text ? false : true;
  }

  @HostBinding('class.floating')
  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  @HostBinding('attr.aria-describedby') describedBy = '';
  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  constructor(public elRef: ElementRef, public injector: Injector, public fm: FocusMonitor) {
    fm.monitor(elRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
  }

  ngOnInit(): void {
    // avoid Cyclic Dependency
    this.ngControl = this.injector.get(NgControl);
    if (this.ngControl != null) { this.ngControl.valueAccessor = this; }

    const editorRef = this.container.nativeElement.querySelector('.editor');
    this.editor = new Quill(editorRef, { theme: 'snow' });
    this.editor.on('text-change', () => {
      if (this.ngControl.touched) {
        this.onChange(this.getValue());
      }
    });
  }

  ngDoCheck(): void {
    if (this.ngControl) {
      this.errorState = this.ngControl.invalid && this.ngControl.touched && !this.focused;
      this.stateChanges.next();
    }
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }

  writeValue(contents: any): void {
    if (this.editor && contents) {
      const delta = this.editor.clipboard.convert(contents); // convert html to delta
      this.editor.setContents(delta);
      this._value = contents;
    }
  }

  onChange = (delta: any) => { };

  registerOnChange(fn: (v: any) => void): void {
    this.onChange = fn;
  }

  onTouched = () => { };

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  onContainerClick(event: MouseEvent) {
    if (!this.focused) {
      this.editor.focus();
      this.focused = true;
      this.stateChanges.next();
    }
  }

  private getValue(): any | undefined {
    if (!this.editor) {
      return undefined;
    }

    const delta: any = this.editor.getContents();
    if (this.isEmpty(delta)) {
      return undefined;
    }

    const converter = new QuillDeltaToHtmlConverter(delta.ops, {});
    const html = converter.convert();

    return html;
  }

  private isEmpty(contents: any): boolean {
    if (contents.ops.length > 1) {
      return false;
    }

    const opsTypes: Array<string> = Object.keys(contents.ops[0]);

    if (opsTypes.length > 1) {
      return false;
    }

    if (opsTypes[0] !== 'insert') {
      return false;
    }

    if (contents.ops[0].insert !== '\n') {
      return false;
    }

    return true;
  }
}
...