Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reduce asynchronous stuff to resolve flickering #1951

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions projects/ngx-quill/config/src/quill-editor.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { InjectionToken } from '@angular/core'
import type { QuillOptions } from 'quill'
import type { Observable } from 'rxjs'

import { defaultModules } from './quill-defaults'
import type { QuillOptions } from 'quill'

export interface CustomOption {
import: string
Expand Down Expand Up @@ -62,6 +63,8 @@ export interface QuillModules {

export type QuillFormat = 'object' | 'json' | 'html' | 'text'

export type QuillBeforeRender = (() => Promise<any>) | (() => Observable<any>)

export interface QuillConfig {
bounds?: HTMLElement | string
customModules?: CustomModule[]
Expand All @@ -82,7 +85,7 @@ export interface QuillConfig {
sanitize?: boolean
// A function, which is executed before the Quill editor is rendered, this might be useful
// for lazy-loading CSS.
beforeRender?: () => Promise<any>
beforeRender?: QuillBeforeRender
}

export const QUILL_CONFIG_TOKEN = new InjectionToken<QuillConfig>('config', {
Expand Down
6 changes: 3 additions & 3 deletions projects/ngx-quill/src/lib/quill-editor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1457,7 +1457,7 @@ describe('QuillEditor - beforeRender', () => {
imports: [QuillModule.forRoot(config)],
})

spyOn(config, 'beforeRender')
spyOn(config, 'beforeRender').and.callThrough()

fixture = TestBed.createComponent(BeforeRenderTestComponent)
fixture.detectChanges()
Expand All @@ -1474,11 +1474,11 @@ describe('QuillEditor - beforeRender', () => {
imports: [QuillModule.forRoot(config)],
})

spyOn(config, 'beforeRender')
spyOn(config, 'beforeRender').and.callThrough()

fixture = TestBed.createComponent(BeforeRenderTestComponent)
fixture.componentInstance.beforeRender = () => Promise.resolve()
spyOn(fixture.componentInstance, 'beforeRender')
spyOn(fixture.componentInstance, 'beforeRender').and.callThrough()
fixture.detectChanges()
await fixture.whenStable()

Expand Down
13 changes: 3 additions & 10 deletions projects/ngx-quill/src/lib/quill-editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { debounceTime, mergeMap } from 'rxjs/operators'

import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'

import { CustomModule, CustomOption, defaultModules, QuillModules } from 'ngx-quill/config'
import { CustomModule, CustomOption, defaultModules, QuillBeforeRender, QuillModules } from 'ngx-quill/config'

import type History from 'quill/modules/history'
import type Toolbar from 'quill/modules/toolbar'
Expand Down Expand Up @@ -92,7 +92,7 @@ export abstract class QuillEditorBase implements AfterViewInit, ControlValueAcce
readonly formats = input<string[] | null | undefined>(undefined)
readonly customToolbarPosition = input<'top' | 'bottom'>('top')
readonly sanitize = input<boolean | undefined>(undefined)
readonly beforeRender = input<() => Promise<any> | undefined>(undefined)
readonly beforeRender = input<QuillBeforeRender>(undefined)
readonly styles = input<any>(null)
readonly registry = input<QuillOptions['registry']>(
undefined
Expand Down Expand Up @@ -223,14 +223,7 @@ export abstract class QuillEditorBase implements AfterViewInit, ControlValueAcce
// this will lead to runtime exceptions, since the code will be executed on DOM nodes that don't exist within the tree.

this.quillSubscription = this.service.getQuill().pipe(
mergeMap((Quill) => {
const promises = [this.service.registerCustomModules(Quill, this.customModules())]
const beforeRender = this.beforeRender() ?? this.service.config.beforeRender
if (beforeRender) {
promises.push(beforeRender())
}
return Promise.all(promises).then(() => Quill)
})
mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))
).subscribe(Quill => {
this.editorElem = this.elementRef.nativeElement.querySelector(
'[quill-editor-element]'
Expand Down
33 changes: 13 additions & 20 deletions projects/ngx-quill/src/lib/quill-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@ import type QuillType from 'quill'
import {
AfterViewInit,
Component,
DestroyRef,
ElementRef,
EventEmitter,
Inject,
NgZone,
OnChanges,
OnDestroy,
Output,
PLATFORM_ID,
Renderer2,
SecurityContext,
SimpleChanges,
ViewEncapsulation,
NgZone,
SecurityContext,
OnDestroy,
input,
EventEmitter,
Output,
inject,
DestroyRef
input
} from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { Subscription } from 'rxjs'
import { DomSanitizer } from '@angular/platform-browser'
import type { Subscription } from 'rxjs'
import { mergeMap } from 'rxjs/operators'

import { CustomOption, CustomModule, QuillModules } from 'ngx-quill/config'
import { CustomModule, CustomOption, QuillBeforeRender, QuillModules } from 'ngx-quill/config'

import { getFormat, raf$ } from './helpers'
import { QuillService } from './quill.service'
import { DomSanitizer } from '@angular/platform-browser'

@Component({
encapsulation: ViewEncapsulation.None,
Expand All @@ -52,7 +52,7 @@ export class QuillViewComponent implements AfterViewInit, OnChanges, OnDestroy {
readonly debug = input<'warn' | 'log' | 'error' | false>(false)
readonly formats = input<string[] | null | undefined>(undefined)
readonly sanitize = input<boolean | undefined>(undefined)
readonly beforeRender = input<() => Promise<any> | undefined>(undefined)
readonly beforeRender = input<QuillBeforeRender>()
readonly strict = input(true)
readonly content = input<any>()
readonly customModules = input<CustomModule[]>([])
Expand All @@ -74,7 +74,7 @@ export class QuillViewComponent implements AfterViewInit, OnChanges, OnDestroy {
protected service: QuillService,
protected domSanitizer: DomSanitizer,
@Inject(PLATFORM_ID) protected platformId: any,
) {}
) { }

valueSetter = (quillEditor: QuillType, value: any): any => {
const format = getFormat(this.format(), this.service.config.format)
Expand Down Expand Up @@ -114,14 +114,7 @@ export class QuillViewComponent implements AfterViewInit, OnChanges, OnDestroy {
}

this.quillSubscription = this.service.getQuill().pipe(
mergeMap((Quill) => {
const promises = [this.service.registerCustomModules(Quill, this.customModules())]
const beforeRender = this.beforeRender() ?? this.service.config.beforeRender
if (beforeRender) {
promises.push(beforeRender())
}
return Promise.all(promises).then(() => Quill)
})
mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))
).subscribe(Quill => {
const modules = Object.assign({}, this.modules() || this.service.config.modules)
modules.toolbar = false
Expand Down
102 changes: 63 additions & 39 deletions projects/ngx-quill/src/lib/quill.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { DOCUMENT } from '@angular/common'
import { Inject, Injectable, Injector, Optional } from '@angular/core'
import { defer, firstValueFrom, isObservable, Observable } from 'rxjs'
import { shareReplay } from 'rxjs/operators'
import { inject, Injectable } from '@angular/core'
import { defer, firstValueFrom, forkJoin, from, isObservable, Observable, of } from 'rxjs'
import { map, shareReplay, tap } from 'rxjs/operators'

import {
CustomModule,
defaultModules,
QUILL_CONFIG_TOKEN,
QuillConfig,
QuillConfig
} from 'ngx-quill/config'

@Injectable({
providedIn: 'root',
})
export class QuillService {
readonly config = inject(QUILL_CONFIG_TOKEN) || { modules:defaultModules } as QuillConfig

private document = inject(DOCUMENT)

private Quill!: any
private document: Document

private quill$: Observable<any> = defer(async () => {
if (!this.Quill) {
// Quill adds events listeners on import https://github.com/quilljs/quill/blob/develop/core/emitter.js#L8
Expand Down Expand Up @@ -54,54 +58,74 @@ export class QuillService {
)
})

return await this.registerCustomModules(
return firstValueFrom(this.registerCustomModules(
this.Quill,
this.config.customModules,
this.config.suppressGlobalRegisterWarning
)
}).pipe(shareReplay({ bufferSize: 1,
refCount: true }))

constructor(
injector: Injector,
@Optional() @Inject(QUILL_CONFIG_TOKEN) public config: QuillConfig
) {
this.document = injector.get(DOCUMENT)
))
}).pipe(
shareReplay({
bufferSize: 1,
refCount: false
})
)

if (!this.config) {
this.config = { modules: defaultModules }
}
}
// A list of custom modules that have already been registered,
// so we don’t need to await their implementation.
private registeredModules = new Set<string>()

getQuill() {
return this.quill$
}

/**
* Marked as internal so it won't be available for `ngx-quill` consumers, this is only
* internal method to be used within the library.
*
* @internal
*/
async registerCustomModules(
/** @internal */
beforeRender(Quill: any, customModules: CustomModule[] | undefined, beforeRender = this.config.beforeRender) {
// This function is called each time the editor needs to be rendered,
// so it operates individually per component. If no custom module needs to be
// registered and no `beforeRender` function is provided, it will emit
// immediately and proceed with the rendering.
const sources = [this.registerCustomModules(Quill, customModules)]
if (beforeRender) {
sources.push(from(beforeRender()))
}
return forkJoin(sources).pipe(map(() => Quill))
}

/** @internal */
private registerCustomModules(
Quill: any,
customModules: CustomModule[] | undefined,
suppressGlobalRegisterWarning?: boolean
): Promise<any> {
if (Array.isArray(customModules)) {
// eslint-disable-next-line prefer-const
for (let { implementation, path } of customModules) {
// The `implementation` might be an observable that resolves the actual implementation,
// e.g. if it should be lazy loaded.
if (isObservable(implementation)) {
implementation = await firstValueFrom(implementation)
}
Quill.register(path, implementation, suppressGlobalRegisterWarning)
) {
if (!Array.isArray(customModules)) {
return of(Quill)
}

const sources: Observable<unknown>[] = []

for (const customModule of customModules) {
const { path, implementation: maybeImplementation } = customModule

// If the module is already registered, proceed to the next module...
if (this.registeredModules.has(path)) {
continue
}

this.registeredModules.add(path)

if (isObservable(maybeImplementation)) {
// If the implementation is an observable, we will wait for it to load and
// then register it with Quill. The caller will wait until the module is registered.
sources.push(maybeImplementation.pipe(
tap((implementation) => {
Quill.register(path, implementation, suppressGlobalRegisterWarning)
})
))
} else {
Quill.register(path, maybeImplementation, suppressGlobalRegisterWarning)
}
}

// Return `Quill` constructor so we'll be able to re-use its return value except of using
// `map` operators, etc.
return Quill
return sources.length > 0 ? forkJoin(sources).pipe(map(() => Quill)) : of(Quill)
}
}