diff --git a/cypress/integration/menus/print.spec.ts b/cypress/integration/menus/print.spec.ts index 4d8923d..110d178 100644 --- a/cypress/integration/menus/print.spec.ts +++ b/cypress/integration/menus/print.spec.ts @@ -10,16 +10,16 @@ describe('菜单-打印', () => { it('打印', () => { - cy.getEditor().then((editor: Editor) => { - const imageList2 = editor.command.getImage() + cy.getEditor().then(async (editor: Editor) => { + const imageList2 = await editor.command.getImage() expect(imageList2.length).to.eq(2) editor.command.executeSelectAll() editor.command.executeBackspace() - cy.wait(200).then(() => { - const imageList1 = editor.command.getImage() + cy.wait(200).then(async () => { + const imageList1 = await editor.command.getImage() expect(imageList1.length).to.eq(1) }) }) diff --git a/src/editor/core/command/Command.ts b/src/editor/core/command/Command.ts index 9187e7a..a35f5e6 100644 --- a/src/editor/core/command/Command.ts +++ b/src/editor/core/command/Command.ts @@ -381,7 +381,7 @@ export class Command { return Command.changeImageDisplay(element, display) } - public getImage(): string[] { + public getImage(): Promise { return Command.getImage() } diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index 5972543..a596655 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -327,7 +327,10 @@ export class CommandAdapt { selection.forEach(el => { el.color = payload }) - this.draw.render({ isSetCursor: false }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) } public highlight(payload: string) { @@ -338,7 +341,10 @@ export class CommandAdapt { selection.forEach(el => { el.highlight = payload }) - this.draw.render({ isSetCursor: false }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) } public rowFlex(payload: RowFlex) { @@ -1173,7 +1179,8 @@ export class CommandAdapt { options.watermark.font = payload.font || font this.draw.render({ isSetCursor: false, - isSubmitHistory: false + isSubmitHistory: false, + isCompute: false }) } @@ -1185,7 +1192,8 @@ export class CommandAdapt { options.watermark = { ...defaultWatermarkOption } this.draw.render({ isSetCursor: false, - isSubmitHistory: false + isSubmitHistory: false, + isCompute: false }) } } @@ -1229,7 +1237,9 @@ export class CommandAdapt { if (index === null) return this.draw.render({ isSetCursor: false, - isSubmitHistory: false + isSubmitHistory: false, + isCompute: false, + isLazy: false }) } @@ -1238,7 +1248,9 @@ export class CommandAdapt { if (index === null) return this.draw.render({ isSetCursor: false, - isSubmitHistory: false + isSubmitHistory: false, + isCompute: false, + isLazy: false }) } @@ -1352,12 +1364,13 @@ export class CommandAdapt { }) } - public print() { + public async print() { const { width, height, scale } = this.options if (scale !== 1) { this.draw.setPageScale(1) } - printImageBase64(this.draw.getDataURL(), width, height) + const base64List = await this.draw.getDataURL() + printImageBase64(base64List, width, height) if (scale !== 1) { this.draw.setPageScale(scale) } @@ -1393,7 +1406,7 @@ export class CommandAdapt { }) } - public getImage(): string[] { + public getImage(): Promise { return this.draw.getDataURL() } diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index e7b2eb8..3b3927c 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -49,6 +49,7 @@ import { IMargin } from '../../interface/Margin' import { BlockParticle } from './particle/block/BlockParticle' import { EDITOR_COMPONENT, EDITOR_PREFIX } from '../../dataset/constant/Editor' import { I18n } from '../i18n/I18n' +import { ImageObserver } from '../observer/ImageObserver' export class Draw { @@ -96,6 +97,7 @@ export class Draw { private workerManager: WorkerManager private scrollObserver: ScrollObserver private selectionObserver: SelectionObserver + private imageObserver: ImageObserver private rowList: IRow[] private pageRowList: IRow[][] @@ -103,6 +105,7 @@ export class Draw { private painterOptions: IPainterOptions | null private visiblePageNoList: number[] private intersectionPageNo: number + private lazyRenderIntersectionObserver: IntersectionObserver | null constructor( rootContainer: HTMLElement, @@ -154,6 +157,7 @@ export class Draw { this.scrollObserver = new ScrollObserver(this) this.selectionObserver = new SelectionObserver() + this.imageObserver = new ImageObserver() this.canvasEvent = new CanvasEvent(this) this.cursor = new Cursor(this, this.canvasEvent) @@ -169,6 +173,7 @@ export class Draw { this.painterOptions = null this.visiblePageNoList = [] this.intersectionPageNo = 0 + this.lazyRenderIntersectionObserver = null this.render({ isSetCursor: false }) } @@ -412,6 +417,10 @@ export class Draw { return this.workerManager } + public getImageObserver(): ImageObserver { + return this.imageObserver + } + public getI18n(): I18n { return this.i18n } @@ -420,7 +429,14 @@ export class Draw { return this.rowList.length } - public getDataURL(): string[] { + public async getDataURL(): Promise { + this.render({ + isLazy: false, + isCompute: false, + isSetCursor: false, + isSubmitHistory: false + }) + await this.imageObserver.allSettled() return this.pageList.map(c => c.toDataURL()) } @@ -1096,11 +1112,35 @@ export class Draw { } } + private _lazyRender() { + const positionList = this.position.getOriginalPositionList() + this.lazyRenderIntersectionObserver?.disconnect() + this.lazyRenderIntersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const index = Number((entry.target).dataset.index) + this._drawPage(positionList, this.pageRowList[index], index) + } + }) + }) + this.pageList.forEach(el => { + this.lazyRenderIntersectionObserver!.observe(el) + }) + } + + private _immediateRender() { + const positionList = this.position.getOriginalPositionList() + for (let i = 0; i < this.pageRowList.length; i++) { + this._drawPage(positionList, this.pageRowList[i], i) + } + } + public render(payload?: IDrawOption) { const { isSubmitHistory = true, isSetCursor = true, - isCompute = true + isCompute = true, + isLazy = true } = payload || {} let { curIndex } = payload || {} const innerWidth = this.getInnerWidth() @@ -1118,30 +1158,22 @@ export class Draw { this.search.compute(searchKeyword) } } - // 清除光标 + // 清除光标等副作用 + this.imageObserver.clearAll() this.cursor.recoveryCursor() - // 绘制元素 + // 创建纸张 const positionList = this.position.getOriginalPositionList() - const { visiblePageNoList } = this.scrollObserver.getPageVisibleInfo() - const minPageNo = visiblePageNoList[0] - const maxPageNo = visiblePageNoList[visiblePageNoList.length - 1] for (let i = 0; i < this.pageRowList.length; i++) { if (!this.pageList[i]) { this._createPage(i) } - // 优先绘制可见页 - if (i >= minPageNo && i <= maxPageNo) { - this._drawPage(positionList, this.pageRowList[i], i) - } } - // 渲染后续不可见页 - nextTick(() => { - for (let i = 0; i < this.pageRowList.length; i++) { - if (i < minPageNo || i > maxPageNo) { - this._drawPage(positionList, this.pageRowList[i], i) - } - } - }) + // 绘制元素 + if (isLazy) { + this._lazyRender() + } else { + this._immediateRender() + } // 移除多余页 nextTick(() => { const curPageCount = this.pageRowList.length @@ -1187,9 +1219,8 @@ export class Draw { self.render({ curIndex, isSubmitHistory: false }) }) } - // 信息变动回调 - setTimeout(() => { + nextTick(() => { // 页面尺寸改变 if (this.listener.pageSizeChange) { this.listener.pageSizeChange(this.pageRowList.length) diff --git a/src/editor/core/draw/interactive/Search.ts b/src/editor/core/draw/interactive/Search.ts index b4548a2..349c388 100644 --- a/src/editor/core/draw/interactive/Search.ts +++ b/src/editor/core/draw/interactive/Search.ts @@ -261,7 +261,11 @@ export class Search { const searchMatchIndexList = this.getSearchNavigateIndexList() if (searchMatchIndexList.includes(s)) { ctx.fillStyle = searchNavigateMatchColor - this.searchNavigateScrollIntoView(position) + // 是否是第一个字符,则移动到可视范围 + const preSearchMatch = this.searchMatchList[s - 1] + if (!preSearchMatch || preSearchMatch.groupId !== searchMatch.groupId) { + this.searchNavigateScrollIntoView(position) + } } else { ctx.fillStyle = searchMatchColor } diff --git a/src/editor/core/draw/particle/ImageParticle.ts b/src/editor/core/draw/particle/ImageParticle.ts index 9cca801..0268528 100644 --- a/src/editor/core/draw/particle/ImageParticle.ts +++ b/src/editor/core/draw/particle/ImageParticle.ts @@ -4,14 +4,20 @@ import { Draw } from '../Draw' export class ImageParticle { + private draw: Draw protected options: Required protected imageCache: Map constructor(draw: Draw) { + this.draw = draw this.options = draw.getOptions() this.imageCache = new Map() } + protected addImageObserver(promise: Promise) { + this.draw.getImageObserver().add(promise) + } + public render(ctx: CanvasRenderingContext2D, element: IElement, x: number, y: number) { const { scale } = this.options const width = element.width! * scale @@ -20,12 +26,19 @@ export class ImageParticle { const img = this.imageCache.get(element.id!)! ctx.drawImage(img, x, y, width, height) } else { - const img = new Image() - img.src = element.value - img.onload = () => { - ctx.drawImage(img, x, y, width, height) - this.imageCache.set(element.id!, img) - } + const imageLoadPromise = new Promise((resolve, reject) => { + const img = new Image() + img.src = element.value + img.onload = () => { + ctx.drawImage(img, x, y, width, height) + this.imageCache.set(element.id!, img) + resolve(element) + } + img.onerror = (error) => { + reject(error) + } + }) + this.addImageObserver(imageLoadPromise) } } diff --git a/src/editor/core/draw/particle/latex/LaTexParticle.ts b/src/editor/core/draw/particle/latex/LaTexParticle.ts index f35a490..36cf9d9 100644 --- a/src/editor/core/draw/particle/latex/LaTexParticle.ts +++ b/src/editor/core/draw/particle/latex/LaTexParticle.ts @@ -21,12 +21,19 @@ export class LaTexParticle extends ImageParticle { const img = this.imageCache.get(element.value)! ctx.drawImage(img, x, y, width, height) } else { - const img = new Image() - img.src = element.laTexSVG! - img.onload = () => { - ctx.drawImage(img, x, y, width, height) - this.imageCache.set(element.value, img) - } + const laTexLoadPromise = new Promise((resolve, reject) => { + const img = new Image() + img.src = element.laTexSVG! + img.onload = () => { + ctx.drawImage(img, x, y, width, height) + this.imageCache.set(element.value, img) + resolve(element) + } + img.onerror = (error) => { + reject(error) + } + }) + this.addImageObserver(laTexLoadPromise) } } diff --git a/src/editor/core/observer/ImageObserver.ts b/src/editor/core/observer/ImageObserver.ts new file mode 100644 index 0000000..589ff09 --- /dev/null +++ b/src/editor/core/observer/ImageObserver.ts @@ -0,0 +1,21 @@ +export class ImageObserver { + + private promiseList: Promise[] + + constructor() { + this.promiseList = [] + } + + public add(payload: Promise) { + this.promiseList.push(payload) + } + + public clearAll() { + this.promiseList = [] + } + + public allSettled() { + return Promise.allSettled(this.promiseList) + } + +} \ No newline at end of file diff --git a/src/editor/interface/Draw.ts b/src/editor/interface/Draw.ts index 45114e5..2642751 100644 --- a/src/editor/interface/Draw.ts +++ b/src/editor/interface/Draw.ts @@ -6,6 +6,7 @@ export interface IDrawOption { isSetCursor?: boolean; isSubmitHistory?: boolean; isCompute?: boolean; + isLazy?: boolean; } export interface IDrawImagePayload {