From 3064fd89dbd153573e57d64f40ada4efb9e17857 Mon Sep 17 00:00:00 2001 From: jocs Date: Sat, 22 Jun 2024 10:22:17 +0800 Subject: [PATCH 01/39] feat: doc support header and footer --- packages/core/src/types/interfaces/i-document-data.ts | 6 +++--- packages/engine-render/src/basics/interfaces.ts | 4 ++-- .../engine-render/src/components/docs/layout/model/page.ts | 4 ++-- packages/engine-render/src/components/docs/layout/tools.ts | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/types/interfaces/i-document-data.ts b/packages/core/src/types/interfaces/i-document-data.ts index 8b0b609f469..ca17b6be2da 100644 --- a/packages/core/src/types/interfaces/i-document-data.ts +++ b/packages/core/src/types/interfaces/i-document-data.ts @@ -323,14 +323,14 @@ export enum BlockType { } export interface IHeaderAndFooterBase { - defaultHeaderId?: string; // defaultHeaderId - defaultFooterId?: string; // defaultFooterId + defaultHeaderId?: string; // defaultHeaderId or the odd page header id + defaultFooterId?: string; // defaultFooterId or the odd page footer id evenPageHeaderId?: string; // evenPageHeaderId evenPageFooterId?: string; // evenPageFooterId firstPageHeaderId?: string; // firstPageHeaderId firstPageFooterId?: string; // firstPageFooterId useFirstPageHeaderFooter?: BooleanNumber; // useFirstPageHeaderFooter - useEvenPageHeaderFooter?: BooleanNumber; // useEvenPageHeaderFooter, + evenAndOddHeaders?: BooleanNumber; // useEvenPageHeaderFooter, } /** diff --git a/packages/engine-render/src/basics/interfaces.ts b/packages/engine-render/src/basics/interfaces.ts index 635dd916a9b..1293f5304e5 100644 --- a/packages/engine-render/src/basics/interfaces.ts +++ b/packages/engine-render/src/basics/interfaces.ts @@ -103,7 +103,7 @@ export interface ISectionBreakConfig extends IDocStyleBase, ISectionBreakBase, I headerIds?: IHeaderIds; footerIds?: IFooterIds; useFirstPageHeaderFooter?: BooleanNumber; - useEvenPageHeaderFooter?: BooleanNumber; + evenAndOddHeaders?: BooleanNumber; } export interface IParagraphConfig { @@ -138,7 +138,7 @@ export interface IFontCreateConfig { // footers?: IFooters; // headers?: IHeaders; // useFirstPageHeaderFooter?: boolean; -// useEvenPageHeaderFooter?: boolean; +// evenAndOddHeaders?: boolean; // } export interface INodeInfo { diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index c1a40d59238..06f87278467 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -49,7 +49,7 @@ export function createSkeletonPage( headerIds = {}, footerIds = {}, useFirstPageHeaderFooter, - useEvenPageHeaderFooter, + evenAndOddHeaders, footerTreeMap, headerTreeMap, columnProperties = [], @@ -88,7 +88,7 @@ export function createSkeletonPage( if (pageNumber === pageNumberStart && useFirstPageHeaderFooter) { headerId = firstPageHeaderId ?? ''; footerId = firstPageFooterId ?? ''; - } else if (pageNumber % 2 === 0 && useEvenPageHeaderFooter) { + } else if (pageNumber % 2 === 0 && evenAndOddHeaders) { headerId = evenPageHeaderId ?? ''; footerId = evenPageFooterId ?? ''; } diff --git a/packages/engine-render/src/components/docs/layout/tools.ts b/packages/engine-render/src/components/docs/layout/tools.ts index 3eb8e4cc4df..216ca89c05d 100644 --- a/packages/engine-render/src/components/docs/layout/tools.ts +++ b/packages/engine-render/src/components/docs/layout/tools.ts @@ -884,7 +884,7 @@ export function prepareSectionBreakConfig(ctx: ILayoutContext, nodeIndex: number firstPageHeaderId: global_firstPageHeaderId, firstPageFooterId: global_firstPageFooterId, useFirstPageHeaderFooter: global_useFirstPageHeaderFooter, - useEvenPageHeaderFooter: global_useEvenPageHeaderFooter, + evenAndOddHeaders: global_evenAndOddHeaders, marginTop: global_marginTop = 0, marginBottom: global_marginBottom = 0, @@ -924,7 +924,7 @@ export function prepareSectionBreakConfig(ctx: ILayoutContext, nodeIndex: number firstPageHeaderId = global_firstPageHeaderId, firstPageFooterId = global_firstPageFooterId, useFirstPageHeaderFooter = global_useFirstPageHeaderFooter, - useEvenPageHeaderFooter = global_useEvenPageHeaderFooter, + evenAndOddHeaders = global_evenAndOddHeaders, columnProperties = [], columnSeparatorType = ColumnSeparatorType.NONE, @@ -963,7 +963,7 @@ export function prepareSectionBreakConfig(ctx: ILayoutContext, nodeIndex: number footerIds, useFirstPageHeaderFooter, - useEvenPageHeaderFooter, + evenAndOddHeaders, columnProperties, columnSeparatorType, From c36f8ab42af1eb77720f3a9a74d01870f74d8072 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 24 Jun 2024 16:22:24 +0800 Subject: [PATCH 02/39] feat: draw margin identifier --- .../src/data/docs/default-document-data-cn.ts | 72 ++++++ .../src/basics/i-document-skeleton-cached.ts | 30 +-- .../engine-render/src/basics/interfaces.ts | 7 +- .../src/components/docs/doc-background.ts | 59 ++++- .../src/components/docs/document.ts | 212 +++++++++++++++++- .../src/components/docs/layout/model/page.ts | 72 +++--- packages/engine-render/src/shape/path.ts | 1 + 7 files changed, 368 insertions(+), 85 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index 715d216f041..eb5a0d9e484 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -18,6 +18,54 @@ import type { IDocumentData } from '@univerjs/core'; import { BooleanNumber, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType, WrapTextType } from '@univerjs/core'; import { ptToPixel } from '@univerjs/engine-render'; +function getDefaultHeaderFooterBody(type: 'header' | 'footer') { + return { + dataStream: type === 'header' ? '荷塘月色\r作者:朱自清 👨‍👩‍👧‍👦 Today 我是页眉页眉\r\n' : '荷塘𠮷\r作者:朱自清 👨‍👩‍👧‍👦 Today 我是页脚页脚\r\n', + textRuns: [ + { + st: 0, + ed: 4, + ts: { + fs: 12, + ff: 'Microsoft YaHei', + cl: { + rgb: 'rgb(0, 0, 0)', + }, + bl: BooleanNumber.TRUE, + ul: { + s: BooleanNumber.TRUE, + }, + }, + }, + { + st: 5, + ed: 36, + ts: { + fs: 12, + ff: 'Times New Roman', + cl: { + rgb: 'rgb(30, 30, 30)', + }, + bl: BooleanNumber.FALSE, + }, + }, + ], + paragraphs: [ + { + startIndex: 4, + }, + { + startIndex: 36, + }, + ], + sectionBreaks: [ + { + startIndex: 37, + }, + ], + }; +} + export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { id: 'd', drawings: { @@ -163,6 +211,26 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { 'shapeTest4', 'shapeTest5', ], + headers: { + defaultHeaderId: { + headerId: 'defaultHeaderId', + body: getDefaultHeaderFooterBody('header'), + }, + // evenHeaderId: { + // }, + // firstPageHeaderId: { + // } + }, + footers: { + defaultFooterId: { + footerId: 'defaultFooterId', + body: getDefaultHeaderFooterBody('footer'), + }, + // evenFooterId: { + // }, + // firstPageFooterId: { + // } + }, body: { dataStream: '荷塘月色\r\r作者:朱自清\r\r这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。\r\r沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁的。路图片一\b是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。\r\r路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常的自己,到了另一个世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话\b现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。\r\r曲曲折折的荷塘上面,弥望的是田田的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星\b又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉的流水,遮住了,不能见一些颜色;而叶子却更见风致了。\r\r月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样\b又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲上奏着的名曲。\r\r荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的\b是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。\r\r忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:\r\r于是妖童女,荡舟心许;鷁首徐回,兼传羽杯;櫂将移而藻挂,船欲动而萍开。尔其纤腰束素,迁延顾步;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾。\r\r可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。\r\r于是又记起,《西洲曲》里的句子:\r\r采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。\r\r今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。\r\r一九二七年七月,北京清华园。\r\r\r\r《荷塘月色》语言朴素典雅,准确生动,贮满诗意,满溢着朱自清的散文语言一贯有朴素的美,不用浓墨重彩,画的是淡墨水彩。\r\r朱自清先生一笔写景一笔说情,看起来松散不知所云,可仔细体会下,就能感受到先生在字里行间表述出的苦闷,而随之读者也被先生的文字所感染,被带进了他当时那苦闷而无法明喻的心情。这就是优异散文的必须品质之一。\r\r扩展资料:\r一首长诗《毁灭》奠定了朱自清在文坛新诗人的地位,而《桨声灯影里的秦淮河》则被公认为白话美文的典范。朱自清用白话美文向复古派宣战,有力地回击了复古派“白话不能作美文”之说,他是“五四”新文学运动的开拓者之一。\r\r朱自清的美文影响了一代又一代人。作家贾平凹说:来到扬州,第一个想到的人是朱自清,他是知识分子中最最了不起的人物。\r\r实际上,朱自清的写作路程是非常曲折的,他早期的时候大多数作品都是诗歌,但是他的诗歌和我国古代诗人的诗有很大区别,他的诗是用白话文写的,这其实也是他写作的惯用风格。\r\r后来,朱自清开始写一些关于社会的文章,因为那个时候社会比较混乱,这时候的作品大多抨击社会的黑暗面,文体风格大多硬朗,基调伉俪。到了后期,大多是写关于山水的文章,这类文章的写作格调大多以清丽雅致为主。\r\r朱自清的写作风格虽然在不同的时期随着他的人生阅历和社会形态的不同而发生着变化,但是他文章的主基调是没有变的,他这一生,所写的所有文章风格上都有一个非常显著的特点,那就是简约平淡,他不是类似古代花间词派的诗人们,不管是他的诗词还是他的文章从来都不用过于华丽的辞藻,他崇尚的是平淡。\r\r英国友人戴立克试过英译朱自清几篇散文,译完一读显得单薄,远远不如原文流利。他不服气,改用稍微古奥的英文重译,好多了:“那是说,朱先生外圆内方,文字尽管浅白,心思却很深沉,译笔只好朝深处经营。”朱自清的很多文章,譬如《背影》《祭亡妇》,读来自有一番只可意会不可言传的东西。\r\r平淡就是朱自清的写作风格。他不是豪放派的作家,他在创作的时候钟情于清新的风格,给人耳目一新的感觉。在他的文章中包含了他对生活的向往,由此可见他的写作风格和他待人处事的态度也是有几分相似的。他的文章非常优美,但又不会让人觉得狭隘,给人一种豁达渊博的感觉,这就是朱自清的写作风格,更是朱自清的为人品质。\r\r写有《荷塘月色》《背影》等名篇的著名散文家朱自清先生,不仅自己一生风骨正气,还用无形的家风涵养子孙。良好的家风家规意蕴深远,催人向善,是凝聚情感、涵养德行、砥砺成才的人生信条。“北有朱自清,南有朱物华,一文一武,一南一北,双星闪耀”,这是中国知识界、教育界对朱家两兄弟的赞誉。\r\r朱自清性格温和,为人和善,对待年轻人平易近人,是个平和的人。他取字“佩弦”,意思要像弓弦那样将自己绷紧,给人的感觉是自我要求高,偶尔有呆气。朱自清教学负责,对学生要求严格,修他的课的学生都受益不少。\r\r1948 年 6 月,患胃病多年的朱自清,在《抗议美国扶日政策并拒绝领取美援面粉宣言》上,一丝不苟地签下了自己的名字。随后,朱自清还将面粉配购证以及面粉票退了回去。1948 年 8 月 12 日,朱自清因不堪胃病折磨,离开人世。在新的时代即将到来时,朱自清却匆匆地离人们远去。他为人们留下了无数经典的诗歌和文字,还有永不屈服的精神。\r\r朱自清没有豪言壮语,他只是用坚定的行动、朴实的语言,向世人展示了中国知识分子在祖国危难之际坚定的革命性,体现了中国人的骨气,表现了无比高贵的民族气节,呈现了人生最有价值的一面,谱就了生命中最华丽的乐章。\r\r他以“自清”为名,自勉在困境中不丧志;他身患重病,至死拒领美援面粉,其气节令世人感佩;他的《背影》《荷塘月色》《匆匆》脍炙人口;他的文字追求“真”,没有半点矫饰,却蕴藏着动人心弦的力量。\r\r朱自清不但在文学创作方面有很高的造诣,也是一名革命民主主义战士,在反饥饿、反内战的斗争中,他始终保持着一个正直的爱国知识分子的气节和情操。毛泽东对朱自清宁肯饿死不领美国“救济粉”的精神给予称赞,赞扬他“表现了我们民族的英雄气概”。\r\n', @@ -773,5 +841,9 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { vertexAngle: 0, centerAngle: 0, }, + defaultHeaderId: 'defaultHeaderId', + defaultFooterId: 'defaultFooterId', + marginHeader: 30, + marginFooter: 20, }, }; diff --git a/packages/engine-render/src/basics/i-document-skeleton-cached.ts b/packages/engine-render/src/basics/i-document-skeleton-cached.ts index b857f38dde8..d0fcba0a545 100644 --- a/packages/engine-render/src/basics/i-document-skeleton-cached.ts +++ b/packages/engine-render/src/basics/i-document-skeleton-cached.ts @@ -36,8 +36,8 @@ export interface IDocumentSkeletonCached extends ISkeletonResourceReference { } export interface ISkeletonResourceReference { - skeHeaders: Map>; // id:{ width: IDocumentSkeletonFooter } - skeFooters: Map>; + skeHeaders: Map>; // id:{ width: IDocumentSkeletonHeaderFooter } + skeFooters: Map>; /* Global cache, does not participate in rendering, only helps skeleton generation */ skeListLevel?: Map; // 有序列表缓存,id:{ level: max(width)的bullet } drawingAnchor?: Map; // Anchor point to assist floating element positioning @@ -49,22 +49,14 @@ export interface IDocumentSkeletonDrawingAnchor { top: number; // relative height for previous block } -export interface IDocumentSkeletonHeaderFooterBase { - lines: IDocumentSkeletonLine[]; - skeDrawings: Map; - height: number; // footer或header的总长度 - st: number; // startIndex 文本开始索引 - ed: number; // endIndex 文本结束索引 - marginLeft: number; -} - -export interface IDocumentSkeletonHeader extends IDocumentSkeletonHeaderFooterBase { - marginTop: number; -} - -export interface IDocumentSkeletonFooter extends IDocumentSkeletonHeaderFooterBase { - marginBottom: number; -} +// export interface IDocumentSkeletonHeaderFooterBase { +// lines: IDocumentSkeletonLine[]; +// skeDrawings: Map; +// height: number; // footer或header的总长度 +// st: number; // startIndex 文本开始索引 +// ed: number; // endIndex 文本结束索引 +// marginLeft: number; +// } export interface IDocumentSkeletonPage { sections: IDocumentSkeletonSection[]; @@ -94,6 +86,8 @@ export interface IDocumentSkeletonPage { parent?: IDocumentSkeletonCached; } +export interface IDocumentSkeletonHeaderFooter extends IDocumentSkeletonPage {} + export interface IDocumentSkeletonSection { columns: IDocumentSkeletonColumn[]; colCount: number; // column Count 列的数量 diff --git a/packages/engine-render/src/basics/interfaces.ts b/packages/engine-render/src/basics/interfaces.ts index 1293f5304e5..76eac8acbc8 100644 --- a/packages/engine-render/src/basics/interfaces.ts +++ b/packages/engine-render/src/basics/interfaces.ts @@ -36,9 +36,8 @@ import type { IDocumentSkeletonDrawing, IDocumentSkeletonDrawingAnchor, IDocumentSkeletonFontStyle, - IDocumentSkeletonFooter, IDocumentSkeletonGlyph, - IDocumentSkeletonHeader, + IDocumentSkeletonHeaderFooter, } from './i-document-skeleton-cached'; import type { Vector2 } from './vector2'; import type { ITransformerConfig } from './transformer-config'; @@ -115,8 +114,8 @@ export interface IParagraphConfig { // pageContentWidth: number; // pageContentHeight: number; paragraphStyle?: IParagraphStyle; - skeHeaders: Map>; - skeFooters: Map>; + skeHeaders: Map>; + skeFooters: Map>; drawingAnchor?: Map; // sectionBreakConfig: ISectionBreakConfig; } diff --git a/packages/engine-render/src/components/docs/doc-background.ts b/packages/engine-render/src/components/docs/doc-background.ts index a39df36c831..c2d3116d4bc 100644 --- a/packages/engine-render/src/components/docs/doc-background.ts +++ b/packages/engine-render/src/components/docs/doc-background.ts @@ -16,7 +16,8 @@ import type { IViewportInfo } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; -import { Rect } from '../../shape'; +import type { IPathProps } from '../../shape'; +import { Path, Rect } from '../../shape'; import { Liquid } from './liquid'; import type { IDocumentsConfig } from './doc-component'; import { DocComponent } from './doc-component'; @@ -24,6 +25,7 @@ import type { DocumentSkeleton } from './layout/doc-skeleton'; const PAGE_STROKE_COLOR = 'rgba(198, 198, 198, 1)'; const PAGE_FILL_COLOR = 'rgba(255, 255, 255, 1)'; +const MARGIN_STROKE_COLOR = 'rgba(158, 158, 158, 1)'; export class DocBackground extends DocComponent { private _drawLiquid: Liquid; @@ -67,15 +69,16 @@ export class DocBackground extends DocComponent { ); pageLeft += x; pageTop += y; + continue; } - // Draw background. - const { width, pageWidth, height, pageHeight } = page; + // Draw background and margin identifier. + const { width, pageWidth, height, pageHeight, marginTop, marginBottom, marginLeft, marginRight } = page; ctx.save(); ctx.translate(pageLeft - 0.5, pageTop - 0.5); - const options = { + const backgroundOptions = { width: pageWidth ?? width, height: pageHeight ?? height, strokeWidth: 1, @@ -83,7 +86,52 @@ export class DocBackground extends DocComponent { fill: PAGE_FILL_COLOR, zIndex: 3, }; - Rect.drawWith(ctx, options); + + const IDENTIFIER_WIDTH = 15; + const marginIdentification: IPathProps = { + dataArray: [{ + command: 'M', + points: [marginLeft - IDENTIFIER_WIDTH, marginTop], + }, { + command: 'L', + points: [marginLeft, marginTop], + }, { + command: 'L', + points: [marginLeft, marginTop - IDENTIFIER_WIDTH], + }, { + command: 'M', + points: [pageWidth - marginRight + IDENTIFIER_WIDTH, marginTop], + }, { + command: 'L', + points: [pageWidth - marginRight, marginTop], + }, { + command: 'L', + points: [pageWidth - marginRight, marginTop - IDENTIFIER_WIDTH], + }, { + command: 'M', + points: [marginLeft - IDENTIFIER_WIDTH, pageHeight - marginBottom], + }, { + command: 'L', + points: [marginLeft, pageHeight - marginBottom], + }, { + command: 'L', + points: [marginLeft, pageHeight - marginBottom + IDENTIFIER_WIDTH], + }, { + command: 'M', + points: [pageWidth - marginRight + IDENTIFIER_WIDTH, pageHeight - marginBottom], + }, { + command: 'L', + points: [pageWidth - marginRight, pageHeight - marginBottom], + }, { + command: 'L', + points: [pageWidth - marginRight, pageHeight - marginBottom + IDENTIFIER_WIDTH], + }] as unknown as IPathProps['dataArray'], + strokeWidth: 1.5, + stroke: MARGIN_STROKE_COLOR, + }; + + Rect.drawWith(ctx, backgroundOptions); + Path.drawWith(ctx, marginIdentification); ctx.restore(); const { x, y } = this._drawLiquid.translatePage( @@ -92,6 +140,7 @@ export class DocBackground extends DocComponent { this.pageMarginLeft, this.pageMarginTop ); + pageLeft += x; pageTop += y; } diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index 7e311229a38..f75d057eaa7 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -16,22 +16,23 @@ import './extensions'; -import type { Nullable } from '@univerjs/core'; import { CellValueType, HorizontalAlign, VerticalAlign, WrapStrategy } from '@univerjs/core'; +import type { IDocumentRenderConfig, Nullable } from '@univerjs/core'; import { Subject } from 'rxjs'; import { calculateRectRotate, getRotateOffsetAndFarthestHypotenuse } from '../../basics/draw'; -import type { IDocumentSkeletonPage } from '../../basics/i-document-skeleton-cached'; +import type { IDocumentSkeletonGlyph, IDocumentSkeletonLine, IDocumentSkeletonPage } from '../../basics/i-document-skeleton-cached'; import { LineType } from '../../basics/i-document-skeleton-cached'; import { degToRad } from '../../basics/tools'; import type { Transform } from '../../basics/transform'; -import type { IViewportInfo } from '../../basics/vector2'; +import type { IBoundRectNoAngle, IViewportInfo } from '../../basics/vector2'; import { Vector2 } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import type { Scene } from '../../scene'; -import type { IExtensionConfig } from '../extension'; +import type { ComponentExtension, IExtensionConfig } from '../extension'; import { DocumentsSpanAndLineExtensionRegistry } from '../extension'; import { VERTICAL_ROTATE_ANGLE } from '../../basics/text-rotation'; +import type { IScale } from '../../../../core/lib/types'; import { Liquid } from './liquid'; import type { IDocumentsConfig, IPageMarginLayout } from './doc-component'; import { DocComponent } from './doc-component'; @@ -104,6 +105,16 @@ export class Documents extends DocComponent { return (this.getScene() as Scene).getEngine(); } + changeSkeleton(newSkeleton: DocumentSkeleton) { + this.setSkeleton(newSkeleton); + + return this; + } + + protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { + this.draw(ctx, bounds); + } + override draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { const skeletonData = this.getSkeleton()?.getSkeletonData(); @@ -113,7 +124,7 @@ export class Documents extends DocComponent { this._drawLiquid.reset(); - const { pages } = skeletonData; + const { pages, skeHeaders, skeFooters } = skeletonData; const parentScale = this.getParentScale(); // const scale = getScale(parentScale); const extensions = this.getExtensionsByOrder(); @@ -123,7 +134,8 @@ export class Documents extends DocComponent { } const backgroundExtension = extensions.find((e) => e.uKey === 'DefaultDocsBackgroundExtension'); - const glyphExtensionsExcludeBackground = extensions.filter((e) => e.type === DOCS_EXTENSION_TYPE.SPAN && e.uKey !== 'DefaultDocsBackgroundExtension'); + const glyphExtensionsExcludeBackground = extensions + .filter((e) => e.type === DOCS_EXTENSION_TYPE.SPAN && e.uKey !== 'DefaultDocsBackgroundExtension'); // broadcasting the pageTop and pageLeft for each page in the document with multiple pages. let pageTop = 0; @@ -139,6 +151,9 @@ export class Documents extends DocComponent { marginRight: pagePaddingRight = 0, width: actualWidth, height: actualHeight, + pageWidth, + headerId, + footerId, renderConfig = {}, } = page; const { @@ -185,9 +200,32 @@ export class Documents extends DocComponent { ); pageLeft += x; pageTop += y; + continue; } + const headerSkeletonPage = skeHeaders.get(headerId)?.get(pageWidth); + + const headerAlignOffsetNoAngle = Vector2.create( + horizontalOffsetNoAngle, + headerSkeletonPage?.marginTop ?? 0 + ); + + if (headerSkeletonPage) { + this._drawHeaderFooter( + headerSkeletonPage, + ctx, + extensions, + backgroundExtension, + glyphExtensionsExcludeBackground, + headerAlignOffsetNoAngle, + centerAngle, + vertexAngle, + renderConfig, + parentScale + ); + } + this._pageRender$.next({ page, pageLeft, @@ -405,14 +443,164 @@ export class Documents extends DocComponent { } } - changeSkeleton(newSkeleton: DocumentSkeleton) { - this.setSkeleton(newSkeleton); + private _drawHeaderFooter( + page: IDocumentSkeletonPage, + ctx: UniverRenderingContext, + extensions: ComponentExtension[], + backgroundExtension: Nullable>, + glyphExtensionsExcludeBackground: ComponentExtension[], + alignOffsetNoAngle: Vector2, + centerAngle: number, + vertexAngle: number, + renderConfig: IDocumentRenderConfig, + parentScale: IScale + ) { + if (this._drawLiquid == null) { + return; + } + const { sections } = page; - return this; - } + for (const section of sections) { + const { columns } = section; - protected override _draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { - this.draw(ctx, bounds); + this._drawLiquid.translateSave(); + this._drawLiquid.translateSection(section); + + for (const column of columns) { + const { lines } = column; + + this._drawLiquid.translateSave(); + this._drawLiquid.translateColumn(column); + + const linesCount = lines.length; + + const alignOffset = alignOffsetNoAngle; + + for (let i = 0; i < linesCount; i++) { + const line = lines[i]; + const { divides, asc = 0, type, lineHeight = 0 } = line; + + const maxLineAsc = asc; + + const maxLineAscSin = maxLineAsc * Math.sin(centerAngle); + const maxLineAscCos = maxLineAsc * Math.cos(centerAngle); + + if (type === LineType.BLOCK) { + for (const extension of extensions) { + if (extension.type === DOCS_EXTENSION_TYPE.LINE) { + extension.extensionOffset = { + alignOffset, + renderConfig, + }; + extension.draw(ctx, parentScale, line); + } + } + } else { + this._drawLiquid.translateSave(); + + this._drawLiquid.translateLine(line, true); + + const divideLength = divides.length; + + for (let i = 0; i < divideLength; i++) { + const divide = divides[i]; + const { glyphGroup } = divide; + + this._drawLiquid.translateSave(); + this._drawLiquid.translateDivide(divide); + + // Draw text background. + for (const glyph of glyphGroup) { + if (!glyph.content || glyph.content.length === 0) { + continue; + } + + const { width: spanWidth, left: spanLeft } = glyph; + + const { x: translateX, y: translateY } = this._drawLiquid; + + const originTranslate = Vector2.create(translateX, translateY); + + const centerPoint = Vector2.create(spanWidth / 2, lineHeight / 2); + + const spanStartPoint = calculateRectRotate( + originTranslate.addByPoint(spanLeft, 0), + centerPoint, + centerAngle, + vertexAngle, + alignOffset + ); + + const extensionOffset: IExtensionConfig = { + spanStartPoint, + }; + + if (backgroundExtension) { + backgroundExtension.extensionOffset = extensionOffset; + backgroundExtension.draw(ctx, parentScale, glyph); + } + } + + // Draw text\border\lines etc. + for (const glyph of glyphGroup) { + if (!glyph.content || glyph.content.length === 0) { + continue; + } + + const { width: spanWidth, left: spanLeft, xOffset } = glyph; + + const { x: translateX, y: translateY } = this._drawLiquid; + + const originTranslate = Vector2.create(translateX, translateY); + + const centerPoint = Vector2.create(spanWidth / 2, lineHeight / 2); + + const spanStartPoint = calculateRectRotate( + originTranslate.addByPoint(spanLeft + xOffset, 0), + centerPoint, + centerAngle, + vertexAngle, + alignOffset + ); + + const spanPointWithFont = calculateRectRotate( + originTranslate.addByPoint( + spanLeft + maxLineAscSin + xOffset, + maxLineAscCos + ), + centerPoint, + centerAngle, + vertexAngle, + alignOffset + ); + + const extensionOffset: IExtensionConfig = { + originTranslate, + spanStartPoint, + spanPointWithFont, + centerPoint, + alignOffset, + renderConfig, + }; + + for (const extension of glyphExtensionsExcludeBackground) { + extension.extensionOffset = extensionOffset; + extension.draw(ctx, parentScale, glyph); + } + } + + this._drawLiquid.translateRestore(); + } + + this._drawLiquid.translateRestore(); + } + } + + this._drawLiquid.translateRestore(); + } + + this._drawLiquid.translateRestore(); + } } private _horizontalHandler( diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index 06f87278467..40fbf69edbe 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -17,8 +17,7 @@ import type { Nullable } from '@univerjs/core'; import { PageOrientType } from '@univerjs/core'; import type { - IDocumentSkeletonFooter, - IDocumentSkeletonHeader, + IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage, ISkeletonResourceReference, } from '../../../../basics/i-document-skeleton-cached'; @@ -32,7 +31,6 @@ import { createSkeletonSection } from './section'; // 新增数据结构框架 // 判断奇数和偶数页码 - export function createSkeletonPage( ctx: ILayoutContext, sectionBreakConfig: ISectionBreakConfig, @@ -93,18 +91,18 @@ export function createSkeletonPage( footerId = evenPageFooterId ?? ''; } - let header: Nullable; - let footer: Nullable; + let header: Nullable; + let footer: Nullable; if (headerId) { if (skeHeaders.get(headerId)?.has(pageWidth)) { header = skeHeaders.get(headerId)?.get(pageWidth); } else if (headerTreeMap && headerTreeMap.has(headerId)) { - header = _createSkeletonHeader( + header = _createSkeletonHeaderFooter( ctx, headerTreeMap.get(headerId)!, sectionBreakConfig, skeletonResourceReference - ) as IDocumentSkeletonHeader; + ); skeHeaders.set(headerId, new Map([[pageWidth, header]])); } page.headerId = headerId; @@ -114,13 +112,13 @@ export function createSkeletonPage( if (skeFooters.get(footerId)?.has(pageWidth)) { footer = skeFooters.get(footerId)?.get(pageWidth); } else if (footerTreeMap && footerTreeMap.has(footerId)) { - footer = _createSkeletonHeader( + footer = _createSkeletonHeaderFooter( ctx, footerTreeMap.get(footerId)!, sectionBreakConfig, skeletonResourceReference - ) as IDocumentSkeletonFooter; - skeFooters.set(headerId, new Map([[pageWidth, footer]])); + ); + skeFooters.set(footerId, new Map([[pageWidth, footer]])); } page.footerId = footerId; } @@ -178,29 +176,20 @@ function _getNullPage() { }; } -function _createSkeletonHeader( +function _createSkeletonHeaderFooter( ctx: ILayoutContext, headerOrFooter: DocumentViewModel, sectionBreakConfig: ISectionBreakConfig, skeletonResourceReference: ISkeletonResourceReference, isHeader = true -): IDocumentSkeletonHeader | IDocumentSkeletonFooter { +): IDocumentSkeletonHeaderFooter { const { - lists, - footerTreeMap, - headerTreeMap, - localeService, - pageSize, - marginLeft = 0, - marginRight = 0, - drawings, - marginTop = 0, - marginBottom = 0, - marginHeader = 0, - marginFooter = 0, + lists, footerTreeMap, headerTreeMap, localeService, pageSize, drawings, + marginLeft = 0, marginRight = 0, marginTop = 0, marginBottom = 0, + marginHeader = 0, marginFooter = 0, } = sectionBreakConfig; const pageWidth = pageSize?.width || Number.POSITIVE_INFINITY; - const headerConfig: ISectionBreakConfig = { + const headerFooterConfig: ISectionBreakConfig = { lists, footerTreeMap, headerTreeMap, @@ -212,50 +201,41 @@ function _createSkeletonHeader( drawings, }; - const areaPage = createSkeletonPage(ctx, headerConfig, skeletonResourceReference); + const areaPage = createSkeletonPage(ctx, headerFooterConfig, skeletonResourceReference); const page = dealWithSection( ctx, headerOrFooter, headerOrFooter.children[0], areaPage, - headerConfig + headerFooterConfig ).pages[0]; updateBlockIndex([page]); - const column = page.sections[0].columns[0]; - const height = column.height || 0; - const { skeDrawings, st, ed } = page; - - const headerOrFooterSke = { - lines: column.lines, - skeDrawings, - height, - st, - ed, - marginLeft, - marginRight, - }; if (isHeader) { return { - ...headerOrFooterSke, - marginTop: __getHeaderMarginTop(marginTop, marginHeader, height), + ...page, + marginTop: marginHeader, + marginBottom: 0, }; } + return { - ...headerOrFooterSke, - marginBottom: __getHeaderMarginBottom(marginBottom, marginFooter, height), + ...page, + marginBottom: marginFooter, + marginTop: 0, }; } function _getVerticalMargin( marginTB: number, marginHF: number, - headerOrFooter: Nullable | Nullable + headerOrFooter: Nullable ) { - if (!headerOrFooter || headerOrFooter.lines.length === 0) { + if (!headerOrFooter || headerOrFooter.sections[0].columns[0].lines.length === 0) { return marginTB; } + return Math.max(marginTB, marginHF, headerOrFooter?.height || 0); } diff --git a/packages/engine-render/src/shape/path.ts b/packages/engine-render/src/shape/path.ts index f2fd5343415..30b743f605d 100644 --- a/packages/engine-render/src/shape/path.ts +++ b/packages/engine-render/src/shape/path.ts @@ -337,6 +337,7 @@ export class Path extends Shape { * rendering */ + // eslint-disable-next-line max-lines-per-function, complexity static parsePathData(data: string) { // Path Data Segment must begin with a moveTo //m (x y)+ Relative moveTo (subsequent points are treated as lineTo) From cc734d39574484e1c6aae6256e53da37ffa6fd92 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 24 Jun 2024 17:10:52 +0800 Subject: [PATCH 03/39] fix: render footer --- .../src/data/docs/default-document-data-cn.ts | 22 ++++++++++------ .../src/basics/i-document-skeleton-cached.ts | 2 ++ .../src/components/docs/doc-background.ts | 26 +++++++++---------- .../src/components/docs/document.ts | 22 ++++++++++++++++ .../src/components/docs/layout/model/page.ts | 9 +++++-- 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index eb5a0d9e484..2f507291a47 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -20,16 +20,16 @@ import { ptToPixel } from '@univerjs/engine-render'; function getDefaultHeaderFooterBody(type: 'header' | 'footer') { return { - dataStream: type === 'header' ? '荷塘月色\r作者:朱自清 👨‍👩‍👧‍👦 Today 我是页眉页眉\r\n' : '荷塘𠮷\r作者:朱自清 👨‍👩‍👧‍👦 Today 我是页脚页脚\r\n', + dataStream: type === 'header' ? '荷塘月色\r作者:朱自清\rToday Office\r我是页眉页眉\r\n' : '荷塘月色\r作者:朱自清\rToday Office\r我是页脚页脚\r\n', textRuns: [ { st: 0, ed: 4, ts: { - fs: 12, + fs: 10, ff: 'Microsoft YaHei', cl: { - rgb: 'rgb(0, 0, 0)', + rgb: 'rgb(155, 155, 0)', }, bl: BooleanNumber.TRUE, ul: { @@ -39,9 +39,9 @@ function getDefaultHeaderFooterBody(type: 'header' | 'footer') { }, { st: 5, - ed: 36, + ed: 31, ts: { - fs: 12, + fs: 10, ff: 'Times New Roman', cl: { rgb: 'rgb(30, 30, 30)', @@ -55,12 +55,18 @@ function getDefaultHeaderFooterBody(type: 'header' | 'footer') { startIndex: 4, }, { - startIndex: 36, + startIndex: 11, + }, + { + startIndex: 24, + }, + { + startIndex: 31, }, ], sectionBreaks: [ { - startIndex: 37, + startIndex: 32, }, ], }; @@ -844,6 +850,6 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { defaultHeaderId: 'defaultHeaderId', defaultFooterId: 'defaultFooterId', marginHeader: 30, - marginFooter: 20, + marginFooter: 30, }, }; diff --git a/packages/engine-render/src/basics/i-document-skeleton-cached.ts b/packages/engine-render/src/basics/i-document-skeleton-cached.ts index d0fcba0a545..bd1f4a3af79 100644 --- a/packages/engine-render/src/basics/i-document-skeleton-cached.ts +++ b/packages/engine-render/src/basics/i-document-skeleton-cached.ts @@ -68,7 +68,9 @@ export interface IDocumentSkeletonPage { pageOrient: PageOrientType; // Paper orientation, whether it's portrait (vertical) or landscape (horizontal) marginLeft: number; // The current page's padding, used to accommodate column title space, follows the snapshot configuration, jointly determined by documentStyle and sectionBreak. It represents the static limit for each page, which may vary per page. marginRight: number; + originMarginTop: number; // The margin top in document style config, used to draw margin identifier. marginTop: number; + originMarginBottom: number; // The margin bottom in document style config, used to draw margin identifier. marginBottom: number; pageNumber: number; // page页数 diff --git a/packages/engine-render/src/components/docs/doc-background.ts b/packages/engine-render/src/components/docs/doc-background.ts index c2d3116d4bc..b2f8d91d182 100644 --- a/packages/engine-render/src/components/docs/doc-background.ts +++ b/packages/engine-render/src/components/docs/doc-background.ts @@ -74,7 +74,7 @@ export class DocBackground extends DocComponent { } // Draw background and margin identifier. - const { width, pageWidth, height, pageHeight, marginTop, marginBottom, marginLeft, marginRight } = page; + const { width, pageWidth, height, pageHeight, originMarginTop, originMarginBottom, marginLeft, marginRight } = page; ctx.save(); ctx.translate(pageLeft - 0.5, pageTop - 0.5); @@ -91,40 +91,40 @@ export class DocBackground extends DocComponent { const marginIdentification: IPathProps = { dataArray: [{ command: 'M', - points: [marginLeft - IDENTIFIER_WIDTH, marginTop], + points: [marginLeft - IDENTIFIER_WIDTH, originMarginTop], }, { command: 'L', - points: [marginLeft, marginTop], + points: [marginLeft, originMarginTop], }, { command: 'L', - points: [marginLeft, marginTop - IDENTIFIER_WIDTH], + points: [marginLeft, originMarginTop - IDENTIFIER_WIDTH], }, { command: 'M', - points: [pageWidth - marginRight + IDENTIFIER_WIDTH, marginTop], + points: [pageWidth - marginRight + IDENTIFIER_WIDTH, originMarginTop], }, { command: 'L', - points: [pageWidth - marginRight, marginTop], + points: [pageWidth - marginRight, originMarginTop], }, { command: 'L', - points: [pageWidth - marginRight, marginTop - IDENTIFIER_WIDTH], + points: [pageWidth - marginRight, originMarginTop - IDENTIFIER_WIDTH], }, { command: 'M', - points: [marginLeft - IDENTIFIER_WIDTH, pageHeight - marginBottom], + points: [marginLeft - IDENTIFIER_WIDTH, pageHeight - originMarginBottom], }, { command: 'L', - points: [marginLeft, pageHeight - marginBottom], + points: [marginLeft, pageHeight - originMarginBottom], }, { command: 'L', - points: [marginLeft, pageHeight - marginBottom + IDENTIFIER_WIDTH], + points: [marginLeft, pageHeight - originMarginBottom + IDENTIFIER_WIDTH], }, { command: 'M', - points: [pageWidth - marginRight + IDENTIFIER_WIDTH, pageHeight - marginBottom], + points: [pageWidth - marginRight + IDENTIFIER_WIDTH, pageHeight - originMarginBottom], }, { command: 'L', - points: [pageWidth - marginRight, pageHeight - marginBottom], + points: [pageWidth - marginRight, pageHeight - originMarginBottom], }, { command: 'L', - points: [pageWidth - marginRight, pageHeight - marginBottom + IDENTIFIER_WIDTH], + points: [pageWidth - marginRight, pageHeight - originMarginBottom + IDENTIFIER_WIDTH], }] as unknown as IPathProps['dataArray'], strokeWidth: 1.5, stroke: MARGIN_STROKE_COLOR, diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index f75d057eaa7..838c402c8d4 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -432,6 +432,28 @@ export class Documents extends DocComponent { this._resetRotation(ctx, finalAngle); + const footerSkeletonPage = skeFooters.get(footerId)?.get(pageWidth); + + if (footerSkeletonPage) { + const footerAlignOffsetNoAngle = Vector2.create( + horizontalOffsetNoAngle, + page.pageHeight - footerSkeletonPage?.height - footerSkeletonPage.marginBottom + ); + + this._drawHeaderFooter( + footerSkeletonPage, + ctx, + extensions, + backgroundExtension, + glyphExtensionsExcludeBackground, + footerAlignOffsetNoAngle, + centerAngle, + vertexAngle, + renderConfig, + parentScale + ); + } + const { x, y } = this._drawLiquid.translatePage( page, this.pageLayoutType, diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index 40fbf69edbe..44f1fc5b6c7 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -116,13 +116,16 @@ export function createSkeletonPage( ctx, footerTreeMap.get(footerId)!, sectionBreakConfig, - skeletonResourceReference + skeletonResourceReference, + false ); skeFooters.set(footerId, new Map([[pageWidth, footer]])); } page.footerId = footerId; } + page.originMarginTop = marginTop; + page.originMarginBottom = marginBottom; page.marginTop = _getVerticalMargin(marginTop, marginHeader, header); page.marginBottom = _getVerticalMargin(marginBottom, marginFooter, footer); @@ -167,7 +170,9 @@ function _getNullPage() { height: 0, marginLeft: 0, marginRight: 0, + originMarginTop: 0, marginTop: 0, + originMarginBottom: 0, marginBottom: 0, breakType: BreakType.SECTION, st: 0, @@ -236,7 +241,7 @@ function _getVerticalMargin( return marginTB; } - return Math.max(marginTB, marginHF, headerOrFooter?.height || 0); + return Math.max(marginTB, (marginHF + headerOrFooter?.height || 0)); } function __getHeaderMarginTop(marginTop: number, marginHeader: number, height: number) { From 740c8562bb6010c7844251e2f1f983c8cfdac220 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 24 Jun 2024 20:13:13 +0800 Subject: [PATCH 04/39] fix: header and footer render --- .../src/data/docs/default-document-data-cn.ts | 2 +- .../src/components/docs/document.ts | 4 ++++ .../layout/block/paragraph/language-ruler.ts | 16 ++++++++-------- .../src/components/docs/layout/model/page.ts | 8 ++++---- .../src/components/docs/layout/tools.ts | 7 ++++--- .../docs/view-model/document-view-model.ts | 14 +++++++++++++- 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index 2f507291a47..ca6db3bf6a9 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -20,7 +20,7 @@ import { ptToPixel } from '@univerjs/engine-render'; function getDefaultHeaderFooterBody(type: 'header' | 'footer') { return { - dataStream: type === 'header' ? '荷塘月色\r作者:朱自清\rToday Office\r我是页眉页眉\r\n' : '荷塘月色\r作者:朱自清\rToday Office\r我是页脚页脚\r\n', + dataStream: type === 'header' ? '苍茫夜色\r作者:朱自清\rToday Office\r我是页眉页眉\r\n' : '苍茫月色\r作者:朱自清\rToday Office\r我是页脚页脚\r\n', textRuns: [ { st: 0, diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index 838c402c8d4..3ecd9664717 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -235,6 +235,9 @@ export class Documents extends DocComponent { this._startRotation(ctx, finalAngle); + // ctx.save(); + // ctx.globalAlpha = 0.5; + for (const section of sections) { const { columns } = section; @@ -429,6 +432,7 @@ export class Documents extends DocComponent { this._drawLiquid.translateRestore(); } } + // ctx.restore(); this._resetRotation(ctx, finalAngle); diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/language-ruler.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/language-ruler.ts index 02005fc9427..9e7e26b358c 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/language-ruler.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/language-ruler.ts @@ -28,7 +28,7 @@ import type { DocumentViewModel } from '../../../view-model/document-view-model' export function otherHandler( index: number, charArray: string, - bodyModel: DocumentViewModel, + viewModel: DocumentViewModel, paragraphNode: DataStreamTreeNode, sectionBreakConfig: ISectionBreakConfig, paragraphStyle: IParagraphStyle @@ -43,7 +43,7 @@ export function otherHandler( break; } - const config = getFontCreateConfig(index + i, bodyModel, paragraphNode, sectionBreakConfig, paragraphStyle); + const config = getFontCreateConfig(index + i, viewModel, paragraphNode, sectionBreakConfig, paragraphStyle); const glyph = createSkeletonLetterGlyph(newChar, config); glyphGroup.push(glyph); @@ -59,13 +59,13 @@ export function otherHandler( export function ArabicHandler( index: number, charArray: string, - bodyModel: DocumentViewModel, + viewModel: DocumentViewModel, paragraphNode: DataStreamTreeNode, sectionBreakConfig: ISectionBreakConfig, paragraphStyle: IParagraphStyle ) { // 组合阿拉伯语的词组 - const config = getFontCreateConfig(index, bodyModel, paragraphNode, sectionBreakConfig, paragraphStyle); + const config = getFontCreateConfig(index, viewModel, paragraphNode, sectionBreakConfig, paragraphStyle); const glyph = []; let step = 0; @@ -88,12 +88,12 @@ export function ArabicHandler( export function emojiHandler( index: number, charArray: string, - bodyModel: DocumentViewModel, + viewModel: DocumentViewModel, paragraphNode: DataStreamTreeNode, sectionBreakConfig: ISectionBreakConfig, paragraphStyle: IParagraphStyle ) { - const config = getFontCreateConfig(index, bodyModel, paragraphNode, sectionBreakConfig, paragraphStyle); + const config = getFontCreateConfig(index, viewModel, paragraphNode, sectionBreakConfig, paragraphStyle); const match = charArray.match(EMOJI_REG); return { @@ -105,13 +105,13 @@ export function emojiHandler( export function TibetanHandler( index: number, charArray: string, - bodyModel: DocumentViewModel, + viewModel: DocumentViewModel, paragraphNode: DataStreamTreeNode, sectionBreakConfig: ISectionBreakConfig, paragraphStyle: IParagraphStyle ) { // 组合藏语词组 - const config = getFontCreateConfig(index, bodyModel, paragraphNode, sectionBreakConfig, paragraphStyle); + const config = getFontCreateConfig(index, viewModel, paragraphNode, sectionBreakConfig, paragraphStyle); const glyph = []; let step = 0; for (let i = 0; i < charArray.length; i++) { diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index 44f1fc5b6c7..43a305620f1 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -183,14 +183,14 @@ function _getNullPage() { function _createSkeletonHeaderFooter( ctx: ILayoutContext, - headerOrFooter: DocumentViewModel, + headerOrFooterViewModel: DocumentViewModel, sectionBreakConfig: ISectionBreakConfig, skeletonResourceReference: ISkeletonResourceReference, isHeader = true ): IDocumentSkeletonHeaderFooter { const { lists, footerTreeMap, headerTreeMap, localeService, pageSize, drawings, - marginLeft = 0, marginRight = 0, marginTop = 0, marginBottom = 0, + marginLeft = 0, marginRight = 0, marginHeader = 0, marginFooter = 0, } = sectionBreakConfig; const pageWidth = pageSize?.width || Number.POSITIVE_INFINITY; @@ -209,8 +209,8 @@ function _createSkeletonHeaderFooter( const areaPage = createSkeletonPage(ctx, headerFooterConfig, skeletonResourceReference); const page = dealWithSection( ctx, - headerOrFooter, - headerOrFooter.children[0], + headerOrFooterViewModel, + headerOrFooterViewModel.children[0], areaPage, headerFooterConfig ).pages[0]; diff --git a/packages/engine-render/src/components/docs/layout/tools.ts b/packages/engine-render/src/components/docs/layout/tools.ts index 216ca89c05d..3b5c04088f4 100644 --- a/packages/engine-render/src/components/docs/layout/tools.ts +++ b/packages/engine-render/src/components/docs/layout/tools.ts @@ -737,7 +737,7 @@ export function getFontConfigFromLastGlyph( export function getFontCreateConfig( index: number, - bodyModel: DocumentViewModel, + viewModel: DocumentViewModel, paragraphNode: DataStreamTreeNode, sectionBreakConfig: ISectionBreakConfig, paragraphStyle: IParagraphStyle @@ -758,10 +758,11 @@ export function getFontCreateConfig( } = sectionBreakConfig; const { isRenderStyle } = renderConfig; const { startIndex } = paragraphNode; + const textRun = isRenderStyle === BooleanNumber.FALSE ? { ts: {}, st: 0, ed: 0 } - : bodyModel.getTextRun(index + startIndex) || { ts: {}, st: 0, ed: 0 }; - const customDecoration = bodyModel.getCustomDecoration(index + startIndex); + : viewModel.getTextRun(index + startIndex) || { ts: {}, st: 0, ed: 0 }; + const customDecoration = viewModel.getCustomDecoration(index + startIndex); const showCustomDecoration = customDecoration && (customDecoration.show !== false); const customDecorationStyle = showCustomDecoration ? getCustomDecorationStyle(customDecoration) : null; diff --git a/packages/engine-render/src/components/docs/view-model/document-view-model.ts b/packages/engine-render/src/components/docs/view-model/document-view-model.ts index e6b30538244..3438b20f7a7 100644 --- a/packages/engine-render/src/components/docs/view-model/document-view-model.ts +++ b/packages/engine-render/src/components/docs/view-model/document-view-model.ts @@ -232,6 +232,18 @@ export class DocumentViewModel implements IDisposable { this._customBlockCurrentIndex = 0; this._tableBlockCurrentIndex = 0; this._customRangeCurrentIndex = 0; + + if (this.headerTreeMap.size > 0) { + for (const header of this.headerTreeMap.values()) { + header.resetCache(); + } + } + + if (this.footerTreeMap.size > 0) { + for (const footer of this.footerTreeMap.values()) { + footer.resetCache(); + } + } } getSectionBreak(index: number) { @@ -323,7 +335,7 @@ export class DocumentViewModel implements IDisposable { * textRun matches according to the selection. If the text length is 10, then the range of textRun is from 0 to 11. */ getTextRun(index: number) { - const textRuns = this.getBody()!.textRuns; + const textRuns = this.getBody()?.textRuns; if (textRuns == null) { return; } From 581e410b597ffe4273b8f619a993385e33a48adb Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 24 Jun 2024 21:27:15 +0800 Subject: [PATCH 05/39] feat: change edit area --- .../doc-header-footer.controller.ts | 120 ++++++++++++++++++ packages/docs-ui/src/docs-ui-plugin.ts | 2 + .../src/components/docs/document.ts | 24 +++- .../components/docs/layout/doc-skeleton.ts | 54 ++++++++ .../docs/view-model/document-view-model.ts | 21 ++- packages/engine-render/src/index.ts | 1 + 6 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 packages/docs-ui/src/controllers/doc-header-footer.controller.ts diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts new file mode 100644 index 00000000000..25c03478f56 --- /dev/null +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentDataModel } from '@univerjs/core'; +import { Disposable, ICommandService, IUniverInstanceService } from '@univerjs/core'; +import type { IMouseEvent, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; +import { PageLayoutType, Vector2 } from '@univerjs/engine-render'; +import { Inject } from '@wendellhu/redi'; + +import { IEditorService } from '@univerjs/ui'; +import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; + +export class DocHeaderFooterController extends Disposable implements IRenderModule { + private _loadedMap = new WeakSet(); + + constructor( + private readonly _context: IRenderContext, + @ICommandService private readonly _commandService: ICommandService, + @IEditorService private readonly _editorService: IEditorService, + @IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService, + @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService + ) { + super(); + + this._initialize(); + } + + private _initialize() { + this._init(); + } + + private _init() { + const { unitId } = this._context; + const docObject = neoGetDocObject(this._context); + if (docObject == null || docObject.document == null) { + return; + } + + if (!this._loadedMap.has(docObject.document)) { + this._initialMain(unitId); + this._loadedMap.add(docObject.document); + } + } + + private _initialMain(unitId: string) { + const docObject = neoGetDocObject(this._context); + const { document } = docObject; + + this.disposeWithMe(document.onDblclickObserver.add((evt: IPointerEvent | IMouseEvent) => { + if (this._isEditorReadOnly(unitId)) { + return; + } + + const { offsetX, offsetY } = evt; + + const { + pageLayoutType = PageLayoutType.VERTICAL, + pageMarginLeft, + pageMarginTop, + } = document.getOffsetConfig(); + + const coord = this._getTransformCoordForDocumentOffset(offsetX, offsetY); + + if (coord == null) { + return; + } + + const viewModel = this._docSkeletonManagerService.getViewModel(); + const skeleton = this._docSkeletonManagerService.getSkeleton(); + const preEditArea = viewModel.getEditArea(); + const editArea = skeleton.findEditAreaByCoord( + coord, + pageLayoutType, + pageMarginLeft, + pageMarginTop + ); + + if (preEditArea !== editArea) { + viewModel.setEditArea(editArea); + } + })); + } + + private _getTransformCoordForDocumentOffset(evtOffsetX: number, evtOffsetY: number) { + const docObject = neoGetDocObject(this._context); + const { document, scene } = docObject; + const { documentTransform } = document.getOffsetConfig(); + const activeViewport = scene.getViewports()[0]; + + if (activeViewport == null) { + return; + } + + const originCoord = activeViewport.getRelativeVector(Vector2.FromArray([evtOffsetX, evtOffsetY])); + + return documentTransform.clone().invert().applyPoint(originCoord); + } + + private _isEditorReadOnly(unitId: string) { + const editor = this._editorService.getEditor(unitId); + if (!editor) { + return false; + } + + return editor.isReadOnly(); + } +} diff --git a/packages/docs-ui/src/docs-ui-plugin.ts b/packages/docs-ui/src/docs-ui-plugin.ts index 945e619debf..546bee66da2 100644 --- a/packages/docs-ui/src/docs-ui-plugin.ts +++ b/packages/docs-ui/src/docs-ui-plugin.ts @@ -56,6 +56,7 @@ import { DocTextSelectionRenderController } from './controllers/render-controlle import { DocBackScrollRenderController } from './controllers/render-controllers/back-scroll.render-controller'; import { DocCanvasPopManagerService } from './services/doc-popup-manager.service'; import { DocsRenderService } from './services/docs-render.service'; +import { DocHeaderFooterController } from './controllers/doc-header-footer.controller'; export class UniverDocsUIPlugin extends Plugin { static override pluginName = DOC_UI_PLUGIN_NAME; @@ -153,6 +154,7 @@ export class UniverDocsUIPlugin extends Plugin { DocBackScrollRenderController, DocFloatingObjectRenderController, DocTextSelectionRenderController, + DocHeaderFooterController, ]).forEach((m) => { this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, m); }); diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index 3ecd9664717..d718d2d7be9 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -38,6 +38,7 @@ import type { IDocumentsConfig, IPageMarginLayout } from './doc-component'; import { DocComponent } from './doc-component'; import { DOCS_EXTENSION_TYPE } from './doc-extension'; import type { DocumentSkeleton } from './layout/doc-skeleton'; +import { DocumentEditArea } from './view-model/document-view-model'; export interface IPageRenderConfig { page: IDocumentSkeletonPage; @@ -122,6 +123,8 @@ export class Documents extends DocComponent { return; } + const isEditBody = this.getSkeleton()?.getViewModel().getEditArea() === DocumentEditArea.BODY; + this._drawLiquid.reset(); const { pages, skeHeaders, skeFooters } = skeletonData; @@ -235,8 +238,10 @@ export class Documents extends DocComponent { this._startRotation(ctx, finalAngle); - // ctx.save(); - // ctx.globalAlpha = 0.5; + if (isEditBody) { + ctx.save(); + ctx.globalAlpha = 0.5; + } for (const section of sections) { const { columns } = section; @@ -432,7 +437,10 @@ export class Documents extends DocComponent { this._drawLiquid.translateRestore(); } } - // ctx.restore(); + + if (isEditBody) { + ctx.restore(); + } this._resetRotation(ctx, finalAngle); @@ -484,6 +492,12 @@ export class Documents extends DocComponent { if (this._drawLiquid == null) { return; } + const isEditHeaderFooter = this.getSkeleton()?.getViewModel().getEditArea() === DocumentEditArea.HEADER_FOOTER; + + if (isEditHeaderFooter) { + ctx.save(); + ctx.globalAlpha = 0.5; + } const { sections } = page; for (const section of sections) { @@ -627,6 +641,10 @@ export class Documents extends DocComponent { this._drawLiquid.translateRestore(); } + + if (isEditHeaderFooter) { + ctx.restore(); + } } private _horizontalHandler( diff --git a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts index bf698228b94..b1ad764d8cb 100644 --- a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts +++ b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts @@ -28,6 +28,7 @@ import type { IViewportInfo, Vector2 } from '../../../basics/vector2'; import { Skeleton } from '../../skeleton'; import { Liquid } from '../liquid'; import type { DocumentViewModel } from '../view-model/document-view-model'; +import { DocumentEditArea } from '../view-model/document-view-model'; import type { ILayoutContext } from './tools'; import { getLastPage, getNullSkeleton, prepareSectionBreakConfig, setPageParent, updateBlockIndex } from './tools'; import { createSkeletonSection } from './model/section'; @@ -257,6 +258,59 @@ export class DocumentSkeleton extends Skeleton { return glyphGroup[glyph]; } + findEditAreaByCoord( + coord: Vector2, + pageLayoutType: PageLayoutType, + pageMarginLeft: number, + pageMarginTop: number + ): DocumentEditArea { + const { x, y } = coord; + let editArea = DocumentEditArea.BODY; + const skeletonData = this.getSkeletonData(); + + if (skeletonData == null) { + return editArea; + } + + this._findLiquid.reset(); + + const pages = skeletonData.pages; + + for (let i = 0, len = pages.length; i < len; i++) { + const page = pages[i]; + + const { marginTop, marginBottom, pageWidth, pageHeight } = page; + + if ( + x > this._findLiquid.x && x < this._findLiquid.x + pageWidth && + y > this._findLiquid.y && y < this._findLiquid.y + marginTop + ) { + editArea = DocumentEditArea.HEADER_FOOTER; + break; + } + + if ( + x > this._findLiquid.x && x < this._findLiquid.x + pageWidth && + y > this._findLiquid.y + marginTop && y < this._findLiquid.y + pageHeight - marginBottom + ) { + editArea = DocumentEditArea.BODY; + break; + } + + if ( + x > this._findLiquid.x && x < this._findLiquid.x + pageWidth && + y > this._findLiquid.y + pageHeight - marginBottom && y < this._findLiquid.y + pageHeight + ) { + editArea = DocumentEditArea.HEADER_FOOTER; + break; + } + + this._translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); + } + + return editArea; + } + findNodeByCoord( coord: Vector2, pageLayoutType: PageLayoutType, diff --git a/packages/engine-render/src/components/docs/view-model/document-view-model.ts b/packages/engine-render/src/components/docs/view-model/document-view-model.ts index 3438b20f7a7..ca1439b1eef 100644 --- a/packages/engine-render/src/components/docs/view-model/document-view-model.ts +++ b/packages/engine-render/src/components/docs/view-model/document-view-model.ts @@ -25,25 +25,24 @@ export interface ICustomRangeInterceptor { getCustomDecoration: (index: number) => Nullable; } +export enum DocumentEditArea { + BODY = 'BODY', + HEADER_FOOTER = 'HEADER_FOOTER', +} + export class DocumentViewModel implements IDisposable { private _interceptor: Nullable = null; children: DataStreamTreeNode[] = []; - private _sectionBreakCurrentIndex = 0; - private _paragraphCurrentIndex = 0; - private _textRunCurrentIndex = 0; - private _customBlockCurrentIndex = 0; - private _tableBlockCurrentIndex = 0; - private _customRangeCurrentIndex = 0; + private _editArea: DocumentEditArea = DocumentEditArea.BODY; headerTreeMap: Map = new Map(); - footerTreeMap: Map = new Map(); constructor(private _documentDataModel: DocumentDataModel) { @@ -71,6 +70,14 @@ export class DocumentViewModel implements IDisposable { // empty } + getEditArea() { + return this._editArea; + } + + setEditArea(editArea: DocumentEditArea) { + this._editArea = editArea; + } + getPositionInParent() { return 0; } diff --git a/packages/engine-render/src/index.ts b/packages/engine-render/src/index.ts index f847f0077de..4102bc26304 100644 --- a/packages/engine-render/src/index.ts +++ b/packages/engine-render/src/index.ts @@ -50,3 +50,4 @@ export { ThinEngine } from './thin-engine'; export { getCharSpaceApply, getNumberUnitValue } from './components/docs/layout/tools'; export { type IChangeObserverConfig } from './scene.transformer'; export { DEFAULT_PADDING_DATA } from './components/sheets/sheet-skeleton'; +export { DocumentEditArea } from './components/docs/view-model/document-view-model'; From c3286f93de233fddd62ba370799ee345a5fe554e Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 24 Jun 2024 22:26:33 +0800 Subject: [PATCH 06/39] feat: change edit area --- .../doc-header-footer.controller.ts | 100 +++++++++++++- packages/docs-ui/src/locale/en-US.ts | 4 + packages/docs-ui/src/locale/ru-RU.ts | 4 + packages/docs-ui/src/locale/zh-CN.ts | 4 + .../src/views/header-footer/text-bubble.ts | 130 ++++++++++++++++++ .../src/components/docs/document.ts | 8 +- 6 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 packages/docs-ui/src/views/header-footer/text-bubble.ts diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index 25c03478f56..969e94030f5 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -15,13 +15,17 @@ */ import type { DocumentDataModel } from '@univerjs/core'; -import { Disposable, ICommandService, IUniverInstanceService } from '@univerjs/core'; -import type { IMouseEvent, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; -import { PageLayoutType, Vector2 } from '@univerjs/engine-render'; +import { Disposable, ICommandService, IUniverInstanceService, LocaleService, toDisposable } from '@univerjs/core'; +import type { Documents, IMouseEvent, IPageRenderConfig, IPathProps, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; +import { DocumentEditArea, IRenderManagerService, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; import { IEditorService } from '@univerjs/ui'; import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; +import { TextBubbleShape } from '../views/header-footer/text-bubble'; + +const HEADER_FOOTER_STROKE_COLOR = 'rgb(0, 0, 255)'; +const HEADER_FOOTER_FILL_COLOR = 'rgb(219, 231, 244)'; export class DocHeaderFooterController extends Disposable implements IRenderModule { private _loadedMap = new WeakSet(); @@ -31,7 +35,9 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu @ICommandService private readonly _commandService: ICommandService, @IEditorService private readonly _editorService: IEditorService, @IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService, - @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService + @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, + @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, + @Inject(LocaleService) private readonly _localeService: LocaleService ) { super(); @@ -40,6 +46,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu private _initialize() { this._init(); + this._drawHeaderFooterLabel(); } private _init() { @@ -109,6 +116,91 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu return documentTransform.clone().invert().applyPoint(originCoord); } + // eslint-disable-next-line max-lines-per-function + private _drawHeaderFooterLabel() { + const localeService = this._localeService; + this._renderManagerService.currentRender$.subscribe((unitId) => { + if (unitId == null) { + return; + } + + const currentRender = this._renderManagerService.getRenderById(unitId); + if (this._editorService.isEditor(unitId) || this._instanceSrv.getUniverDocInstance(unitId) == null) { + return; + } + + if (currentRender == null) { + return; + } + + const { mainComponent } = currentRender; + + const docsComponent = mainComponent as Documents; + + const pageSize = docsComponent.getSkeleton()?.getPageSize(); + + this.disposeWithMe( + toDisposable( + docsComponent.onPageRenderObservable.add((config: IPageRenderConfig) => { + const viewModel = this._docSkeletonManagerService.getViewModel(); + const isEditBody = viewModel.getEditArea() === DocumentEditArea.BODY; + if (this._editorService.isEditor(unitId) || isEditBody) { + return; + } + + // Draw page borders + const { page, pageLeft, pageTop, ctx } = config; + + const { pageWidth, pageHeight, marginTop, marginBottom } = page; + + ctx.save(); + + ctx.translate(pageLeft - 0.5, pageTop - 0.5); + + const headerPathConfigIPathProps = { + dataArray: [{ + command: 'M', + points: [0, marginTop], + }, { + command: 'L', + points: [pageWidth, marginTop], + }] as unknown as IPathProps['dataArray'], + strokeWidth: 1, + stroke: HEADER_FOOTER_STROKE_COLOR, + }; + + const footerPathConfigIPathProps = { + dataArray: [{ + command: 'M', + points: [0, pageHeight - marginBottom], + }, { + command: 'L', + points: [pageWidth, pageHeight - marginBottom], + }] as unknown as IPathProps['dataArray'], + strokeWidth: 1, + stroke: HEADER_FOOTER_STROKE_COLOR, + }; + + Path.drawWith(ctx, headerPathConfigIPathProps); + Path.drawWith(ctx, footerPathConfigIPathProps); + + ctx.translate(0, marginTop + 1); + TextBubbleShape.drawWith(ctx, { + text: localeService.t('headerFooter.header'), + color: HEADER_FOOTER_FILL_COLOR, + }); + ctx.translate(0, pageHeight - marginTop - marginBottom); + TextBubbleShape.drawWith(ctx, { + text: localeService.t('headerFooter.footer'), + color: HEADER_FOOTER_FILL_COLOR, + }); + ctx.restore(); + }) + ) + ); + }); + } + private _isEditorReadOnly(unitId: string) { const editor = this._editorService.getEditor(unitId); if (!editor) { diff --git a/packages/docs-ui/src/locale/en-US.ts b/packages/docs-ui/src/locale/en-US.ts index 603102626ad..69a7d6410f2 100644 --- a/packages/docs-ui/src/locale/en-US.ts +++ b/packages/docs-ui/src/locale/en-US.ts @@ -44,6 +44,10 @@ const locale: typeof zhCN = { alignRight: 'Align Right', alignJustify: 'Justify', }, + headerFooter: { + header: 'Header', + footer: 'Footer', + }, }; export default locale; diff --git a/packages/docs-ui/src/locale/ru-RU.ts b/packages/docs-ui/src/locale/ru-RU.ts index a48b2ef9e57..b77f799d0d9 100644 --- a/packages/docs-ui/src/locale/ru-RU.ts +++ b/packages/docs-ui/src/locale/ru-RU.ts @@ -44,6 +44,10 @@ const locale: typeof zhCN = { alignRight: 'Выровнять по правому краю', alignJustify: 'Выровнять по ширине', }, + headerFooter: { + header: 'Header', + footer: 'Footer', + }, }; export default locale; diff --git a/packages/docs-ui/src/locale/zh-CN.ts b/packages/docs-ui/src/locale/zh-CN.ts index dd4b0658e72..c359f8c4775 100644 --- a/packages/docs-ui/src/locale/zh-CN.ts +++ b/packages/docs-ui/src/locale/zh-CN.ts @@ -42,6 +42,10 @@ const locale = { alignRight: '右对齐', alignJustify: '两端对齐', }, + headerFooter: { + header: '页眉', + footer: '页脚', + }, }; export default locale; diff --git a/packages/docs-ui/src/views/header-footer/text-bubble.ts b/packages/docs-ui/src/views/header-footer/text-bubble.ts new file mode 100644 index 00000000000..64d4db05040 --- /dev/null +++ b/packages/docs-ui/src/views/header-footer/text-bubble.ts @@ -0,0 +1,130 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + IRectProps, + IShapeProps, + Rect, + UniverRenderingContext } from '@univerjs/engine-render'; +import { Shape } from '@univerjs/engine-render'; + +export const COLLAB_CURSOR_LABEL_HEIGHT = 18; +export const COLLAB_CURSOR_LABEL_MAX_WIDTH = 200; +export const COLLAB_CURSOR_LABEL_TEXT_PADDING_LR = 6; +export const COLLAB_CURSOR_LABEL_TEXT_PADDING_TB = 4; + +function drawBubble(ctx: CanvasRenderingContext2D, props: IRectProps | Rect): void { + let { radius, width, height } = props; + + radius = radius ?? 0; + width = width ?? 30; + height = height ?? 30; + let bottomRight = 0; + bottomRight = Math.min(radius, width / 2, height / 2); + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(width, 0); + ctx.lineTo(width, height - bottomRight); + ctx.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false); + ctx.lineTo(0, height); + ctx.lineTo(0, 0); + ctx.closePath(); + + if (props.fill) { + ctx.save(); + ctx.fillStyle = props.fill!; + if (props.fillRule === 'evenodd') { + ctx.fill('evenodd'); + } else { + ctx.fill(); + } + ctx.restore(); + } +} + +export interface ITextBubbleShapeProps extends IShapeProps { + color: string; + text: string; +} + +/** + * Render a single collaborated cursor on the canvas. + */ +export class TextBubbleShape< + T extends ITextBubbleShapeProps = ITextBubbleShapeProps +> extends Shape { + color: string; + text: string; + + constructor(key: string, props: T) { + super(key, props); + + this.color = props?.color; + this.text = props?.text; + } + + static override drawWith(ctx: CanvasRenderingContext2D, props: ITextBubbleShapeProps): void { + const { text, color } = props; + ctx.save(); + // Measure the text width + ctx.font = 'bold 13px Source Han Sans CN'; + const textWidth = ctx.measureText(text).width; + const realInfoWidth = Math.min( + textWidth + 2 * COLLAB_CURSOR_LABEL_TEXT_PADDING_LR, + COLLAB_CURSOR_LABEL_MAX_WIDTH + ); + + // Draw the bubble. which is rect-like + drawBubble(ctx, { + height: COLLAB_CURSOR_LABEL_HEIGHT, + width: realInfoWidth, + radius: 4, + fill: color, + evented: false, + }); + + ctx.fillStyle = 'rgb(0, 0, 255)'; + + // Draw the text with truncation if needed + const offsetX = COLLAB_CURSOR_LABEL_TEXT_PADDING_LR; + const offsetY = COLLAB_CURSOR_LABEL_HEIGHT - COLLAB_CURSOR_LABEL_TEXT_PADDING_TB; + const maxTextWidth = COLLAB_CURSOR_LABEL_MAX_WIDTH - 2 * COLLAB_CURSOR_LABEL_TEXT_PADDING_LR; + if (textWidth > maxTextWidth) { + let truncatedText = ''; + let currentWidth = 0; + for (const element of text) { + const charWidth = ctx.measureText(element).width; + if (currentWidth + charWidth <= maxTextWidth - ctx.measureText('...').width) { + truncatedText += element; + currentWidth += charWidth; + } else { + truncatedText += '...'; + break; + } + } + ctx.fillText(truncatedText, offsetX, offsetY); + } else { + ctx.fillText(text, offsetX, offsetY); + } + + ctx.restore(); + } + + protected override _draw(ctx: UniverRenderingContext) { + TextBubbleShape.drawWith(ctx, this); + } +} diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index d718d2d7be9..e29517f90d5 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -238,7 +238,7 @@ export class Documents extends DocComponent { this._startRotation(ctx, finalAngle); - if (isEditBody) { + if (!isEditBody) { ctx.save(); ctx.globalAlpha = 0.5; } @@ -438,7 +438,7 @@ export class Documents extends DocComponent { } } - if (isEditBody) { + if (!isEditBody) { ctx.restore(); } @@ -494,7 +494,7 @@ export class Documents extends DocComponent { } const isEditHeaderFooter = this.getSkeleton()?.getViewModel().getEditArea() === DocumentEditArea.HEADER_FOOTER; - if (isEditHeaderFooter) { + if (!isEditHeaderFooter) { ctx.save(); ctx.globalAlpha = 0.5; } @@ -642,7 +642,7 @@ export class Documents extends DocComponent { this._drawLiquid.translateRestore(); } - if (isEditHeaderFooter) { + if (!isEditHeaderFooter) { ctx.restore(); } } From 19ef0c60e2df4130d2ff6edbeaf620b274d91e50 Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 25 Jun 2024 21:25:39 +0800 Subject: [PATCH 07/39] feat: text selection in header and footer --- .../doc-header-footer.controller.ts | 124 ++++++++++++++- .../text-selection.render-controller.ts | 1 + .../src/commands/commands/delete.command.ts | 6 +- .../src/controllers/move-cursor.controller.ts | 49 +++--- .../text-selection-manager.service.ts | 17 +- .../engine-render/src/basics/interfaces.ts | 3 + packages/engine-render/src/basics/range.ts | 2 + .../src/components/docs/document.ts | 3 +- .../components/docs/layout/doc-skeleton.ts | 149 ++++++++++++++---- .../docs/text-selection/convert-cursor.ts | 50 ++++-- .../docs/text-selection/text-range.ts | 11 +- .../text-selection-render-manager.ts | 50 ++---- .../docs/view-model/document-view-model.ts | 3 +- 13 files changed, 340 insertions(+), 128 deletions(-) diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index 969e94030f5..e9addcdcb22 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -15,18 +15,108 @@ */ import type { DocumentDataModel } from '@univerjs/core'; -import { Disposable, ICommandService, IUniverInstanceService, LocaleService, toDisposable } from '@univerjs/core'; -import type { Documents, IMouseEvent, IPageRenderConfig, IPathProps, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; -import { DocumentEditArea, IRenderManagerService, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; +import { BooleanNumber, Disposable, ICommandService, IUniverInstanceService, LocaleService, toDisposable } from '@univerjs/core'; +import type { Documents, DocumentViewModel, IMouseEvent, IPageRenderConfig, IPathProps, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; +import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; import { IEditorService } from '@univerjs/ui'; import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; +import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; const HEADER_FOOTER_STROKE_COLOR = 'rgb(0, 0, 255)'; const HEADER_FOOTER_FILL_COLOR = 'rgb(219, 231, 244)'; +export enum HeaderFooterType { + FIRST_PAGE_HEADER, + FIRST_PAGE_FOOTER, + DEFAULT_HEADER, + DEFAULT_FOOTER, + EVEN_PAGE_HEADER, + EVEN_PAGE_FOOTER, +} + +interface IHeaderFooterCreate { + createType: Nullable; + headerFooterId: Nullable; +} + +// TODO: @JOCS also need to check sectionBreak config in the future. +function checkCreateHeaderFooterType(viewModel: DocumentViewModel, editArea: DocumentEditArea, pageNumber: number): IHeaderFooterCreate { + const { documentStyle } = viewModel.getDataModel().getSnapshot(); + const { + defaultHeaderId, + defaultFooterId, + evenPageHeaderId, + evenPageFooterId, + firstPageHeaderId, + firstPageFooterId, + evenAndOddHeaders, + useFirstPageHeaderFooter, + } = documentStyle; + + switch (editArea) { + case DocumentEditArea.BODY: + return { + createType: null, + headerFooterId: null, + }; + case DocumentEditArea.HEADER: { + if (useFirstPageHeaderFooter === BooleanNumber.TRUE && !firstPageHeaderId) { + return { + createType: HeaderFooterType.FIRST_PAGE_HEADER, + headerFooterId: null, + }; + } + + if (evenAndOddHeaders === BooleanNumber.TRUE && pageNumber % 2 === 0 && !evenPageHeaderId) { + return { + createType: HeaderFooterType.EVEN_PAGE_HEADER, + headerFooterId: null, + }; + } + + return defaultHeaderId + ? { + createType: null, + headerFooterId: defaultHeaderId, + } + : { + createType: HeaderFooterType.DEFAULT_HEADER, + headerFooterId: null, + }; + } + case DocumentEditArea.FOOTER: { + if (useFirstPageHeaderFooter === BooleanNumber.TRUE && !firstPageFooterId) { + return { + createType: HeaderFooterType.FIRST_PAGE_FOOTER, + headerFooterId: null, + }; + } + + if (evenAndOddHeaders === BooleanNumber.TRUE && pageNumber % 2 === 0 && !evenPageFooterId) { + return { + createType: HeaderFooterType.EVEN_PAGE_FOOTER, + headerFooterId: null, + }; + } + + return defaultFooterId + ? { + createType: null, + headerFooterId: defaultFooterId, + } + : { + createType: HeaderFooterType.DEFAULT_FOOTER, + headerFooterId: null, + }; + } + default: + throw new Error(`Invalid editArea: ${editArea}`); + } +} + export class DocHeaderFooterController extends Disposable implements IRenderModule { private _loadedMap = new WeakSet(); @@ -37,6 +127,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu @IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, + @ITextSelectionRenderManager private readonly _textSelectionRenderManager: ITextSelectionRenderManager, @Inject(LocaleService) private readonly _localeService: LocaleService ) { super(); @@ -88,15 +179,30 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu const viewModel = this._docSkeletonManagerService.getViewModel(); const skeleton = this._docSkeletonManagerService.getSkeleton(); const preEditArea = viewModel.getEditArea(); - const editArea = skeleton.findEditAreaByCoord( + const { editArea, pageNumber } = skeleton.findEditAreaByCoord( coord, pageLayoutType, pageMarginLeft, pageMarginTop ); - if (preEditArea !== editArea) { - viewModel.setEditArea(editArea); + if (preEditArea === editArea) { + return; + } + + viewModel.setEditArea(editArea); + + const { createType, headerFooterId } = checkCreateHeaderFooterType(viewModel, editArea, pageNumber); + + if (editArea === DocumentEditArea.BODY) { + this._textSelectionRenderManager.setSegment(''); + } else { + if (createType != null) { + // TODO: create header or footer and set segment. + } else if (headerFooterId != null) { + this._textSelectionRenderManager.setSegment(headerFooterId); + // TODO: set selection to header or footer. + } } })); } @@ -137,8 +243,6 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu const docsComponent = mainComponent as Documents; - const pageSize = docsComponent.getSkeleton()?.getPageSize(); - this.disposeWithMe( toDisposable( docsComponent.onPageRenderObservable.add((config: IPageRenderConfig) => { @@ -209,4 +313,8 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu return editor.isReadOnly(); } + + private _getDocDataModel() { + return this._context.unit; + } } diff --git a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts index e1a192d0676..f5b8fced577 100644 --- a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts @@ -123,6 +123,7 @@ export class DocTextSelectionRenderController extends Disposable implements IRen if (this._isEditorReadOnly(unitId)) { return; } + this._textSelectionRenderManager.handleTripleClick(evt); })); } diff --git a/packages/docs/src/commands/commands/delete.command.ts b/packages/docs/src/commands/commands/delete.command.ts index 1f3e57c360c..9aaae78f21c 100644 --- a/packages/docs/src/commands/commands/delete.command.ts +++ b/packages/docs/src/commands/commands/delete.command.ts @@ -67,7 +67,7 @@ export const DeleteLeftCommand: ICommand = { const actualRange = getDeleteSelection(activeRange, body); const { startOffset, collapsed } = actualRange; const { segmentId, style } = activeRange; - const curGlyph = skeleton.findNodeByCharIndex(startOffset); + const curGlyph = skeleton.findNodeByCharIndex(startOffset, segmentId); // is in bullet list? const isBullet = hasListGlyph(curGlyph); @@ -77,7 +77,7 @@ export const DeleteLeftCommand: ICommand = { let cursor = startOffset; // Get the deleted glyph. It maybe null or undefined when the curGlyph is first glyph in skeleton. - const preGlyph = skeleton.findNodeByCharIndex(startOffset - 1); + const preGlyph = skeleton.findNodeByCharIndex(startOffset - 1, segmentId); const isUpdateParagraph = isFirstGlyph(curGlyph) && preGlyph !== curGlyph && (isBullet === true || isIndent === true); @@ -220,7 +220,7 @@ export const DeleteRightCommand: ICommand = { let result: boolean = false; if (collapsed === true) { - const needDeleteSpan = skeleton.findNodeByCharIndex(startOffset)!; + const needDeleteSpan = skeleton.findNodeByCharIndex(startOffset, segmentId)!; // skip custom-range-split-symbol if (needDeleteSpan.content === '\r') { diff --git a/packages/docs/src/controllers/move-cursor.controller.ts b/packages/docs/src/controllers/move-cursor.controller.ts index 469bdcf7df6..92149fda117 100644 --- a/packages/docs/src/controllers/move-cursor.controller.ts +++ b/packages/docs/src/controllers/move-cursor.controller.ts @@ -106,7 +106,10 @@ export class MoveCursorController extends Disposable { return; } - const { startOffset, endOffset, style, collapsed, direction: rangeDirection } = activeRange; + const { + startOffset, endOffset, style, collapsed, direction: rangeDirection, + segmentId, startNodePosition, endNodePosition, + } = activeRange; if (allRanges.length > 1) { let min = Number.POSITIVE_INFINITY; @@ -138,11 +141,11 @@ export class MoveCursorController extends Disposable { const dataStreamLength = docDataModel.getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; if (direction === Direction.LEFT || direction === Direction.RIGHT) { - const preSpan = skeleton.findNodeByCharIndex(focusOffset - 1); - const curSpan = skeleton.findNodeByCharIndex(focusOffset)!; + const preGlyph = skeleton.findNodeByCharIndex(focusOffset - 1, segmentId); + const curGlyph = skeleton.findNodeByCharIndex(focusOffset, segmentId)!; focusOffset = - direction === Direction.RIGHT ? focusOffset + curSpan.count : focusOffset - (preSpan?.count ?? 0); + direction === Direction.RIGHT ? focusOffset + curGlyph.count : focusOffset - (preGlyph?.count ?? 0); focusOffset = Math.min(dataStreamLength - 2, Math.max(0, focusOffset)); @@ -154,11 +157,12 @@ export class MoveCursorController extends Disposable { }, ], false); } else { - const focusSpan = skeleton.findNodeByCharIndex(focusOffset); - + const focusGlyph = skeleton.findNodeByCharIndex(focusOffset, segmentId); const documentOffsetConfig = docObject.document.getOffsetConfig(); + const focusNodePosition = collapsed ? startNodePosition : rangeDirection === RANGE_DIRECTION.FORWARD ? endNodePosition : startNodePosition; + + const newPos = this._getTopOrBottomPosition(skeleton, focusGlyph, focusNodePosition, direction === Direction.DOWN); - const newPos = this._getTopOrBottomPosition(skeleton, focusSpan, direction === Direction.DOWN); if (newPos == null) { // move selection const newFocusOffset = direction === Direction.UP ? 0 : dataStreamLength - 2; @@ -194,7 +198,7 @@ export class MoveCursorController extends Disposable { } } - // eslint-disable-next-line max-lines-per-function + // eslint-disable-next-line max-lines-per-function, complexity private _handleMoveCursor(direction: Direction) { const activeRange = this._textSelectionManagerService.getActiveRange(); const allRanges = this._textSelectionManagerService.getSelections(); @@ -211,7 +215,7 @@ export class MoveCursorController extends Disposable { return; } - const { startOffset, endOffset, style, collapsed } = activeRange; + const { startOffset, endOffset, style, collapsed, segmentId, startNodePosition, endNodePosition } = activeRange; const dataStreamLength = docDataModel.getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; @@ -229,8 +233,8 @@ export class MoveCursorController extends Disposable { cursor = direction === Direction.LEFT ? min : max; } else { - const preSpan = skeleton.findNodeByCharIndex(startOffset - 1); - const curSpan = skeleton.findNodeByCharIndex(startOffset)!; + const preSpan = skeleton.findNodeByCharIndex(startOffset - 1, segmentId); + const curSpan = skeleton.findNodeByCharIndex(startOffset, segmentId)!; if (direction === Direction.LEFT) { cursor = Math.max(0, startOffset - (preSpan?.count ?? 0)); @@ -248,14 +252,15 @@ export class MoveCursorController extends Disposable { }, ], false); } else { - const startNode = skeleton.findNodeByCharIndex(startOffset); - const endNode = skeleton.findNodeByCharIndex(endOffset); + const startNode = skeleton.findNodeByCharIndex(startOffset, segmentId); + const endNode = skeleton.findNodeByCharIndex(endOffset, segmentId); const documentOffsetConfig = docObject.document.getOffsetConfig(); const newPos = this._getTopOrBottomPosition( skeleton, direction === Direction.UP ? startNode : endNode, + direction === Direction.UP ? startNodePosition : endNodePosition, direction === Direction.DOWN ); @@ -300,13 +305,14 @@ export class MoveCursorController extends Disposable { private _getTopOrBottomPosition( docSkeleton: DocumentSkeleton, glyph: Nullable, + nodePosition: Nullable, direction: boolean ): Nullable { - if (glyph == null) { + if (glyph == null || nodePosition == null) { return; } - const offsetLeft = this._getSpanLeftOffsetInLine(glyph); + const offsetLeft = this._getGlyphLeftOffsetInLine(glyph); const line = this._getNextOrPrevLine(glyph, direction); @@ -314,7 +320,7 @@ export class MoveCursorController extends Disposable { return; } - const position: Nullable = this._matchPositionByLeftOffset(docSkeleton, line, offsetLeft); + const position: Nullable = this._matchPositionByLeftOffset(docSkeleton, line, offsetLeft, nodePosition); if (position == null) { return; @@ -324,7 +330,7 @@ export class MoveCursorController extends Disposable { return { ...position, isBack: true }; } - private _getSpanLeftOffsetInLine(glyph: IDocumentSkeletonGlyph) { + private _getGlyphLeftOffsetInLine(glyph: IDocumentSkeletonGlyph) { const divide = glyph.parent; if (divide == null) { @@ -332,15 +338,13 @@ export class MoveCursorController extends Disposable { } const divideLeft = divide.left; - const { left } = glyph; - const start = divideLeft + left; return start; } - private _matchPositionByLeftOffset(docSkeleton: DocumentSkeleton, line: IDocumentSkeletonLine, offsetLeft: number) { + private _matchPositionByLeftOffset(docSkeleton: DocumentSkeleton, line: IDocumentSkeletonLine, offsetLeft: number, nodePosition: INodePosition) { const nearestNode: { glyph?: IDocumentSkeletonGlyph; distance: number; @@ -368,9 +372,12 @@ export class MoveCursorController extends Disposable { return; } - return docSkeleton.findPositionByGlyph(nearestNode.glyph); + const { segmentPage } = nodePosition; + + return docSkeleton.findPositionByGlyph(nearestNode.glyph, segmentPage); } + // eslint-disable-next-line max-lines-per-function private _getNextOrPrevLine(glyph: IDocumentSkeletonGlyph, direction: boolean) { const divide = glyph.parent; if (divide == null) { diff --git a/packages/docs/src/services/text-selection-manager.service.ts b/packages/docs/src/services/text-selection-manager.service.ts index de9248098ec..e82f202318f 100644 --- a/packages/docs/src/services/text-selection-manager.service.ts +++ b/packages/docs/src/services/text-selection-manager.service.ts @@ -75,7 +75,6 @@ export class TextSelectionManagerService extends RxDisposable { private readonly _textSelectionInfo: ITextSelectionInfo = new Map(); private readonly _textSelection$ = new BehaviorSubject>(null); - readonly textSelection$ = this._textSelection$.asObservable(); constructor( @@ -191,13 +190,15 @@ export class TextSelectionManagerService extends RxDisposable { // All textRanges should be synchronized from the render layer. private _syncSelectionFromRenderService() { - this._textSelectionRenderManager.textSelectionInner$.pipe(takeUntil(this.dispose$)).subscribe((params) => { - if (params == null) { - return; - } - - this._replaceTextRangesWithNoRefresh(params); - }); + this._textSelectionRenderManager.textSelectionInner$ + .pipe(takeUntil(this.dispose$)) + .subscribe((params) => { + if (params == null) { + return; + } + + this._replaceTextRangesWithNoRefresh(params); + }); } private _replaceTextRangesWithNoRefresh(textSelectionInfo: ITextSelectionInnerParam) { diff --git a/packages/engine-render/src/basics/interfaces.ts b/packages/engine-render/src/basics/interfaces.ts index 76eac8acbc8..4c752d2d9f2 100644 --- a/packages/engine-render/src/basics/interfaces.ts +++ b/packages/engine-render/src/basics/interfaces.ts @@ -144,6 +144,7 @@ export interface INodeInfo { node: IDocumentSkeletonGlyph; ratioX: number; ratioY: number; + segmentPage: number; // The index of the page where node is located. } export interface INodeSearch { @@ -153,6 +154,8 @@ export interface INodeSearch { column: number; section: number; page: number; + segmentPage: number; // The index of the page where the header and footer reside. + isInBody: boolean; } export interface INodePosition extends INodeSearch { diff --git a/packages/engine-render/src/basics/range.ts b/packages/engine-render/src/basics/range.ts index e25e61aef56..e1898bf7865 100644 --- a/packages/engine-render/src/basics/range.ts +++ b/packages/engine-render/src/basics/range.ts @@ -38,6 +38,8 @@ export interface ITextRangeWithStyle extends ITextRangeParam { export interface ISuccinctTextRangeParam { startOffset: number; endOffset: number; + segmentId?: string; // Header of footer id. + pageIndex?: number; // Optional, because header and footer are in different pages, so need pageIndex to allocate selection in header or footer. style?: ITextSelectionStyle; } diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index e29517f90d5..6a2decaecba 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -492,7 +492,8 @@ export class Documents extends DocComponent { if (this._drawLiquid == null) { return; } - const isEditHeaderFooter = this.getSkeleton()?.getViewModel().getEditArea() === DocumentEditArea.HEADER_FOOTER; + const editArea = this.getSkeleton()?.getViewModel().getEditArea(); + const isEditHeaderFooter = editArea === DocumentEditArea.HEADER || editArea === DocumentEditArea.FOOTER; if (!isEditHeaderFooter) { ctx.save(); diff --git a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts index b1ad764d8cb..c682976cc33 100644 --- a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts +++ b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts @@ -154,18 +154,15 @@ export class DocumentSkeleton extends Skeleton { return this.getViewModel().getDataModel().documentStyle.pageSize; } - findPositionByGlyph(glyph: IDocumentSkeletonGlyph): Nullable { + findPositionByGlyph(glyph: IDocumentSkeletonGlyph, segmentPage: number): Nullable { const divide = glyph.parent; - const line = divide?.parent; - const column = line?.parent; - const section = column?.parent; - const page = section?.parent; - const skeletonData = this.getSkeletonData(); + const viewModel = this.getViewModel(); + const editArea = viewModel.getEditArea(); if (!divide || !column || !section || !page || !skeletonData) { return; @@ -181,7 +178,9 @@ export class DocumentSkeleton extends Skeleton { const sectionIndex = page.sections.indexOf(section); - const pageIndex = skeletonData.pages.indexOf(page); + const pageIndex = editArea !== DocumentEditArea.BODY + ? 0 // Because header or footer only has one page. + : skeletonData.pages.indexOf(page); return { glyph: glyphIndex, @@ -190,11 +189,13 @@ export class DocumentSkeleton extends Skeleton { column: columnIndex, section: sectionIndex, page: pageIndex, + segmentPage, + isInBody: editArea === DocumentEditArea.BODY, }; } - findNodePositionByCharIndex(charIndex: number, isBack: boolean = true): Nullable { - const nodes = this._findNodeIterator(charIndex); + findNodePositionByCharIndex(charIndex: number, isBack: boolean = true, segmentId = ''): Nullable { + const nodes = this._findNodeIterator(charIndex, segmentId); if (nodes == null) { return; @@ -221,8 +222,8 @@ export class DocumentSkeleton extends Skeleton { }; } - findNodeByCharIndex(charIndex: number): Nullable { - const nodes = this._findNodeIterator(charIndex); + findNodeByCharIndex(charIndex: number, segmentId = ''): Nullable { + const nodes = this._findNodeIterator(charIndex, segmentId); return nodes?.glyph; } @@ -238,7 +239,10 @@ export class DocumentSkeleton extends Skeleton { return; } - const { divide, line, column, section, page, isBack } = position; + const editArea = this.getViewModel().getEditArea(); + const { pages, skeFooters, skeHeaders } = skeletonData; + + const { divide, line, column, section, page, isBack, isInBody, segmentPage } = position; let { glyph } = position; @@ -248,8 +252,31 @@ export class DocumentSkeleton extends Skeleton { glyph = glyph < 0 ? 0 : glyph; + let skePage = pages[page]; + + if (editArea !== DocumentEditArea.BODY) { + skePage = pages[segmentPage]; + const { headerId, footerId, pageWidth } = skePage; + + if (editArea === DocumentEditArea.HEADER) { + const skeHeader = skeHeaders.get(headerId)?.get(pageWidth); + if (skeHeader == null) { + return; + } else { + skePage = skeHeader; + } + } else if (editArea === DocumentEditArea.FOOTER) { + const skeFooter = skeFooters.get(footerId)?.get(pageWidth); + if (skeFooter == null) { + return; + } else { + skePage = skeFooter; + } + } + } + const glyphGroup = - skeletonData.pages[page].sections[section].columns[column].lines[line].divides[divide].glyphGroup; + skePage.sections[section].columns[column].lines[line].divides[divide].glyphGroup; if (glyphGroup[glyph].glyphType === GlyphType.LIST) { return glyphGroup[glyph + 1]; @@ -263,13 +290,23 @@ export class DocumentSkeleton extends Skeleton { pageLayoutType: PageLayoutType, pageMarginLeft: number, pageMarginTop: number - ): DocumentEditArea { + ): { + editArea: DocumentEditArea; + pageNumber: number; + page: Nullable; + } { const { x, y } = coord; let editArea = DocumentEditArea.BODY; + let pageNumber = 0; + let pageSkeleton = null; const skeletonData = this.getSkeletonData(); if (skeletonData == null) { - return editArea; + return { + editArea, + page: pageSkeleton, + pageNumber, + }; } this._findLiquid.reset(); @@ -285,7 +322,9 @@ export class DocumentSkeleton extends Skeleton { x > this._findLiquid.x && x < this._findLiquid.x + pageWidth && y > this._findLiquid.y && y < this._findLiquid.y + marginTop ) { - editArea = DocumentEditArea.HEADER_FOOTER; + editArea = DocumentEditArea.HEADER; + pageSkeleton = page; + pageNumber = i; break; } @@ -294,6 +333,8 @@ export class DocumentSkeleton extends Skeleton { y > this._findLiquid.y + marginTop && y < this._findLiquid.y + pageHeight - marginBottom ) { editArea = DocumentEditArea.BODY; + pageSkeleton = page; + pageNumber = i; break; } @@ -301,14 +342,20 @@ export class DocumentSkeleton extends Skeleton { x > this._findLiquid.x && x < this._findLiquid.x + pageWidth && y > this._findLiquid.y + pageHeight - marginBottom && y < this._findLiquid.y + pageHeight ) { - editArea = DocumentEditArea.HEADER_FOOTER; + editArea = DocumentEditArea.HEADER; + pageSkeleton = page; + pageNumber = i; break; } this._translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); } - return editArea; + return { + editArea, + page: pageSkeleton, + pageNumber, + }; } findNodeByCoord( @@ -322,12 +369,13 @@ export class DocumentSkeleton extends Skeleton { this._findLiquid.reset(); const skeletonData = this.getSkeletonData(); - if (skeletonData == null) { return; } - const pages = skeletonData.pages; + const editArea = this.getViewModel().getEditArea(); + + const { pages, skeHeaders, skeFooters } = skeletonData; let nearestNodeList: INodeInfo[] = []; @@ -335,8 +383,9 @@ export class DocumentSkeleton extends Skeleton { let nearestNodeDistanceY = Number.POSITIVE_INFINITY; - for (let i = 0, len = pages.length; i < len; i++) { - const page = pages[i]; + for (let pi = 0, len = pages.length; pi < len; pi++) { + let page = pages[pi]; + const { headerId, footerId, pageWidth } = page; // const { startX, startY, endX, endY } = this._getPageBoundingBox(page, pageLayoutType); @@ -345,12 +394,28 @@ export class DocumentSkeleton extends Skeleton { // continue; // } - this._findLiquid.translatePagePadding(page); + if (editArea === DocumentEditArea.HEADER) { + const headerSke = skeHeaders.get(headerId)?.get(pageWidth) as IDocumentSkeletonPage; + + if (headerSke) { + page = headerSke; + } + } else if (editArea === DocumentEditArea.FOOTER) { + const footerSke = skeFooters.get(footerId)?.get(pageWidth) as IDocumentSkeletonPage; + + if (footerSke) { + page = footerSke; + } + } const { sections } = page; + this._findLiquid.translatePagePadding({ + ...page, + marginLeft: pages[pi].marginLeft, // Because header or footer margin Left is 0. + }); for (const section of sections) { - const { columns, height } = section; + const { columns } = section; this._findLiquid.translateSection(section); @@ -361,7 +426,7 @@ export class DocumentSkeleton extends Skeleton { // } for (const column of columns) { - const { lines, width: columnWidth } = column; + const { lines } = column; this._findLiquid.translateSave(); this._findLiquid.translateColumn(column); @@ -400,7 +465,7 @@ export class DocumentSkeleton extends Skeleton { const divideLength = divides.length; for (let i = 0; i < divideLength; i++) { const divide = divides[i]; - const { glyphGroup, width: divideWidth } = divide; + const { glyphGroup } = divide; this._findLiquid.translateSave(); this._findLiquid.translateDivide(divide); @@ -429,6 +494,7 @@ export class DocumentSkeleton extends Skeleton { if (x >= startX_fin && x <= endX_fin) { return { node: glyph, + segmentPage: pi, ratioX: x / (startX_fin + endX_fin), ratioY: y / (startY_fin + endY_fin), }; @@ -440,6 +506,7 @@ export class DocumentSkeleton extends Skeleton { } nearestNodeList.push({ node: glyph, + segmentPage: pi, ratioX: x / (startX_fin + endX_fin), ratioY: y / (startY_fin + endY_fin), }); @@ -459,6 +526,7 @@ export class DocumentSkeleton extends Skeleton { if (distanceY === nearestNodeDistanceY) { nearestNodeList.push({ node: glyph, + segmentPage: pi, ratioX: x / (startX_fin + endX_fin), ratioY: y / (startY_fin + endY_fin), }); @@ -475,7 +543,7 @@ export class DocumentSkeleton extends Skeleton { } } this._findLiquid.restorePagePadding(page); - this._translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); + this._translatePage(pages[pi], pageLayoutType, pageMarginLeft, pageMarginTop); } return this._getNearestNode(nearestNodeList, nearestNodeDistanceList); @@ -484,6 +552,7 @@ export class DocumentSkeleton extends Skeleton { private _getNearestNode(nearestNodeList: INodeInfo[], nearestNodeDistanceList: number[]) { const miniValue = Math.min(...nearestNodeDistanceList); const miniValueIndex = nearestNodeDistanceList.indexOf(miniValue); + return nearestNodeList[miniValueIndex]; } @@ -735,17 +804,37 @@ export class DocumentSkeleton extends Skeleton { sections.push(newSection); } - private _findNodeIterator(charIndex: number) { + private _findNodeIterator(charIndex: number, segmentId = '') { const skeletonData = this.getSkeletonData(); if (!skeletonData) { return; } - const pages = skeletonData.pages; + const editArea = this.getViewModel().getEditArea(); + const { pages, skeFooters, skeHeaders } = skeletonData; for (const page of pages) { - const { sections, st, ed } = page; + const { pageWidth } = page; + let segmentPage = page; + + if (editArea === DocumentEditArea.HEADER) { + const headerSke = skeHeaders.get(segmentId)?.get(pageWidth); + if (headerSke) { + segmentPage = headerSke; + } else { + continue; + } + } else if (editArea === DocumentEditArea.FOOTER) { + const footerSke = skeFooters.get(segmentId)?.get(pageWidth); + if (footerSke) { + segmentPage = footerSke; + } else { + continue; + } + } + + const { sections, st, ed } = segmentPage; if (charIndex < st || charIndex > ed) { continue; diff --git a/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts b/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts index d55db12f74b..5bd0489944b 100644 --- a/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts +++ b/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts @@ -30,6 +30,7 @@ import type { IPoint } from '../../../basics/vector2'; import type { DocumentSkeleton } from '../layout/doc-skeleton'; import type { IDocumentOffsetConfig } from '../document'; import { Liquid } from '../liquid'; +import { DocumentEditArea } from '../view-model/document-view-model'; export enum NodePositionStateType { NORMAL, @@ -443,8 +444,8 @@ export class NodePositionConvertToCursor { startPosition: INodePosition, endPosition: INodePosition, func: ( - startSpanIndex: number, - endSpanIndex: number, + startGlyphIndex: number, + endGlyphIndex: number, isFirst: boolean, isLast: boolean, divide: IDocumentSkeletonDivide, @@ -459,6 +460,9 @@ export class NodePositionConvertToCursor { return []; } + const viewModel = this._docSkeleton.getViewModel(); + const editArea = viewModel.getEditArea(); + this._liquid.reset(); const skeletonData = skeleton.getSkeletonData(); @@ -467,11 +471,10 @@ export class NodePositionConvertToCursor { return []; } - const pages = skeletonData.pages; - - const { page: pageIndex } = startPosition; + const { pages, skeHeaders, skeFooters } = skeletonData; - const { page: endPageIndex } = endPosition; + const { page: pageIndex, segmentPage } = startPosition; + const { page: endPageIndex, segmentPage: endSegmentPage } = endPosition; this._resetCurrentNodePositionState(); @@ -481,24 +484,45 @@ export class NodePositionConvertToCursor { const { pageLayoutType, pageMarginLeft, pageMarginTop } = this._documentOffsetConfig; - for (let p = 0; p <= pageIndex - 1; p++) { + const skipPageIndex = editArea === DocumentEditArea.BODY ? pageIndex : segmentPage; + + for (let p = 0; p < skipPageIndex; p++) { const page = pages[p]; this._liquid.translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); } - for (let p = pageIndex; p <= endPageIndex; p++) { + const endIndex = editArea === DocumentEditArea.BODY ? endPageIndex : endSegmentPage; + + for (let p = skipPageIndex; p <= endIndex; p++) { const page = pages[p]; - const sections = page.sections; + const { headerId, footerId, pageWidth } = page; + let segmentPage: Nullable = page; + + if (editArea === DocumentEditArea.HEADER) { + segmentPage = skeHeaders.get(headerId)?.get(pageWidth); + } else if (editArea === DocumentEditArea.FOOTER) { + segmentPage = skeFooters.get(footerId)?.get(pageWidth); + } + + if (segmentPage == null) { + this._liquid.translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); + continue; + } + + const sections = segmentPage.sections; const { start_next: start_s, end_next: end_s } = this._getSelectionRuler( NodePositionMap.page, startPosition, endPosition, sections.length - 1, - p + editArea === DocumentEditArea.BODY ? p : 0 ); this._liquid.translateSave(); - this._liquid.translatePagePadding(page); + this._liquid.translatePagePadding({ + ...segmentPage, + marginLeft: page.marginLeft, + }); for (let s = start_s; s <= end_s; s++) { const section = sections[s]; @@ -566,7 +590,7 @@ export class NodePositionConvertToCursor { isLast = true; } - func && func(start_sp, end_sp, isFirst, isLast, divide, line, column, section, page); + func && func(start_sp, end_sp, isFirst, isLast, divide, line, column, section, segmentPage); this._liquid.translateRestore(); } @@ -575,8 +599,8 @@ export class NodePositionConvertToCursor { } } } - this._liquid.translateRestore(); + this._liquid.translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop); } } diff --git a/packages/engine-render/src/components/docs/text-selection/text-range.ts b/packages/engine-render/src/components/docs/text-selection/text-range.ts index 7cdbaaeb2bc..d775454f961 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-range.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-range.ts @@ -52,12 +52,12 @@ export function cursorConvertToTextRange( docSkeleton: DocumentSkeleton, document: Documents ): Nullable { - const { startOffset, endOffset, style = NORMAL_TEXT_SELECTION_PLUGIN_STYLE } = range; + const { startOffset, endOffset, style = NORMAL_TEXT_SELECTION_PLUGIN_STYLE, segmentId = '' } = range; - const anchorNodePosition = docSkeleton.findNodePositionByCharIndex(startOffset); - const focusNodePosition = startOffset !== endOffset ? docSkeleton.findNodePositionByCharIndex(endOffset) : null; + const anchorNodePosition = docSkeleton.findNodePositionByCharIndex(startOffset, true, segmentId); + const focusNodePosition = startOffset !== endOffset ? docSkeleton.findNodePositionByCharIndex(endOffset, true, segmentId) : null; - const textRange = new TextRange(scene, document, docSkeleton, anchorNodePosition, focusNodePosition, style); + const textRange = new TextRange(scene, document, docSkeleton, anchorNodePosition, focusNodePosition, style, segmentId); textRange.refresh(); @@ -99,7 +99,8 @@ export class TextRange { private _docSkeleton: DocumentSkeleton, public anchorNodePosition?: Nullable, public focusNodePosition?: Nullable, - public style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE + public style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE, + public segmentId = '' ) { this._anchorBlink(); } diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index 6bc42529877..21bbed10a05 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -61,7 +61,7 @@ export function getCanvasOffsetByEngine(engine: Nullable) { }; } -function getParagraphInfoBySpan(node: IDocumentSkeletonGlyph) { +function getParagraphInfoByGlyph(node: IDocumentSkeletonGlyph) { const line = node.parent?.parent; const column = line?.parent; @@ -130,42 +130,25 @@ export interface ITextSelectionRenderManager { readonly textSelectionInner$: Observable>; __getEditorContainer(): HTMLElement; - getViewPort(): Viewport; - enableSelection(): void; - disableSelection(): void; - setSegment(id: string): void; - setStyle(style: ITextSelectionStyle): void; - resetStyle(): void; - removeAllTextRanges(): void; - addTextRanges(ranges: ISuccinctTextRangeParam[], isEditing?: boolean): void; - sync(): void; - activate(x: number, y: number): void; deactivate(): void; - hasFocus(): boolean; focus(): void; blur(): void; - changeRuntime(docSkeleton: DocumentSkeleton, scene: Scene, document: Documents): void; - dispose(): void; - handleDblClick(evt: IPointerEvent | IMouseEvent): void; - handleTripleClick(evt: IPointerEvent | IMouseEvent): void; - eventTrigger(evt: IPointerEvent | IMouseEvent): void; - setCursorManually(evtOffsetX: number, evtOffsetY: number): void; } @@ -209,23 +192,16 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel private readonly _onBlur$ = new Subject>(); readonly onBlur$ = this._onBlur$.asObservable(); - private _container!: HTMLDivElement; - private _inputParent!: HTMLDivElement; - private _input!: HTMLDivElement; - private _scrollTimers: ScrollTimer[] = []; - + private _viewportScrollX: number = 0; + private _viewportScrollY: number = 0; private _rangeList: TextRange[] = []; - private _currentSegmentId: string = ''; - private _selectionStyle: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE; - private _isSelectionEnabled: boolean = true; - private _viewPortObserverMap = new Map< string, { @@ -235,13 +211,9 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel >(); private _isIMEInputApply = false; - private _activeViewport!: Viewport; - private _docSkeleton: Nullable; - private _scene: Nullable; - private _document: Nullable; private _scenePointerMoveSubs: Array = []; private _scenePointerUpSubs: Array = []; @@ -365,7 +337,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel return; } - const paragraphInfo = getParagraphInfoBySpan(startNode.node); + const paragraphInfo = getParagraphInfoByGlyph(startNode.node); if (paragraphInfo == null) { return; } @@ -423,7 +395,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel return; } - const paragraphInfo = getParagraphInfoBySpan(startNode.node); + const paragraphInfo = getParagraphInfoByGlyph(startNode.node); if (paragraphInfo == null) { return; } @@ -490,7 +462,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel if (evt.shiftKey && this._getActiveRangeInstance()) { this._updateActiveRangeFocusPosition(position); } else if (evt.ctrlKey || this._isEmpty()) { - const newTextSelection = new TextRange(scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle); + const newTextSelection = new TextRange(scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle, this._currentSegmentId); this._addTextRange(newTextSelection); } else { @@ -680,9 +652,9 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel return; } - const { node: glyph, ratioX } = node; + const { node: glyph, ratioX, segmentPage } = node; - const position = this._docSkeleton?.findPositionByGlyph(glyph); + const position = this._docSkeleton?.findPositionByGlyph(glyph, segmentPage); if (position == null) { return; @@ -752,7 +724,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel let lastRange = this._rangeList.pop(); if (!lastRange) { - lastRange = new TextRange(this._scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle); + lastRange = new TextRange(this._scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle, this._currentSegmentId); } this._removeAllTextRanges(); @@ -1031,7 +1003,9 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel pageMarginTop, } = this._document!.getOffsetConfig(); - return this._docSkeleton?.findNodeByCoord(coord, pageLayoutType, pageMarginLeft, pageMarginTop); + return this._docSkeleton?.findNodeByCoord( + coord, pageLayoutType, pageMarginLeft, pageMarginTop + ); } private _detachEvent() { diff --git a/packages/engine-render/src/components/docs/view-model/document-view-model.ts b/packages/engine-render/src/components/docs/view-model/document-view-model.ts index ca1439b1eef..4722430925b 100644 --- a/packages/engine-render/src/components/docs/view-model/document-view-model.ts +++ b/packages/engine-render/src/components/docs/view-model/document-view-model.ts @@ -27,7 +27,8 @@ export interface ICustomRangeInterceptor { export enum DocumentEditArea { BODY = 'BODY', - HEADER_FOOTER = 'HEADER_FOOTER', + HEADER = 'HEADER', + FOOTER = 'FOOTER', } export class DocumentViewModel implements IDisposable { From a4386fb5a8765d0acc481b1be35ea60038d25db1 Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 25 Jun 2024 22:01:42 +0800 Subject: [PATCH 08/39] feat: insert text in header --- .../core/src/docs/data-model/json-x/json-x.ts | 4 ++-- .../commands/commands/core-editing.command.ts | 17 ++++++++++----- .../mutations/core-editing.mutation.ts | 4 ++++ packages/docs/src/commands/util.ts | 21 +++++++++++++++++++ .../components/docs/layout/doc-skeleton.ts | 10 ++++++--- .../text-selection-render-manager.ts | 1 + 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/core/src/docs/data-model/json-x/json-x.ts b/packages/core/src/docs/data-model/json-x/json-x.ts index 520b196b9e8..9f762334e74 100644 --- a/packages/core/src/docs/data-model/json-x/json-x.ts +++ b/packages/core/src/docs/data-model/json-x/json-x.ts @@ -113,9 +113,9 @@ export class JSONX { return json1.replaceOp(path, oldVal, newVal); } - editOp(subOp: TextXAction[]) { + editOp(subOp: TextXAction[], path = ['body']) { // Hardcode the path to ['body'] for now. Because rich text is in the body property. - return json1.editOp(['body'], TextX.name, subOp); + return json1.editOp(path, TextX.name, subOp); } } diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index 3c116cfadbb..f4eb85f9422 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -31,6 +31,7 @@ import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; import { isIntersecting, shouldDeleteCustomRange } from '../../basics/custom-range'; import { TextSelectionManagerService } from '../../services/text-selection-manager.service'; import { getInsertSelection } from '../../basics/selection'; +import { getRichTextEditPath } from '../util'; export interface IInsertCommandParams { unitId: string; @@ -52,12 +53,16 @@ export const InsertCommand: ICommand = { handler: async (accessor, params: IInsertCommandParams) => { const commandService = accessor.get(ICommandService); - + const univerInstanceService = accessor.get(IUniverInstanceService); const { range, segmentId, body, unitId, textRanges: propTextRanges } = params; + const docDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); + + if (docDataModel == null) { + return false; + } + const textSelectionManagerService = accessor.get(TextSelectionManagerService); - const univerInstanceService = accessor.get(IUniverInstanceService); - const doc = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); - const originBody = doc?.getBody(); + const originBody = docDataModel?.getBody(); const activeRange = textSelectionManagerService.getActiveRange(); if (!originBody) { @@ -112,7 +117,9 @@ export const InsertCommand: ICommand = { line: 0, segmentId, }); - doMutation.params.actions = jsonX.editOp(textX.serialize()); + + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/mutations/core-editing.mutation.ts b/packages/docs/src/commands/mutations/core-editing.mutation.ts index 65d1cd4ed40..9039f0f2f0f 100644 --- a/packages/docs/src/commands/mutations/core-editing.mutation.ts +++ b/packages/docs/src/commands/mutations/core-editing.mutation.ts @@ -95,6 +95,10 @@ export const RichTextEditingMutation: IMutation Date: Wed, 26 Jun 2024 14:57:13 +0800 Subject: [PATCH 09/39] feat: header and footer support edit --- .../core/src/docs/data-model/replacement.ts | 5 +--- .../commands/commands/break-line.command.ts | 3 +- .../commands/clipboard.inner.command.ts | 22 +++++++------- .../commands/commands/core-editing.command.ts | 29 +++++++++++++++++-- .../src/commands/commands/delete.command.ts | 7 +++-- .../commands/commands/ime-input.command.ts | 17 +++++++---- .../commands/inline-format.command.ts | 12 ++++---- .../src/commands/commands/list.command.ts | 16 +++++----- .../commands/paragraph-align.command.ts | 12 ++++---- .../commands/replace-content.command.ts | 23 +++++++++------ 10 files changed, 93 insertions(+), 53 deletions(-) diff --git a/packages/core/src/docs/data-model/replacement.ts b/packages/core/src/docs/data-model/replacement.ts index 1fa69c872b1..b68213bb9cf 100644 --- a/packages/core/src/docs/data-model/replacement.ts +++ b/packages/core/src/docs/data-model/replacement.ts @@ -62,10 +62,7 @@ export function replaceInDocumentBody(body: IDocumentBody, query: string, target } textX.delete(queryLen); - - const actions = textX.serialize(); - - documentDataModel.apply(jsonX.editOp(actions)); + documentDataModel.apply(jsonX.editOp(textX.serialize())); } const newBody = documentDataModel.getBody()!; diff --git a/packages/docs/src/commands/commands/break-line.command.ts b/packages/docs/src/commands/commands/break-line.command.ts index 7ec6d5c77f5..19b1a9adf29 100644 --- a/packages/docs/src/commands/commands/break-line.command.ts +++ b/packages/docs/src/commands/commands/break-line.command.ts @@ -16,7 +16,6 @@ import type { ICommand, IParagraph } from '@univerjs/core'; import { CommandType, DataStreamTreeTokenType, ICommandService, IUniverInstanceService, Tools } from '@univerjs/core'; - import { TextSelectionManagerService } from '../../services/text-selection-manager.service'; import { InsertCommand } from './core-editing.command'; @@ -66,7 +65,7 @@ export const BreakLineCommand: ICommand = { } const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!docDataModel) { + if (docDataModel == null) { return false; } diff --git a/packages/docs/src/commands/commands/clipboard.inner.command.ts b/packages/docs/src/commands/commands/clipboard.inner.command.ts index 846856f2a7c..363e507512e 100644 --- a/packages/docs/src/commands/commands/clipboard.inner.command.ts +++ b/packages/docs/src/commands/commands/clipboard.inner.command.ts @@ -39,6 +39,7 @@ import type { IRichTextEditingMutationParams } from '../mutations/core-editing.m import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; import { isIntersecting, shouldDeleteCustomRange } from '../../basics/custom-range'; import { getDeleteSelection } from '../../basics/selection'; +import { getRichTextEditPath } from '../util'; export interface IInnerPasteCommandParams { segmentId: string; @@ -63,13 +64,13 @@ export const InnerPasteCommand: ICommand = { return false; } - const docsModel = univerInstanceService.getCurrentUniverDocInstance(); - const originBody = docsModel?.getBody(); - if (!docsModel || !originBody) { + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + const originBody = docDataModel?.getSelfOrHeaderFooterModel(segmentId).getBody(); + if (docDataModel == null || originBody == null) { return false; } - const unitId = docsModel.getUnitId(); + const unitId = docDataModel.getUnitId(); const doMutation: IMutationInfo = { id: RichTextEditingMutation.id, @@ -81,7 +82,6 @@ export const InnerPasteCommand: ICommand = { }; const memoryCursor = new MemoryCursor(); - memoryCursor.reset(); const textX = new TextX(); @@ -120,7 +120,8 @@ export const InnerPasteCommand: ICommand = { memoryCursor.moveCursor(endOffset); } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, @@ -158,9 +159,9 @@ export const CutContentCommand: ICommand = { return false; } - const documentModel = univerInstanceService.getUniverDocInstance(unitId); - const originBody = getDocsUpdateBody(documentModel!.getSnapshot(), segmentId); - if (originBody == null) { + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + const originBody = getDocsUpdateBody(docDataModel!.getSnapshot(), segmentId); + if (docDataModel == null || originBody == null) { return false; } @@ -199,7 +200,8 @@ export const CutContentCommand: ICommand = { memoryCursor.moveCursor(endOffset); } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index f4eb85f9422..77cef79f2dd 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -149,10 +149,18 @@ export interface IDeleteCommandParams { export const DeleteCommand: ICommand = { id: 'doc.command.delete-text', type: CommandType.COMMAND, + // eslint-disable-next-line max-lines-per-function handler: async (accessor, params: IDeleteCommandParams) => { const commandService = accessor.get(ICommandService); - const { range, segmentId, unitId, direction, len = 1 } = params; const univerInstanceService = accessor.get(IUniverInstanceService); + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + + if (docDataModel == null) { + return false; + } + + const { range, segmentId, unitId, direction, len = 1 } = params; + const { startOffset } = range; const documentDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); if (!documentDataModel) { @@ -213,7 +221,15 @@ export const DeleteCommand: ICommand = { cursor = deleteIndex + 1; } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + textX.push({ + t: TextXActionType.DELETE, + len, + line: 0, + segmentId, + }); + + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, @@ -244,6 +260,12 @@ export const UpdateCommand: ICommand = { handler: async (accessor, params: IUpdateCommandParams) => { const { range, segmentId, updateBody, coverType, unitId, textRanges } = params; const commandService = accessor.get(ICommandService); + const univerInstanceService = accessor.get(IUniverInstanceService); + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + + if (docDataModel == null) { + return false; + } const doMutation: IMutationInfo = { id: RichTextEditingMutation.id, @@ -273,7 +295,8 @@ export const UpdateCommand: ICommand = { coverType, }); - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/commands/delete.command.ts b/packages/docs/src/commands/commands/delete.command.ts index 9aaae78f21c..cdb83668acd 100644 --- a/packages/docs/src/commands/commands/delete.command.ts +++ b/packages/docs/src/commands/commands/delete.command.ts @@ -32,7 +32,7 @@ import { TextSelectionManagerService } from '../../services/text-selection-manag import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation'; import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; import { getDeleteSelection, getInsertSelection } from '../../basics/selection'; -import { getCommandSkeleton } from '../util'; +import { getCommandSkeleton, getRichTextEditPath } from '../util'; import { CutContentCommand } from './clipboard.inner.command'; import { DeleteCommand, DeleteDirection, UpdateCommand } from './core-editing.command'; @@ -291,7 +291,7 @@ export const MergeTwoParagraphCommand: ICommand = { } const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!docDataModel) { + if (docDataModel == null) { return false; } @@ -362,7 +362,8 @@ export const MergeTwoParagraphCommand: ICommand = { segmentId, }); - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/commands/ime-input.command.ts b/packages/docs/src/commands/commands/ime-input.command.ts index 722ef79f9d9..397e4480525 100644 --- a/packages/docs/src/commands/commands/ime-input.command.ts +++ b/packages/docs/src/commands/commands/ime-input.command.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { DocumentDataModel, ICommand, ICommandInfo } from '@univerjs/core'; +import type { ICommand, ICommandInfo } from '@univerjs/core'; import { CommandType, ICommandService, IUniverInstanceService, JSONX, TextX, TextXActionType } from '@univerjs/core'; import type { ITextRangeWithStyle } from '@univerjs/engine-render'; @@ -23,6 +23,7 @@ import { IMEInputManagerService } from '../../services/ime-input-manager.service import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation'; import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; import { getInsertSelection } from '../../basics/selection'; +import { getRichTextEditPath } from '../util'; export interface IIMEInputCommandParams { unitId: string; @@ -43,10 +44,15 @@ export const IMEInputCommand: ICommand = { // console.log('===ime', params); const commandService = accessor.get(ICommandService); const imeInputManagerService = accessor.get(IMEInputManagerService); - const previousActiveRange = imeInputManagerService.getActiveRange(); const univerInstanceService = accessor.get(IUniverInstanceService); - const doc = univerInstanceService.getUnit(unitId); - const body = doc?.getBody(); + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + + if (docDataModel == null) { + return false; + } + + const previousActiveRange = imeInputManagerService.getActiveRange(); + const body = docDataModel?.getBody(); if (!previousActiveRange || !body) { return false; } @@ -115,7 +121,8 @@ export const IMEInputCommand: ICommand = { segmentId, }); - doMutation.params!.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params!.actions = jsonX.editOp(textX.serialize(), path); doMutation.params!.noHistory = !isCompositionEnd; diff --git a/packages/docs/src/commands/commands/inline-format.command.ts b/packages/docs/src/commands/commands/inline-format.command.ts index 23376302247..a7e08508d0d 100644 --- a/packages/docs/src/commands/commands/inline-format.command.ts +++ b/packages/docs/src/commands/commands/inline-format.command.ts @@ -32,6 +32,7 @@ import type { TextRange } from '@univerjs/engine-render'; import { serializeTextRange, TextSelectionManagerService } from '../../services/text-selection-manager.service'; import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation'; import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; +import { getRichTextEditPath } from '../util'; function handleInlineFormat( preCommandId: string, @@ -276,12 +277,12 @@ export const SetInlineFormatCommand: ICommand = { return false; } - const documentDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!documentDataModel) { + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + if (!docDataModel) { return false; } - const unitId = documentDataModel.getUnitId(); + const unitId = docDataModel.getUnitId(); let formatValue; @@ -293,7 +294,7 @@ export const SetInlineFormatCommand: ICommand = { case SetInlineFormatSubscriptCommand.id: // fallthrough case SetInlineFormatSuperscriptCommand.id: { formatValue = getReverseFormatValueInSelection( - documentDataModel.getBody()!.textRuns!, + docDataModel.getBody()!.textRuns!, preCommandId, selections ); @@ -379,7 +380,8 @@ export const SetInlineFormatCommand: ICommand = { memoryCursor.moveCursor(endOffset); } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/commands/list.command.ts b/packages/docs/src/commands/commands/list.command.ts index 83df220c2fd..de51d8e89f2 100644 --- a/packages/docs/src/commands/commands/list.command.ts +++ b/packages/docs/src/commands/commands/list.command.ts @@ -33,6 +33,7 @@ import { getCharSpaceApply, getNumberUnitValue, type IActiveTextRange } from '@u import { serializeTextRange, TextSelectionManagerService } from '../../services/text-selection-manager.service'; import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation'; import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; +import { getRichTextEditPath } from '../util'; interface IListOperationCommandParams { listType: PresetListType; @@ -51,14 +52,14 @@ export const ListOperationCommand: ICommand = { const { listType } = params; - const dataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!dataModel) { + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + if (!docDataModel) { return false; } const activeRange = textSelectionManagerService.getActiveRange(); const selections = textSelectionManagerService.getSelections() ?? []; - const paragraphs = dataModel.getBody()?.paragraphs; + const paragraphs = docDataModel.getBody()?.paragraphs; const serializedSelections = selections.map(serializeTextRange); if (activeRange == null || paragraphs == null) { @@ -69,7 +70,7 @@ export const ListOperationCommand: ICommand = { const { segmentId } = activeRange; - const unitId = dataModel.getUnitId(); + const unitId = docDataModel.getUnitId(); const isAlreadyList = currentParagraphs.every((paragraph) => paragraph.bullet?.listType === listType); @@ -105,14 +106,14 @@ export const ListOperationCommand: ICommand = { const textX = new TextX(); const jsonX = JSONX.getInstance(); - const customLists = dataModel.getSnapshot().lists ?? {}; + const customLists = docDataModel.getSnapshot().lists ?? {}; const lists = { ...PRESET_LIST_TYPE, ...customLists, }; - const { charSpace, defaultTabStop = 36, gridType } = dataModel.getSnapshot().documentStyle; + const { charSpace, defaultTabStop = 36, gridType } = docDataModel.getSnapshot().documentStyle; for (const paragraph of currentParagraphs) { const { startIndex, paragraphStyle = {} } = paragraph; @@ -170,7 +171,8 @@ export const ListOperationCommand: ICommand = { memoryCursor.moveCursorTo(startIndex + 1); } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/commands/paragraph-align.command.ts b/packages/docs/src/commands/commands/paragraph-align.command.ts index 3dd9b7090de..74882bdeb54 100644 --- a/packages/docs/src/commands/commands/paragraph-align.command.ts +++ b/packages/docs/src/commands/commands/paragraph-align.command.ts @@ -29,6 +29,7 @@ import { import { serializeTextRange, TextSelectionManagerService } from '../../services/text-selection-manager.service'; import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation'; import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; +import { getRichTextEditPath } from '../util'; import { getParagraphsInRange } from './list.command'; interface IAlignOperationCommandParams { @@ -48,19 +49,19 @@ export const AlignOperationCommand: ICommand = { const { alignType } = params; - const dataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!dataModel) return false; + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + if (!docDataModel) return false; const activeRange = textSelectionManagerService.getActiveRange(); const selections = textSelectionManagerService.getSelections() ?? []; - const paragraphs = dataModel.getBody()?.paragraphs; + const paragraphs = docDataModel.getBody()?.paragraphs; const serializedSelections = selections.map(serializeTextRange); if (activeRange == null || paragraphs == null) return false; const currentParagraphs = getParagraphsInRange(activeRange, paragraphs); const { segmentId } = activeRange; - const unitId = dataModel.getUnitId(); + const unitId = docDataModel.getUnitId(); const isAlreadyAligned = currentParagraphs.every((paragraph) => paragraph.paragraphStyle?.horizontalAlign === alignType); const doMutation: IMutationInfo = { @@ -113,7 +114,8 @@ export const AlignOperationCommand: ICommand = { memoryCursor.moveCursorTo(startIndex + 1); } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); const result = commandService.syncExecuteCommand< IRichTextEditingMutationParams, diff --git a/packages/docs/src/commands/commands/replace-content.command.ts b/packages/docs/src/commands/commands/replace-content.command.ts index f08af6d387e..c9664307006 100644 --- a/packages/docs/src/commands/commands/replace-content.command.ts +++ b/packages/docs/src/commands/commands/replace-content.command.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import type { ICommand, IDocumentBody, IMutationInfo } from '@univerjs/core'; +import type { DocumentDataModel, ICommand, IDocumentBody, IMutationInfo } from '@univerjs/core'; import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, JSONX, TextX, TextXActionType } from '@univerjs/core'; import type { ITextRangeWithStyle } from '@univerjs/engine-render'; import { TextSelectionManagerService } from '../../services/text-selection-manager.service'; import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation'; import { RichTextEditingMutation } from '../mutations/core-editing.mutation'; +import { getRichTextEditPath } from '../util'; interface IReplaceContentCommandParams { unitId: string; @@ -41,10 +42,11 @@ export const ReplaceContentCommand: ICommand = { const commandService = accessor.get(ICommandService); const textSelectionManagerService = accessor.get(TextSelectionManagerService); - const prevBody = univerInstanceService.getUniverDocInstance(unitId)?.getSnapshot().body; + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + const prevBody = docDataModel?.getSnapshot().body; const selections = textSelectionManagerService.getSelections(); - if (prevBody == null) { + if (docDataModel == null || prevBody == null) { return false; } @@ -52,7 +54,7 @@ export const ReplaceContentCommand: ICommand = { return false; } - const doMutation = getMutationParams(unitId, segmentId, prevBody, body); + const doMutation = getMutationParams(unitId, segmentId, docDataModel, prevBody, body); doMutation.params.textRanges = textRanges; @@ -83,13 +85,15 @@ export const CoverContentCommand: ICommand = { const commandService = accessor.get(ICommandService); const undoRedoService = accessor.get(IUndoRedoService); - const prevBody = univerInstanceService.getUniverDocInstance(unitId)?.getSnapshot().body; + const docDatModel = univerInstanceService.getUniverDocInstance(unitId); - if (prevBody == null) { + const prevBody = docDatModel?.getSnapshot().body; + + if (docDatModel == null || prevBody == null) { return false; } - const doMutation = getMutationParams(unitId, segmentId, prevBody, body); + const doMutation = getMutationParams(unitId, segmentId, docDatModel, prevBody, body); // No need to set the cursor or selection. doMutation.params.noNeedSetTextRange = true; @@ -106,7 +110,7 @@ export const CoverContentCommand: ICommand = { }, }; -function getMutationParams(unitId: string, segmentId: string, prevBody: IDocumentBody, body: IDocumentBody) { +function getMutationParams(unitId: string, segmentId: string, docDatModel: DocumentDataModel, prevBody: IDocumentBody, body: IDocumentBody) { const doMutation: IMutationInfo = { id: RichTextEditingMutation.id, params: { @@ -139,7 +143,8 @@ function getMutationParams(unitId: string, segmentId: string, prevBody: IDocumen }); } - doMutation.params.actions = jsonX.editOp(textX.serialize()); + const path = getRichTextEditPath(docDatModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); return doMutation; } From 2e34a0ef7121d95d8800b43ddc8b7737ab54b61d Mon Sep 17 00:00:00 2001 From: jocs Date: Wed, 26 Jun 2024 16:43:14 +0800 Subject: [PATCH 10/39] feat: inline format in header --- .../src/data/docs/default-document-data-cn.ts | 12 + packages/docs-ui/src/controllers/menu/menu.ts | 10 +- .../commands/commands/break-line.command.ts | 2 +- .../src/commands/commands/delete.command.ts | 220 +++++++++--------- .../commands/inline-format.command.ts | 17 +- .../src/commands/commands/list.command.ts | 12 +- .../commands/paragraph-align.command.ts | 13 +- .../docs/text-selection/text-range.ts | 2 +- .../text-selection-render-manager.ts | 2 +- 9 files changed, 151 insertions(+), 139 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index ca6db3bf6a9..823a3320156 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -53,15 +53,27 @@ function getDefaultHeaderFooterBody(type: 'header' | 'footer') { paragraphs: [ { startIndex: 4, + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, }, { startIndex: 11, + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, }, { startIndex: 24, + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, }, { startIndex: 31, + spaceAbove: 10, + lineSpacing: 2, + spaceBelow: 0, }, ], sectionBreaks: [ diff --git a/packages/docs-ui/src/controllers/menu/menu.ts b/packages/docs-ui/src/controllers/menu/menu.ts index 31a644dbade..14f2c2333d8 100644 --- a/packages/docs-ui/src/controllers/menu/menu.ts +++ b/packages/docs-ui/src/controllers/menu/menu.ts @@ -614,21 +614,21 @@ export function BackgroundColorSelectorMenuItemFactory(accessor: IAccessor): IMe function getFontStyleAtCursor(accessor: IAccessor) { const univerInstanceService = accessor.get(IUniverInstanceService); const textSelectionService = accessor.get(TextSelectionManagerService); - const editorDataModel = univerInstanceService.getCurrentUniverDocInstance(); + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); const activeTextRange = textSelectionService.getActiveRange(); - if (editorDataModel == null || activeTextRange == null) { + if (docDataModel == null || activeTextRange == null) { return; } - const textRuns = editorDataModel.getBody()?.textRuns; + const { startOffset, segmentId } = activeTextRange; + + const textRuns = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.textRuns; if (textRuns == null) { return; } - const { startOffset } = activeTextRange; - let textRun; for (let i = 0; i < textRuns.length; i++) { diff --git a/packages/docs/src/commands/commands/break-line.command.ts b/packages/docs/src/commands/commands/break-line.command.ts index 19b1a9adf29..28689d2609c 100644 --- a/packages/docs/src/commands/commands/break-line.command.ts +++ b/packages/docs/src/commands/commands/break-line.command.ts @@ -72,7 +72,7 @@ export const BreakLineCommand: ICommand = { const unitId = docDataModel.getUnitId(); const { startOffset, segmentId } = activeRange; - const paragraphs = docDataModel.getBody()?.paragraphs ?? []; + const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs ?? []; const prevParagraph = paragraphs.find((p) => p.startIndex >= startOffset); // split paragraph into two. diff --git a/packages/docs/src/commands/commands/delete.command.ts b/packages/docs/src/commands/commands/delete.command.ts index cdb83668acd..7501207e904 100644 --- a/packages/docs/src/commands/commands/delete.command.ts +++ b/packages/docs/src/commands/commands/delete.command.ts @@ -36,6 +36,117 @@ import { getCommandSkeleton, getRichTextEditPath } from '../util'; import { CutContentCommand } from './clipboard.inner.command'; import { DeleteCommand, DeleteDirection, UpdateCommand } from './core-editing.command'; +interface IMergeTwoParagraphParams { + direction: DeleteDirection; + range: IActiveTextRange; +} + +export const MergeTwoParagraphCommand: ICommand = { + id: 'doc.command.merge-two-paragraph', + + type: CommandType.COMMAND, + + // eslint-disable-next-line max-lines-per-function + handler: async (accessor, params: IMergeTwoParagraphParams) => { + const textSelectionManagerService = accessor.get(TextSelectionManagerService); + const univerInstanceService = accessor.get(IUniverInstanceService); + const commandService = accessor.get(ICommandService); + + const { direction, range } = params; + + const activeRange = textSelectionManagerService.getActiveRange(); + + if (activeRange == null) { + return false; + } + + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + if (docDataModel == null) { + return false; + } + const unitId = docDataModel.getUnitId(); + const { startOffset, collapsed, segmentId, style } = activeRange; + + if (!collapsed) { + return false; + } + + const segmentDataModel = docDataModel.getSelfOrHeaderFooterModel(segmentId); + const documentBody = segmentDataModel?.getBody(); + + if (documentBody == null) { + return false; + } + + const startIndex = direction === DeleteDirection.LEFT ? startOffset : startOffset + 1; + + const endIndex = (documentBody.paragraphs ?? []).find((p) => p.startIndex >= startIndex)!.startIndex; + const body = getParagraphBody(documentBody, startIndex, endIndex); + + const cursor = direction === DeleteDirection.LEFT ? startOffset - 1 : startOffset; + + const textRanges = [ + { + startOffset: cursor, + endOffset: cursor, + style, + }, + ] as ITextRangeWithStyle[]; + + const doMutation: IMutationInfo = { + id: RichTextEditingMutation.id, + params: { + unitId, + actions: [], + textRanges, + prevTextRanges: [range], + }, + }; + + const textX = new TextX(); + const jsonX = JSONX.getInstance(); + + textX.push({ + t: TextXActionType.RETAIN, + len: direction === DeleteDirection.LEFT ? startOffset - 1 : startOffset, + segmentId, + }); + + if (body.dataStream.length) { + textX.push({ + t: TextXActionType.INSERT, + body, + len: body.dataStream.length, + line: 0, + segmentId, + }); + } + + textX.push({ + t: TextXActionType.RETAIN, + len: 1, + segmentId, + }); + + textX.push({ + t: TextXActionType.DELETE, + len: endIndex + 1 - startIndex, + line: 0, + segmentId, + }); + + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); + + const result = commandService.syncExecuteCommand< + IRichTextEditingMutationParams, + IRichTextEditingMutationParams + >(doMutation.id, doMutation.params); + + return Boolean(result); + }, +}; + // Handle BACKSPACE key. export const DeleteLeftCommand: ICommand = { id: 'doc.command.delete-left', @@ -265,115 +376,6 @@ export const DeleteRightCommand: ICommand = { }, }; -interface IMergeTwoParagraphParams { - direction: DeleteDirection; - range: IActiveTextRange; -} - -export const MergeTwoParagraphCommand: ICommand = { - id: 'doc.command.merge-two-paragraph', - - type: CommandType.COMMAND, - - // eslint-disable-next-line max-lines-per-function - handler: async (accessor, params: IMergeTwoParagraphParams) => { - const textSelectionManagerService = accessor.get(TextSelectionManagerService); - const univerInstanceService = accessor.get(IUniverInstanceService); - const commandService = accessor.get(ICommandService); - - const { direction, range } = params; - - const activeRange = textSelectionManagerService.getActiveRange(); - const ranges = textSelectionManagerService.getSelections(); - - if (activeRange == null || ranges == null) { - return false; - } - - const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (docDataModel == null) { - return false; - } - - const { startOffset, collapsed, segmentId, style } = activeRange; - - if (!collapsed) { - return false; - } - - const startIndex = direction === DeleteDirection.LEFT ? startOffset : startOffset + 1; - const endIndex = docDataModel - .getBody() - ?.paragraphs - ?.find((p) => p.startIndex >= startIndex)?.startIndex!; - const body = getParagraphBody(docDataModel.getBody()!, startIndex, endIndex); - - const cursor = direction === DeleteDirection.LEFT ? startOffset - 1 : startOffset; - - const unitId = docDataModel.getUnitId(); - - const textRanges = [ - { - startOffset: cursor, - endOffset: cursor, - style, - }, - ] as ITextRangeWithStyle[]; - - const doMutation: IMutationInfo = { - id: RichTextEditingMutation.id, - params: { - unitId, - actions: [], - textRanges, - prevTextRanges: [range], - }, - }; - - const textX = new TextX(); - const jsonX = JSONX.getInstance(); - - textX.push({ - t: TextXActionType.RETAIN, - len: direction === DeleteDirection.LEFT ? startOffset - 1 : startOffset, - segmentId, - }); - - if (body.dataStream.length) { - textX.push({ - t: TextXActionType.INSERT, - body, - len: body.dataStream.length, - line: 0, - segmentId, - }); - } - - textX.push({ - t: TextXActionType.RETAIN, - len: 1, - segmentId, - }); - - textX.push({ - t: TextXActionType.DELETE, - len: endIndex + 1 - startIndex, - line: 0, - segmentId, - }); - - const path = getRichTextEditPath(docDataModel, segmentId); - doMutation.params.actions = jsonX.editOp(textX.serialize(), path); - - const result = commandService.syncExecuteCommand< - IRichTextEditingMutationParams, - IRichTextEditingMutationParams - >(doMutation.id, doMutation.params); - - return Boolean(result); - }, -}; - function getParagraphBody(body: IDocumentBody, startIndex: number, endIndex: number): IDocumentBody { const { textRuns: originTextRuns } = body; const dataStream = body.dataStream.substring(startIndex, endIndex); diff --git a/packages/docs/src/commands/commands/inline-format.command.ts b/packages/docs/src/commands/commands/inline-format.command.ts index a7e08508d0d..d596c327f5e 100644 --- a/packages/docs/src/commands/commands/inline-format.command.ts +++ b/packages/docs/src/commands/commands/inline-format.command.ts @@ -18,15 +18,10 @@ import type { ICommand, IDocumentBody, IMutationInfo, IStyleBase, ITextDecoration, ITextRun, } from '@univerjs/core'; import { - BaselineOffset, - BooleanNumber, - CommandType, - ICommandService, - IUniverInstanceService, - JSONX, - MemoryCursor, - TextX, - TextXActionType, + BaselineOffset, BooleanNumber, CommandType, + ICommandService, IUniverInstanceService, + JSONX, MemoryCursor, + TextX, TextXActionType, } from '@univerjs/core'; import type { TextRange } from '@univerjs/engine-render'; import { serializeTextRange, TextSelectionManagerService } from '../../services/text-selection-manager.service'; @@ -278,7 +273,7 @@ export const SetInlineFormatCommand: ICommand = { } const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!docDataModel) { + if (docDataModel == null) { return false; } @@ -294,7 +289,7 @@ export const SetInlineFormatCommand: ICommand = { case SetInlineFormatSubscriptCommand.id: // fallthrough case SetInlineFormatSuperscriptCommand.id: { formatValue = getReverseFormatValueInSelection( - docDataModel.getBody()!.textRuns!, + docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()!.textRuns!, preCommandId, selections ); diff --git a/packages/docs/src/commands/commands/list.command.ts b/packages/docs/src/commands/commands/list.command.ts index de51d8e89f2..c718ba98201 100644 --- a/packages/docs/src/commands/commands/list.command.ts +++ b/packages/docs/src/commands/commands/list.command.ts @@ -53,23 +53,23 @@ export const ListOperationCommand: ICommand = { const { listType } = params; const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!docDataModel) { + const activeRange = textSelectionManagerService.getActiveRange(); + if (docDataModel == null || activeRange == null) { return false; } - const activeRange = textSelectionManagerService.getActiveRange(); + const { segmentId } = activeRange; + const selections = textSelectionManagerService.getSelections() ?? []; - const paragraphs = docDataModel.getBody()?.paragraphs; + const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs; const serializedSelections = selections.map(serializeTextRange); - if (activeRange == null || paragraphs == null) { + if (paragraphs == null) { return false; } const currentParagraphs = getParagraphsInRange(activeRange, paragraphs); - const { segmentId } = activeRange; - const unitId = docDataModel.getUnitId(); const isAlreadyList = currentParagraphs.every((paragraph) => paragraph.bullet?.listType === listType); diff --git a/packages/docs/src/commands/commands/paragraph-align.command.ts b/packages/docs/src/commands/commands/paragraph-align.command.ts index 74882bdeb54..91391467422 100644 --- a/packages/docs/src/commands/commands/paragraph-align.command.ts +++ b/packages/docs/src/commands/commands/paragraph-align.command.ts @@ -50,17 +50,20 @@ export const AlignOperationCommand: ICommand = { const { alignType } = params; const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - if (!docDataModel) return false; - const activeRange = textSelectionManagerService.getActiveRange(); + if (docDataModel == null || activeRange == null) { + return false; + } + const { segmentId } = activeRange; const selections = textSelectionManagerService.getSelections() ?? []; - const paragraphs = docDataModel.getBody()?.paragraphs; + const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs; const serializedSelections = selections.map(serializeTextRange); - if (activeRange == null || paragraphs == null) return false; + if (paragraphs == null) { + return false; + } const currentParagraphs = getParagraphsInRange(activeRange, paragraphs); - const { segmentId } = activeRange; const unitId = docDataModel.getUnitId(); const isAlreadyAligned = currentParagraphs.every((paragraph) => paragraph.paragraphStyle?.horizontalAlign === alignType); diff --git a/packages/engine-render/src/components/docs/text-selection/text-range.ts b/packages/engine-render/src/components/docs/text-selection/text-range.ts index d775454f961..61d95e6ad60 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-range.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-range.ts @@ -380,7 +380,7 @@ export class TextRange { return; } - const OPACITY = 0.2; + const OPACITY = 0.3; const polygon = new RegularPolygon(TEXT_RANGE_KEY_PREFIX + Tools.generateRandomId(ID_LENGTH), { pointsGroup, fill: this.style?.fill || getColor(COLORS.black, OPACITY), diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index 9f0dbb16b09..6bd94a79757 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -543,7 +543,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel strokeWidth: 1.5, stroke: 'rgba(0, 0, 0, 0)', strokeActive: 'rgba(0, 0, 0, 1)', - fill: `rgba(${r}, ${g}, ${b}, ${a ?? 0.2})`, + fill: `rgba(${r}, ${g}, ${b}, ${a ?? 0.3})`, }; this.setStyle(style); From b173927c1911fccaac58809d97e32f0ef7a1f4b5 Mon Sep 17 00:00:00 2001 From: jocs Date: Wed, 26 Jun 2024 22:28:41 +0800 Subject: [PATCH 11/39] feat: edit in header and footer --- .../src/data/docs/default-document-data-cn.ts | 16 ++--- .../src/types/interfaces/i-selection-data.ts | 1 + .../doc-header-footer.controller.ts | 5 +- packages/docs-ui/src/controllers/menu/menu.ts | 10 +-- .../text-selection.render-controller.ts | 47 +++++++++++++- .../src/views/header-footer/text-bubble.ts | 4 +- .../commands/commands/core-editing.command.ts | 2 - .../src/commands/commands/delete.command.ts | 21 +++---- .../mutations/core-editing.mutation.ts | 1 + .../src/controllers/move-cursor.controller.ts | 18 +++--- .../controllers/normal-input.controller.ts | 15 ++++- .../text-selection-manager.service.ts | 13 ++-- .../engine-render/src/basics/interfaces.ts | 1 + packages/engine-render/src/basics/range.ts | 2 +- .../layout/block/paragraph/layout-ruler.ts | 1 + .../components/docs/layout/doc-skeleton.ts | 62 +++++++++++++------ .../src/components/docs/layout/model/page.ts | 6 +- .../src/components/docs/liquid.ts | 2 +- .../docs/text-selection/convert-cursor.ts | 37 +++++++---- .../docs/text-selection/text-range.ts | 12 ++-- .../text-selection-render-manager.ts | 37 +++++++++-- 21 files changed, 217 insertions(+), 96 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index 823a3320156..70a0e3a8a4e 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -53,26 +53,26 @@ function getDefaultHeaderFooterBody(type: 'header' | 'footer') { paragraphs: [ { startIndex: 4, - spaceAbove: 10, - lineSpacing: 2, + spaceAbove: 0, + lineSpacing: 1.5, spaceBelow: 0, }, { startIndex: 11, - spaceAbove: 10, - lineSpacing: 2, + spaceAbove: 0, + lineSpacing: 1.5, spaceBelow: 0, }, { startIndex: 24, - spaceAbove: 10, - lineSpacing: 2, + spaceAbove: 0, + lineSpacing: 1.5, spaceBelow: 0, }, { startIndex: 31, - spaceAbove: 10, - lineSpacing: 2, + spaceAbove: 0, + lineSpacing: 1.5, spaceBelow: 0, }, ], diff --git a/packages/core/src/types/interfaces/i-selection-data.ts b/packages/core/src/types/interfaces/i-selection-data.ts index c6dcc9deb4c..8909b487de7 100644 --- a/packages/core/src/types/interfaces/i-selection-data.ts +++ b/packages/core/src/types/interfaces/i-selection-data.ts @@ -74,6 +74,7 @@ export interface ITextRange extends ITextRangeStart { export interface ITextRangeParam extends ITextRange { segmentId?: string; //The ID of the header, footer or footnote the location is in. An empty segment ID signifies the document's body. + segmentPage?: number; //The page number of the header, footer or footnote the location is in. An empty segment ID signifies the document's body. isActive?: boolean; // Whether the text range is active or current range. } diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index e9addcdcb22..5f2183c388f 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -25,8 +25,8 @@ import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; -const HEADER_FOOTER_STROKE_COLOR = 'rgb(0, 0, 255)'; -const HEADER_FOOTER_FILL_COLOR = 'rgb(219, 231, 244)'; +const HEADER_FOOTER_STROKE_COLOR = 'rgba(58, 96, 247, 1)'; +const HEADER_FOOTER_FILL_COLOR = 'rgba(58, 96, 247, 0.08)'; export enum HeaderFooterType { FIRST_PAGE_HEADER, @@ -196,6 +196,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu if (editArea === DocumentEditArea.BODY) { this._textSelectionRenderManager.setSegment(''); + this._textSelectionRenderManager.setSegmentPage(pageNumber); } else { if (createType != null) { // TODO: create header or footer and set segment. diff --git a/packages/docs-ui/src/controllers/menu/menu.ts b/packages/docs-ui/src/controllers/menu/menu.ts index 14f2c2333d8..b68c9398f61 100644 --- a/packages/docs-ui/src/controllers/menu/menu.ts +++ b/packages/docs-ui/src/controllers/menu/menu.ts @@ -652,21 +652,21 @@ function getFontStyleAtCursor(accessor: IAccessor) { function getParagraphStyleAtCursor(accessor: IAccessor) { const univerInstanceService = accessor.get(IUniverInstanceService); const textSelectionService = accessor.get(TextSelectionManagerService); - const editorDataModel = univerInstanceService.getCurrentUniverDocInstance(); + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); const activeTextRange = textSelectionService.getActiveRange(); - if (editorDataModel == null || activeTextRange == null) { + if (docDataModel == null || activeTextRange == null) { return; } - const paragraphs = editorDataModel.getBody()?.paragraphs; + const { startOffset, segmentId } = activeTextRange; + + const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs; if (paragraphs == null) { return; } - const { startOffset } = activeTextRange; - let prevIndex = -1; for (const paragraph of paragraphs) { diff --git a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts index f5b8fced577..fcecb3414ba 100644 --- a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts @@ -17,7 +17,7 @@ import type { DocumentDataModel, ICommandInfo } from '@univerjs/core'; import { Disposable, ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import type { Documents, IMouseEvent, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; -import { CURSOR_TYPE, ITextSelectionRenderManager } from '@univerjs/engine-render'; +import { CURSOR_TYPE, DocumentEditArea, ITextSelectionRenderManager, PageLayoutType, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; import { IEditorService } from '@univerjs/ui'; @@ -60,6 +60,7 @@ export class DocTextSelectionRenderController extends Disposable implements IRen } } + // eslint-disable-next-line max-lines-per-function private _initialMain(unitId: string) { const docObject = neoGetDocObject(this._context); const { document, scene } = docObject; @@ -81,11 +82,36 @@ export class DocTextSelectionRenderController extends Disposable implements IRen } // FIXME:@Jocs: editor status should not be coupled with the instance service. - const currentDocInstance = this._instanceSrv.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); - if (currentDocInstance?.getUnitId() !== unitId) { + const docDataModel = this._instanceSrv.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); + if (docDataModel?.getUnitId() !== unitId) { this._instanceSrv.setCurrentUnitForType(unitId); } + const skeleton = this._docSkeletonManagerService.getSkeleton(); + const { offsetX, offsetY } = evt; + const coord = this._getTransformCoordForDocumentOffset(offsetX, offsetY); + + if (coord != null) { + const { + pageLayoutType = PageLayoutType.VERTICAL, + pageMarginLeft, + pageMarginTop, + } = document.getOffsetConfig(); + const { editArea } = skeleton.findEditAreaByCoord( + coord, + pageLayoutType, + pageMarginLeft, + pageMarginTop + ); + + const viewModel = this._docSkeletonManagerService.getViewModel(); + const preEditArea = viewModel.getEditArea(); + + if (preEditArea !== DocumentEditArea.BODY && editArea !== preEditArea) { + viewModel.setEditArea(editArea); + } + } + this._textSelectionRenderManager.eventTrigger(evt); if (this._editorService.getEditor(unitId)) { @@ -128,6 +154,21 @@ export class DocTextSelectionRenderController extends Disposable implements IRen })); } + private _getTransformCoordForDocumentOffset(evtOffsetX: number, evtOffsetY: number) { + const docObject = neoGetDocObject(this._context); + const { document, scene } = docObject; + const { documentTransform } = document.getOffsetConfig(); + const activeViewport = scene.getViewports()[0]; + + if (activeViewport == null) { + return; + } + + const originCoord = activeViewport.getRelativeVector(Vector2.FromArray([evtOffsetX, evtOffsetY])); + + return documentTransform.clone().invert().applyPoint(originCoord); + } + private _isEditorReadOnly(unitId: string) { const editor = this._editorService.getEditor(unitId); if (!editor) { diff --git a/packages/docs-ui/src/views/header-footer/text-bubble.ts b/packages/docs-ui/src/views/header-footer/text-bubble.ts index 64d4db05040..689d26ca17f 100644 --- a/packages/docs-ui/src/views/header-footer/text-bubble.ts +++ b/packages/docs-ui/src/views/header-footer/text-bubble.ts @@ -81,7 +81,7 @@ export class TextBubbleShape< const { text, color } = props; ctx.save(); // Measure the text width - ctx.font = 'bold 13px Source Han Sans CN'; + ctx.font = '13px Source Han Sans CN'; const textWidth = ctx.measureText(text).width; const realInfoWidth = Math.min( textWidth + 2 * COLLAB_CURSOR_LABEL_TEXT_PADDING_LR, @@ -97,7 +97,7 @@ export class TextBubbleShape< evented: false, }); - ctx.fillStyle = 'rgb(0, 0, 255)'; + ctx.fillStyle = 'rgba(58, 96, 247, 1)'; // Draw the text with truncation if needed const offsetX = COLLAB_CURSOR_LABEL_TEXT_PADDING_LR; diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index 77cef79f2dd..8d669c5350b 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -48,9 +48,7 @@ export const EditorInsertTextCommandId = 'doc.command.insert-text'; */ export const InsertCommand: ICommand = { id: EditorInsertTextCommandId, - type: CommandType.COMMAND, - handler: async (accessor, params: IInsertCommandParams) => { const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); diff --git a/packages/docs/src/commands/commands/delete.command.ts b/packages/docs/src/commands/commands/delete.command.ts index 7501207e904..4572bee4054 100644 --- a/packages/docs/src/commands/commands/delete.command.ts +++ b/packages/docs/src/commands/commands/delete.command.ts @@ -177,24 +177,24 @@ export const DeleteLeftCommand: ICommand = { const actualRange = getDeleteSelection(activeRange, body); const { startOffset, collapsed } = actualRange; - const { segmentId, style } = activeRange; - const curGlyph = skeleton.findNodeByCharIndex(startOffset, segmentId); + const { segmentId, style, segmentPage } = activeRange; + const curGlyph = skeleton.findNodeByCharIndex(startOffset, segmentId, segmentPage); // is in bullet list? const isBullet = hasListGlyph(curGlyph); // is in indented paragraph? - const isIndent = isIndentByGlyph(curGlyph, docDataModel.getBody()); + const isIndent = isIndentByGlyph(curGlyph, docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()); let cursor = startOffset; // Get the deleted glyph. It maybe null or undefined when the curGlyph is first glyph in skeleton. - const preGlyph = skeleton.findNodeByCharIndex(startOffset - 1, segmentId); + const preGlyph = skeleton.findNodeByCharIndex(startOffset - 1, segmentId, segmentPage); const isUpdateParagraph = isFirstGlyph(curGlyph) && preGlyph !== curGlyph && (isBullet === true || isIndent === true); if (isUpdateParagraph && collapsed) { - const paragraph = getParagraphByGlyph(curGlyph, docDataModel.getBody()); + const paragraph = getParagraphByGlyph(curGlyph, docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()); if (paragraph == null) { return false; @@ -323,18 +323,17 @@ export const DeleteRightCommand: ICommand = { const actualRange = getInsertSelection(activeRange, body); const { startOffset, collapsed } = actualRange; - const { segmentId, style } = activeRange; + const { segmentId, style, segmentPage } = activeRange; // No need to delete when the cursor is at the last position of the last paragraph. - if (startOffset === docDataModel.getBody()!.dataStream.length - 2 && collapsed) { + if (startOffset === docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()!.dataStream.length - 2 && collapsed) { return true; } let result: boolean = false; if (collapsed === true) { - const needDeleteSpan = skeleton.findNodeByCharIndex(startOffset, segmentId)!; + const needDeleteGlyph = skeleton.findNodeByCharIndex(startOffset, segmentId, segmentPage)!; - // skip custom-range-split-symbol - if (needDeleteSpan.content === '\r') { + if (needDeleteGlyph.content === '\r') { result = await commandService.executeCommand(MergeTwoParagraphCommand.id, { direction: DeleteDirection.RIGHT, range: activeRange, @@ -358,7 +357,7 @@ export const DeleteRightCommand: ICommand = { segmentId, direction: DeleteDirection.RIGHT, textRanges, - len: needDeleteSpan.count, + len: needDeleteGlyph.count, }); } } else { diff --git a/packages/docs/src/commands/mutations/core-editing.mutation.ts b/packages/docs/src/commands/mutations/core-editing.mutation.ts index 9039f0f2f0f..234e7b6e0b0 100644 --- a/packages/docs/src/commands/mutations/core-editing.mutation.ts +++ b/packages/docs/src/commands/mutations/core-editing.mutation.ts @@ -96,6 +96,7 @@ export const RichTextEditingMutation: IMutation 1) { @@ -141,8 +141,8 @@ export class MoveCursorController extends Disposable { const dataStreamLength = docDataModel.getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; if (direction === Direction.LEFT || direction === Direction.RIGHT) { - const preGlyph = skeleton.findNodeByCharIndex(focusOffset - 1, segmentId); - const curGlyph = skeleton.findNodeByCharIndex(focusOffset, segmentId)!; + const preGlyph = skeleton.findNodeByCharIndex(focusOffset - 1, segmentId, segmentPage); + const curGlyph = skeleton.findNodeByCharIndex(focusOffset, segmentId, segmentPage)!; focusOffset = direction === Direction.RIGHT ? focusOffset + curGlyph.count : focusOffset - (preGlyph?.count ?? 0); @@ -157,7 +157,7 @@ export class MoveCursorController extends Disposable { }, ], false); } else { - const focusGlyph = skeleton.findNodeByCharIndex(focusOffset, segmentId); + const focusGlyph = skeleton.findNodeByCharIndex(focusOffset, segmentId, segmentPage); const documentOffsetConfig = docObject.document.getOffsetConfig(); const focusNodePosition = collapsed ? startNodePosition : rangeDirection === RANGE_DIRECTION.FORWARD ? endNodePosition : startNodePosition; @@ -215,7 +215,7 @@ export class MoveCursorController extends Disposable { return; } - const { startOffset, endOffset, style, collapsed, segmentId, startNodePosition, endNodePosition } = activeRange; + const { startOffset, endOffset, style, collapsed, segmentId, startNodePosition, endNodePosition, segmentPage } = activeRange; const dataStreamLength = docDataModel.getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; @@ -233,8 +233,8 @@ export class MoveCursorController extends Disposable { cursor = direction === Direction.LEFT ? min : max; } else { - const preSpan = skeleton.findNodeByCharIndex(startOffset - 1, segmentId); - const curSpan = skeleton.findNodeByCharIndex(startOffset, segmentId)!; + const preSpan = skeleton.findNodeByCharIndex(startOffset - 1, segmentId, segmentPage); + const curSpan = skeleton.findNodeByCharIndex(startOffset, segmentId, segmentPage)!; if (direction === Direction.LEFT) { cursor = Math.max(0, startOffset - (preSpan?.count ?? 0)); @@ -252,8 +252,8 @@ export class MoveCursorController extends Disposable { }, ], false); } else { - const startNode = skeleton.findNodeByCharIndex(startOffset, segmentId); - const endNode = skeleton.findNodeByCharIndex(endOffset, segmentId); + const startNode = skeleton.findNodeByCharIndex(startOffset, segmentId, segmentPage); + const endNode = skeleton.findNodeByCharIndex(endOffset, segmentId, segmentPage); const documentOffsetConfig = docObject.document.getOffsetConfig(); diff --git a/packages/docs/src/controllers/normal-input.controller.ts b/packages/docs/src/controllers/normal-input.controller.ts index b9fdc6e4b20..41c9db1e45f 100644 --- a/packages/docs/src/controllers/normal-input.controller.ts +++ b/packages/docs/src/controllers/normal-input.controller.ts @@ -75,13 +75,26 @@ export class NormalInputController extends Disposable { return; } - const { segmentId } = activeRange; + const { startOffset, segmentId, style, segmentPage } = activeRange; + + const len = content.length; + + const textRanges = [ + { + startOffset: startOffset + len, + endOffset: startOffset + len, + segmentId, + segmentPage, + style, + }, + ]; await this._commandService.executeCommand(InsertCommand.id, { unitId, body: { dataStream: content, }, + textRanges, range: activeRange, segmentId, }); diff --git a/packages/docs/src/services/text-selection-manager.service.ts b/packages/docs/src/services/text-selection-manager.service.ts index e82f202318f..9074fad2222 100644 --- a/packages/docs/src/services/text-selection-manager.service.ts +++ b/packages/docs/src/services/text-selection-manager.service.ts @@ -44,6 +44,7 @@ export interface ITextActiveRange { direction: RANGE_DIRECTION; segmentId: string; style: ITextSelectionStyle; + segmentPage: number; } interface ITextSelectionManagerInsertParam extends ITextSelectionManagerSearchParam, ITextSelectionInnerParam {} @@ -137,7 +138,7 @@ export class TextSelectionManagerService extends RxDisposable { return; } - const { textRanges, segmentId, style } = selectionInfo; + const { textRanges, segmentId, style, segmentPage } = selectionInfo; const activeTextRange = textRanges.find((textRange) => textRange.isActive()); if (activeTextRange == null) { @@ -158,6 +159,7 @@ export class TextSelectionManagerService extends RxDisposable { endNodePosition, direction, segmentId, + segmentPage, style, }; } @@ -172,6 +174,7 @@ export class TextSelectionManagerService extends RxDisposable { ...this._currentSelection, textRanges: textRanges as TextRange[], segmentId: '', + segmentPage: -1, isEditing, style: NORMAL_TEXT_SELECTION_PLUGIN_STYLE, // mock style. }); @@ -256,7 +259,7 @@ export class TextSelectionManagerService extends RxDisposable { } private _replaceByParam(insertParam: ITextSelectionManagerInsertParam) { - const { unitId, subUnitId, style, segmentId, textRanges, isEditing } = insertParam; + const { unitId, subUnitId, style, segmentId, textRanges, isEditing, segmentPage } = insertParam; if (!this._textSelectionInfo.has(unitId)) { this._textSelectionInfo.set(unitId, new Map()); @@ -264,11 +267,11 @@ export class TextSelectionManagerService extends RxDisposable { const unitTextRange = this._textSelectionInfo.get(unitId)!; - unitTextRange.set(subUnitId, { textRanges, style, segmentId, isEditing }); + unitTextRange.set(subUnitId, { textRanges, style, segmentId, isEditing, segmentPage }); } private _addByParam(insertParam: ITextSelectionManagerInsertParam): void { - const { unitId, subUnitId, textRanges, style, segmentId, isEditing } = insertParam; + const { unitId, subUnitId, textRanges, style, segmentId, isEditing, segmentPage } = insertParam; if (!this._textSelectionInfo.has(unitId)) { this._textSelectionInfo.set(unitId, new Map()); @@ -277,7 +280,7 @@ export class TextSelectionManagerService extends RxDisposable { const unitTextRange = this._textSelectionInfo.get(unitId)!; if (!unitTextRange.has(subUnitId)) { - unitTextRange.set(subUnitId, { textRanges, style, segmentId, isEditing }); + unitTextRange.set(subUnitId, { textRanges, style, segmentId, isEditing, segmentPage }); } else { const OldTextRanges = unitTextRange.get(subUnitId)!; OldTextRanges.textRanges.push(...textRanges); diff --git a/packages/engine-render/src/basics/interfaces.ts b/packages/engine-render/src/basics/interfaces.ts index 4c752d2d9f2..5769760adfc 100644 --- a/packages/engine-render/src/basics/interfaces.ts +++ b/packages/engine-render/src/basics/interfaces.ts @@ -144,6 +144,7 @@ export interface INodeInfo { node: IDocumentSkeletonGlyph; ratioX: number; ratioY: number; + segmentId: string; segmentPage: number; // The index of the page where node is located. } diff --git a/packages/engine-render/src/basics/range.ts b/packages/engine-render/src/basics/range.ts index e1898bf7865..47cb9ef0a49 100644 --- a/packages/engine-render/src/basics/range.ts +++ b/packages/engine-render/src/basics/range.ts @@ -39,7 +39,7 @@ export interface ISuccinctTextRangeParam { startOffset: number; endOffset: number; segmentId?: string; // Header of footer id. - pageIndex?: number; // Optional, because header and footer are in different pages, so need pageIndex to allocate selection in header or footer. + segmentPage?: number; // Optional, because header and footer are in different pages, so need pageIndex to allocate selection in header or footer. style?: ITextSelectionStyle; } diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts index ce2564f9548..0d07ccf42ad 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts @@ -491,6 +491,7 @@ function _lineOperator( ); const lineHeight = marginTop + paddingTop + contentHeight + paddingBottom; + let section = column.parent; if (!section) { // 做一个兜底,指向当前页最后一个section diff --git a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts index 6af86d65a84..bfd96af1720 100644 --- a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts +++ b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts @@ -194,8 +194,8 @@ export class DocumentSkeleton extends Skeleton { }; } - findNodePositionByCharIndex(charIndex: number, isBack: boolean = true, segmentId = ''): Nullable { - const nodes = this._findNodeIterator(charIndex, segmentId); + findNodePositionByCharIndex(charIndex: number, isBack: boolean = true, segmentId = '', segmentPIndex = -1): Nullable { + const nodes = this._findNodeByIndex(charIndex, segmentId, segmentPIndex); if (nodes == null) { return; @@ -225,8 +225,8 @@ export class DocumentSkeleton extends Skeleton { }; } - findNodeByCharIndex(charIndex: number, segmentId = ''): Nullable { - const nodes = this._findNodeIterator(charIndex, segmentId); + findNodeByCharIndex(charIndex: number, segmentId = '', segmentPageIndex = -1): Nullable { + const nodes = this._findNodeByIndex(charIndex, segmentId, segmentPageIndex); return nodes?.glyph; } @@ -300,7 +300,7 @@ export class DocumentSkeleton extends Skeleton { } { const { x, y } = coord; let editArea = DocumentEditArea.BODY; - let pageNumber = 0; + let pageNumber = -1; let pageSkeleton = null; const skeletonData = this.getSkeletonData(); @@ -314,7 +314,7 @@ export class DocumentSkeleton extends Skeleton { this._findLiquid.reset(); - const pages = skeletonData.pages; + const { pages } = skeletonData; for (let i = 0, len = pages.length; i < len; i++) { const page = pages[i]; @@ -345,7 +345,7 @@ export class DocumentSkeleton extends Skeleton { x > this._findLiquid.x && x < this._findLiquid.x + pageWidth && y > this._findLiquid.y + pageHeight - marginBottom && y < this._findLiquid.y + pageHeight ) { - editArea = DocumentEditArea.HEADER; + editArea = DocumentEditArea.FOOTER; pageSkeleton = page; pageNumber = i; break; @@ -384,6 +384,8 @@ export class DocumentSkeleton extends Skeleton { let nearestNodeDistanceList: number[] = []; + let segmentId = ''; + let nearestNodeDistanceY = Number.POSITIVE_INFINITY; for (let pi = 0, len = pages.length; pi < len; pi++) { @@ -402,20 +404,36 @@ export class DocumentSkeleton extends Skeleton { if (headerSke) { page = headerSke; + segmentId = headerId; } } else if (editArea === DocumentEditArea.FOOTER) { const footerSke = skeFooters.get(footerId)?.get(pageWidth) as IDocumentSkeletonPage; if (footerSke) { page = footerSke; + segmentId = footerId; } } const { sections } = page; - this._findLiquid.translatePagePadding({ - ...page, - marginLeft: pages[pi].marginLeft, // Because header or footer margin Left is 0. - }); + + this._findLiquid.translateSave(); + switch (editArea) { + case DocumentEditArea.HEADER: + this._findLiquid.translatePagePadding({ + ...page, + marginLeft: pages[pi].marginLeft, // Because header or footer margin Left is 0. + }); + break; + case DocumentEditArea.FOOTER: { + const footerTop = pages[pi].pageHeight - page.height - page.marginBottom; + this._findLiquid.translate(pages[pi].marginLeft, footerTop); + break; + } + default: + this._findLiquid.translatePagePadding(pages[pi]); + break; + } for (const section of sections) { const { columns } = section; @@ -497,7 +515,8 @@ export class DocumentSkeleton extends Skeleton { if (x >= startX_fin && x <= endX_fin) { return { node: glyph, - segmentPage: pi, + segmentPage: editArea === DocumentEditArea.BODY ? -1 : pi, + segmentId, ratioX: x / (startX_fin + endX_fin), ratioY: y / (startY_fin + endY_fin), }; @@ -509,7 +528,8 @@ export class DocumentSkeleton extends Skeleton { } nearestNodeList.push({ node: glyph, - segmentPage: pi, + segmentPage: editArea === DocumentEditArea.BODY ? -1 : pi, + segmentId, ratioX: x / (startX_fin + endX_fin), ratioY: y / (startY_fin + endY_fin), }); @@ -529,7 +549,8 @@ export class DocumentSkeleton extends Skeleton { if (distanceY === nearestNodeDistanceY) { nearestNodeList.push({ node: glyph, - segmentPage: pi, + segmentPage: editArea === DocumentEditArea.BODY ? -1 : pi, + segmentId, ratioX: x / (startX_fin + endX_fin), ratioY: y / (startY_fin + endY_fin), }); @@ -545,7 +566,7 @@ export class DocumentSkeleton extends Skeleton { this._findLiquid.translateRestore(); } } - this._findLiquid.restorePagePadding(page); + this._findLiquid.translateRestore(); this._translatePage(pages[pi], pageLayoutType, pageMarginLeft, pageMarginTop); } @@ -807,10 +828,10 @@ export class DocumentSkeleton extends Skeleton { sections.push(newSection); } - private _findNodeIterator(charIndex: number, segmentId = '') { + private _findNodeByIndex(charIndex: number, segmentId = '', segmentPageIndex = -1) { const skeletonData = this.getSkeletonData(); - if (!skeletonData) { + if (skeletonData == null) { return; } @@ -818,6 +839,11 @@ export class DocumentSkeleton extends Skeleton { const { pages, skeFooters, skeHeaders } = skeletonData; for (const page of pages) { + const curPageIndex = pages.indexOf(page); + if (editArea !== DocumentEditArea.BODY && curPageIndex !== segmentPageIndex) { + continue; + } + const { pageWidth } = page; let segmentPage = page; @@ -887,7 +913,7 @@ export class DocumentSkeleton extends Skeleton { line, divide, glyph, - segmentPageIndex: pages.indexOf(page), + segmentPageIndex, }; } } diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index 43a305620f1..707bd8ea39d 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -221,14 +221,14 @@ function _createSkeletonHeaderFooter( return { ...page, marginTop: marginHeader, - marginBottom: 0, + marginBottom: 5, // Space between header and content }; } return { ...page, marginBottom: marginFooter, - marginTop: 0, + marginTop: 5, // Space between footer and content }; } @@ -241,7 +241,7 @@ function _getVerticalMargin( return marginTB; } - return Math.max(marginTB, (marginHF + headerOrFooter?.height || 0)); + return Math.max(marginTB, (headerOrFooter.marginTop + headerOrFooter.height + headerOrFooter.marginBottom || 0)); } function __getHeaderMarginTop(marginTop: number, marginHeader: number, height: number) { diff --git a/packages/engine-render/src/components/docs/liquid.ts b/packages/engine-render/src/components/docs/liquid.ts index 9130cf7b584..4724cf2ead6 100644 --- a/packages/engine-render/src/components/docs/liquid.ts +++ b/packages/engine-render/src/components/docs/liquid.ts @@ -206,7 +206,7 @@ export class Liquid { }; } - translateSpan(glyph: IDocumentSkeletonGlyph) { + translateGlyph(glyph: IDocumentSkeletonGlyph) { const { left: spanLeft } = glyph; this.translate(spanLeft, 0); diff --git a/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts b/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts index 5bd0489944b..e3fba62953f 100644 --- a/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts +++ b/packages/engine-render/src/components/docs/text-selection/convert-cursor.ts @@ -154,22 +154,22 @@ export function getOneTextSelectionRange(rangeList: ITextRange[]): Nullable { - const { startOffset, endOffset, style = NORMAL_TEXT_SELECTION_PLUGIN_STYLE, segmentId = '' } = range; + const { startOffset, endOffset, style = NORMAL_TEXT_SELECTION_PLUGIN_STYLE, segmentId = '', segmentPage } = range; + const anchorNodePosition = docSkeleton.findNodePositionByCharIndex(startOffset, true, segmentId, segmentPage); + const focusNodePosition = startOffset !== endOffset ? docSkeleton.findNodePositionByCharIndex(endOffset, true, segmentId, segmentPage) : null; - const anchorNodePosition = docSkeleton.findNodePositionByCharIndex(startOffset, true, segmentId); - const focusNodePosition = startOffset !== endOffset ? docSkeleton.findNodePositionByCharIndex(endOffset, true, segmentId) : null; - - const textRange = new TextRange(scene, document, docSkeleton, anchorNodePosition, focusNodePosition, style, segmentId); + const textRange = new TextRange(scene, document, docSkeleton, anchorNodePosition, focusNodePosition, style); textRange.refresh(); @@ -99,8 +98,7 @@ export class TextRange { private _docSkeleton: DocumentSkeleton, public anchorNodePosition?: Nullable, public focusNodePosition?: Nullable, - public style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE, - public segmentId = '' + public style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE ) { this._anchorBlink(); } diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index 6bd94a79757..cc4dcb4e03f 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -103,6 +103,7 @@ export interface ITextSelectionInnerParam { segmentId: string; isEditing: boolean; style: ITextSelectionStyle; + segmentPage: number; } export interface IActiveTextRange { @@ -113,6 +114,7 @@ export interface IActiveTextRange { endNodePosition: Nullable; direction: RANGE_DIRECTION; segmentId: string; + segmentPage: number; style: ITextSelectionStyle; } @@ -134,6 +136,7 @@ export interface ITextSelectionRenderManager { enableSelection(): void; disableSelection(): void; setSegment(id: string): void; + setSegmentPage(pageIndex: number): void; setStyle(style: ITextSelectionStyle): void; resetStyle(): void; removeAllTextRanges(): void; @@ -200,6 +203,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel private _viewportScrollY: number = 0; private _rangeList: TextRange[] = []; private _currentSegmentId: string = ''; + private _currentSegmentPage: number = -1; private _selectionStyle: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE; private _isSelectionEnabled: boolean = true; private _viewPortObserverMap = new Map< @@ -238,6 +242,10 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel this._currentSegmentId = id; } + setSegmentPage(pageIndex: number) { + this._currentSegmentPage = pageIndex; + } + setStyle(style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE) { this._selectionStyle = style; } @@ -262,6 +270,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel style: this._selectionStyle, ...range, segmentId: this._currentSegmentId, + segmentPage: this._currentSegmentPage, }, docSkeleton!, this._document!); this._add(textSelection); @@ -270,6 +279,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel this._textSelectionInner$.next({ textRanges: this._getAllTextRanges(), segmentId: this._currentSegmentId, + segmentPage: this._currentSegmentPage, style: this._selectionStyle, isEditing, }); @@ -450,11 +460,20 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel const position = this._getNodePosition(startNode); - if (position == null) { + if (position == null || startNode == null) { this._removeAllTextRanges(); return; } + const { segmentId, segmentPage } = startNode; + + if (segmentId !== this._currentSegmentId) { + this.setSegment(segmentId); + } + + if (segmentPage !== this._currentSegmentPage) { + this.setSegmentPage(segmentPage); + } if (startNode?.node.streamType === DataStreamTreeTokenType.PARAGRAPH) { position.isBack = true; @@ -463,7 +482,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel if (evt.shiftKey && this._getActiveRangeInstance()) { this._updateActiveRangeFocusPosition(position); } else if (evt.ctrlKey || this._isEmpty()) { - const newTextSelection = new TextRange(scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle, this._currentSegmentId); + const newTextSelection = new TextRange(scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle); this._addTextRange(newTextSelection); } else { @@ -513,12 +532,15 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel scene.enableEvent(); - this._textSelectionInner$.next({ + const selectionInfo = { textRanges: this._getAllTextRanges(), segmentId: this._currentSegmentId, + segmentPage: this._currentSegmentPage, style: this._selectionStyle, isEditing: false, - }); + }; + + this._textSelectionInner$.next(selectionInfo); this._scrollTimers.forEach((timer) => { timer?.dispose(); @@ -574,6 +596,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel endNodePosition, direction, segmentId: this._currentSegmentId, + segmentPage: this._currentSegmentPage, style: this._selectionStyle, }; } @@ -725,7 +748,7 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel let lastRange = this._rangeList.pop(); if (!lastRange) { - lastRange = new TextRange(this._scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle, this._currentSegmentId); + lastRange = new TextRange(this._scene, this._document!, this._docSkeleton!, position, undefined, this._selectionStyle); } this._removeAllTextRanges(); @@ -1004,9 +1027,11 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel pageMarginTop, } = this._document!.getOffsetConfig(); - return this._docSkeleton?.findNodeByCoord( + const nodeInfo = this._docSkeleton?.findNodeByCoord( coord, pageLayoutType, pageMarginLeft, pageMarginTop ); + + return nodeInfo; } private _detachEvent() { From 0e8d2c87d62830f8d920ac916d1af13af7463f36 Mon Sep 17 00:00:00 2001 From: jocs Date: Thu, 27 Jun 2024 14:35:38 +0800 Subject: [PATCH 12/39] fix: list --- .vscode/settings.json | 1 + packages/core/src/docs/data-model/text-x/text-x.ts | 6 +++--- .../src/components/docs/layout/block/paragraph/layout.ts | 1 + .../components/docs/layout/block/paragraph/linebreaking.ts | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ffbfb135bc9..0de52c3e7fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -106,6 +106,7 @@ "universlide", "Unkonwn", "Unmerge", + "Unorder", "Unprotect", "unsync", "Verdana", diff --git a/packages/core/src/docs/data-model/text-x/text-x.ts b/packages/core/src/docs/data-model/text-x/text-x.ts index 357417fafbe..dbfb9514053 100644 --- a/packages/core/src/docs/data-model/text-x/text-x.ts +++ b/packages/core/src/docs/data-model/text-x/text-x.ts @@ -161,11 +161,11 @@ export class TextX { } if (action.t === TextXActionType.RETAIN && action.body != null) { - const { textRuns, customDecorations } = getBodySlice(doc, index, index + action.len, true); + const body = getBodySlice(doc, index, index + action.len, true); + action.oldBody = { + ...body, dataStream: '', - textRuns, - customDecorations, }; } diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/layout.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/layout.ts index 0c647d5a651..1c97e9f6e7d 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/layout.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/layout.ts @@ -46,6 +46,7 @@ export function dealWidthParagraph( // Step 2: Line Breaking. const allPages = lineBreaking( ctx, + viewModel, shapedTextList, curPage, paragraphNode, diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts index d18da0360d5..3d49d235e76 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts @@ -24,6 +24,7 @@ import type { ILayoutContext } from '../../tools'; import { getLastNotFullColumnInfo } from '../../tools'; import type { DataStreamTreeNode } from '../../../view-model/data-stream-tree-node'; import type { IParagraphConfig, ISectionBreakConfig } from '../../../../../basics/interfaces'; +import type { DocumentViewModel } from '../../../view-model/document-view-model'; import type { IShapedText } from './shaping'; import { layoutParagraph } from './layout-ruler'; import { dealWithBullet } from './bullet'; @@ -111,12 +112,13 @@ function _getNextPageNumber(lastPage: IDocumentSkeletonPage) { export function lineBreaking( ctx: ILayoutContext, + viewModel: DocumentViewModel, shapedTextList: IShapedText[], curPage: IDocumentSkeletonPage, paragraphNode: DataStreamTreeNode, sectionBreakConfig: ISectionBreakConfig ): IDocumentSkeletonPage[] { - const { viewModel, skeletonResourceReference } = ctx; + const { skeletonResourceReference } = ctx; const { lists, drawings = {}, From 2b1e7c2c0a07ed65a1ea0e3db686d86d579e505e Mon Sep 17 00:00:00 2001 From: jocs Date: Thu, 27 Jun 2024 20:44:26 +0800 Subject: [PATCH 13/39] feat: create command --- .../src/data/docs/default-document-data-cn.ts | 26 ++- .../docs/data-model/document-data-model.ts | 11 +- .../commands/header-footer.command.ts | 177 ++++++++++++++++++ .../doc-header-footer.controller.ts | 26 ++- .../mutations/core-editing.mutation.ts | 7 +- 5 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 packages/docs-ui/src/commands/commands/header-footer.command.ts diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index 70a0e3a8a4e..b5627963c6c 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -230,20 +230,20 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { 'shapeTest5', ], headers: { - defaultHeaderId: { - headerId: 'defaultHeaderId', - body: getDefaultHeaderFooterBody('header'), - }, + // defaultHeaderId: { + // headerId: 'defaultHeaderId', + // body: getDefaultHeaderFooterBody('header'), + // }, // evenHeaderId: { // }, // firstPageHeaderId: { // } }, footers: { - defaultFooterId: { - footerId: 'defaultFooterId', - body: getDefaultHeaderFooterBody('footer'), - }, + // defaultFooterId: { + // footerId: 'defaultFooterId', + // body: getDefaultHeaderFooterBody('footer'), + // }, // evenFooterId: { // }, // firstPageFooterId: { @@ -859,8 +859,14 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { vertexAngle: 0, centerAngle: 0, }, - defaultHeaderId: 'defaultHeaderId', - defaultFooterId: 'defaultFooterId', + defaultHeaderId: '', + defaultFooterId: '', + evenPageHeaderId: '', + evenPageFooterId: '', + firstPageHeaderId: '', + firstPageFooterId: '', + evenAndOddHeaders: BooleanNumber.FALSE, + useFirstPageHeaderFooter: BooleanNumber.FALSE, marginHeader: 30, marginFooter: 30, }, diff --git a/packages/core/src/docs/data-model/document-data-model.ts b/packages/core/src/docs/data-model/document-data-model.ts index 4ee8fcc6c19..adb11f78db4 100644 --- a/packages/core/src/docs/data-model/document-data-model.ts +++ b/packages/core/src/docs/data-model/document-data-model.ts @@ -286,7 +286,16 @@ export class DocumentDataModel extends DocumentDataModelSimple { return; } - return JSONX.apply(this.snapshot, actions); + this.snapshot = JSONX.apply(this.snapshot, actions) as unknown as IDocumentData; + + // FIXME: @JOCS, ANY better solution to find action that create or delete header/footer? + if (actions?.some((a) => Array.isArray(a) && (a?.[0] === 'headers' || a?.[0] === 'footers'))) { + this.headerModelMap.clear(); + this.footerModelMap.clear(); + this._initializeHeaderFooterModel(); + } + + return this.snapshot; } sliceBody(startOffset: number, endOffset: number): Nullable { diff --git a/packages/docs-ui/src/commands/commands/header-footer.command.ts b/packages/docs-ui/src/commands/commands/header-footer.command.ts new file mode 100644 index 00000000000..11756fd10d6 --- /dev/null +++ b/packages/docs-ui/src/commands/commands/header-footer.command.ts @@ -0,0 +1,177 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { BooleanNumber, ICommand, IMutationInfo, JSONXActions } from '@univerjs/core'; +import { CommandType, ICommandService, IUniverInstanceService, JSONX, Tools } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import { RichTextEditingMutation } from '@univerjs/docs'; +import type { ITextRangeWithStyle } from '@univerjs/engine-render'; +import { HeaderFooterType } from '../../controllers/doc-header-footer.controller'; + +function getEmptyHeaderFooterBody() { + return { + dataStream: '\r\n', + textRuns: [], + paragraphs: [ + { + startIndex: 0, + spaceAbove: 0, + lineSpacing: 1.5, + spaceBelow: 0, + }, + ], + sectionBreaks: [ + { + startIndex: 1, + }, + ], + }; +} + +interface IHeaderFooterProps { + marginHeader?: number; // marginHeader + marginFooter?: number; // marginFooter + useFirstPageHeaderFooter?: BooleanNumber; // useFirstPageHeaderFooter + evenAndOddHeaders?: BooleanNumber; // useEvenPageHeaderFooter, +} + +export interface ICoreHeaderFooterParams { + unitId: string; + createType?: HeaderFooterType; + segmentId?: string; + headerFooterProps?: IHeaderFooterProps; +} + +export const CoreHeaderFooterCommandId = 'doc.command.core-header-footer'; + +/** + * The command to update header and footer or create them. + */ +export const CoreHeaderFooterCommand: ICommand = { + id: CoreHeaderFooterCommandId, + type: CommandType.COMMAND, + // eslint-disable-next-line max-lines-per-function + handler: async (accessor, params: ICoreHeaderFooterParams) => { + const commandService = accessor.get(ICommandService); + const univerInstanceService = accessor.get(IUniverInstanceService); + const { unitId, segmentId, createType, headerFooterProps } = params; + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + + if (docDataModel == null) { + return false; + } + + const { documentStyle } = docDataModel.getSnapshot(); + + const textRanges: ITextRangeWithStyle[] = [ + { + startOffset: 0, + endOffset: 0, + collapsed: true, + }, + ]; + + const doMutation: IMutationInfo = { + id: RichTextEditingMutation.id, + params: { + unitId, + actions: [], + textRanges, + debounce: true, + }, + }; + + const jsonX = JSONX.getInstance(); + const rawActions: JSONXActions = []; + + if (createType != null) { + const ID_LEN = 6; + const headerFooterId = segmentId ?? Tools.generateRandomId(ID_LEN); + const isHeader = createType === HeaderFooterType.DEFAULT_HEADER || createType === HeaderFooterType.FIRST_PAGE_HEADER || createType === HeaderFooterType.EVEN_PAGE_HEADER; + const insertAction = jsonX.insertOp([isHeader ? 'headers' : 'footers', headerFooterId], { + [isHeader ? 'headerId' : 'footerId']: headerFooterId, + body: getEmptyHeaderFooterBody(), + }); + + rawActions.push(insertAction!); + + let key = 'defaultHeaderId'; + + switch (createType) { + case HeaderFooterType.DEFAULT_HEADER: + key = 'defaultHeaderId'; + break; + case HeaderFooterType.DEFAULT_FOOTER: + key = 'defaultFooterId'; + break; + case HeaderFooterType.FIRST_PAGE_HEADER: + key = 'firstPageHeaderId'; + break; + case HeaderFooterType.FIRST_PAGE_FOOTER: + key = 'firstPageFooterId'; + break; + case HeaderFooterType.EVEN_PAGE_HEADER: + key = 'evenPageHeaderId'; + break; + case HeaderFooterType.EVEN_PAGE_FOOTER: + key = 'evenPageFooterId'; + break; + default: + throw new Error(`Unknown header footer type: ${createType}`); + } + + if (documentStyle[key as keyof IHeaderFooterProps] != null) { + const replaceAction = jsonX.replaceOp(['documentStyle', key], documentStyle[key as keyof IHeaderFooterProps], headerFooterId); + rawActions.push(replaceAction!); + } else { + const insertAction = jsonX.insertOp(['documentStyle', key], headerFooterId); + rawActions.push(insertAction!); + } + } + + if (headerFooterProps != null) { + Object.keys(headerFooterProps).forEach((key) => { + const value = headerFooterProps[key as keyof IHeaderFooterProps]; + const oldValue = documentStyle[key as keyof IHeaderFooterProps]; + let action; + if (oldValue === undefined) { + action = jsonX.insertOp(['documentStyle', key], value); + } else { + action = jsonX.replaceOp(['documentStyle', key], oldValue, value); + } + + rawActions.push(action!); + + // TODO: Create a new header footer if the header footer is not created. + }); + } + + if (rawActions.length === 0) { + return false; + } + + doMutation.params.actions = rawActions.reduce((acc, cur) => { + return JSONX.compose(acc, cur as JSONXActions); + }, null as JSONXActions); + + const result = commandService.syncExecuteCommand< + IRichTextEditingMutationParams, + IRichTextEditingMutationParams + >(doMutation.id, doMutation.params); + + return Boolean(result); + }, +}; diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index 5f2183c388f..81c465ffdf5 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -15,7 +15,7 @@ */ import type { DocumentDataModel } from '@univerjs/core'; -import { BooleanNumber, Disposable, ICommandService, IUniverInstanceService, LocaleService, toDisposable } from '@univerjs/core'; +import { BooleanNumber, Disposable, ICommandService, IUniverInstanceService, LocaleService, toDisposable, Tools } from '@univerjs/core'; import type { Documents, DocumentViewModel, IMouseEvent, IPageRenderConfig, IPathProps, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; @@ -24,6 +24,7 @@ import { IEditorService } from '@univerjs/ui'; import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; +import { CoreHeaderFooterCommand } from '../commands/commands/header-footer.command'; const HEADER_FOOTER_STROKE_COLOR = 'rgba(58, 96, 247, 1)'; const HEADER_FOOTER_FILL_COLOR = 'rgba(58, 96, 247, 0.08)'; @@ -43,7 +44,7 @@ interface IHeaderFooterCreate { } // TODO: @JOCS also need to check sectionBreak config in the future. -function checkCreateHeaderFooterType(viewModel: DocumentViewModel, editArea: DocumentEditArea, pageNumber: number): IHeaderFooterCreate { +function checkCreateHeaderFooterType(viewModel: DocumentViewModel, editArea: DocumentEditArea, segmentPage: number): IHeaderFooterCreate { const { documentStyle } = viewModel.getDataModel().getSnapshot(); const { defaultHeaderId, @@ -70,7 +71,7 @@ function checkCreateHeaderFooterType(viewModel: DocumentViewModel, editArea: Doc }; } - if (evenAndOddHeaders === BooleanNumber.TRUE && pageNumber % 2 === 0 && !evenPageHeaderId) { + if (evenAndOddHeaders === BooleanNumber.TRUE && segmentPage % 2 === 0 && !evenPageHeaderId) { return { createType: HeaderFooterType.EVEN_PAGE_HEADER, headerFooterId: null, @@ -95,7 +96,7 @@ function checkCreateHeaderFooterType(viewModel: DocumentViewModel, editArea: Doc }; } - if (evenAndOddHeaders === BooleanNumber.TRUE && pageNumber % 2 === 0 && !evenPageFooterId) { + if (evenAndOddHeaders === BooleanNumber.TRUE && segmentPage % 2 === 0 && !evenPageFooterId) { return { createType: HeaderFooterType.EVEN_PAGE_FOOTER, headerFooterId: null, @@ -138,6 +139,11 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu private _initialize() { this._init(); this._drawHeaderFooterLabel(); + this._registerCommands(); + } + + private _registerCommands() { + [CoreHeaderFooterCommand].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); } private _init() { @@ -157,7 +163,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu const docObject = neoGetDocObject(this._context); const { document } = docObject; - this.disposeWithMe(document.onDblclickObserver.add((evt: IPointerEvent | IMouseEvent) => { + this.disposeWithMe(document.onDblclickObserver.add(async (evt: IPointerEvent | IMouseEvent) => { if (this._isEditorReadOnly(unitId)) { return; } @@ -196,10 +202,18 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu if (editArea === DocumentEditArea.BODY) { this._textSelectionRenderManager.setSegment(''); - this._textSelectionRenderManager.setSegmentPage(pageNumber); + this._textSelectionRenderManager.setSegmentPage(-1); } else { if (createType != null) { // TODO: create header or footer and set segment. + const segmentId = Tools.generateRandomId(6); + this._textSelectionRenderManager.setSegment(segmentId); + this._textSelectionRenderManager.setSegmentPage(pageNumber); + await this._commandService.executeCommand(CoreHeaderFooterCommand.id, { + unitId, + createType, + segmentId, + }); } else if (headerFooterId != null) { this._textSelectionRenderManager.setSegment(headerFooterId); // TODO: set selection to header or footer. diff --git a/packages/docs/src/commands/mutations/core-editing.mutation.ts b/packages/docs/src/commands/mutations/core-editing.mutation.ts index 234e7b6e0b0..f39c4f34a0d 100644 --- a/packages/docs/src/commands/mutations/core-editing.mutation.ts +++ b/packages/docs/src/commands/mutations/core-editing.mutation.ts @@ -98,13 +98,12 @@ export const RichTextEditingMutation: IMutation Date: Fri, 28 Jun 2024 14:39:32 +0800 Subject: [PATCH 14/39] feat: update command --- .../commands/doc-header-footer.command.ts | 214 ++++++++++++++++++ .../commands/header-footer.command.ts | 177 --------------- .../doc-header-footer.controller.ts | 7 +- 3 files changed, 218 insertions(+), 180 deletions(-) create mode 100644 packages/docs-ui/src/commands/commands/doc-header-footer.command.ts delete mode 100644 packages/docs-ui/src/commands/commands/header-footer.command.ts diff --git a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts new file mode 100644 index 00000000000..7ae45ba0f45 --- /dev/null +++ b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts @@ -0,0 +1,214 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICommand, IMutationInfo, JSONXActions } from '@univerjs/core'; +import { BooleanNumber, CommandType, ICommandService, IUniverInstanceService, JSONX, Tools } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { DocumentEditArea, IRenderManagerService, type ITextRangeWithStyle } from '@univerjs/engine-render'; +import { HeaderFooterType } from '../../controllers/doc-header-footer.controller'; + +function getEmptyHeaderFooterBody() { + return { + dataStream: '\r\n', + textRuns: [], + paragraphs: [ + { + startIndex: 0, + spaceAbove: 0, + lineSpacing: 1.5, + spaceBelow: 0, + }, + ], + sectionBreaks: [ + { + startIndex: 1, + }, + ], + }; +} + +function createHeaderFooterAction(segmentId: string, createType: HeaderFooterType, documentStyle: IHeaderFooterProps, actions: JSONXActions) { + const jsonX = JSONX.getInstance(); + const ID_LEN = 6; + const firstSegmentId = segmentId ?? Tools.generateRandomId(ID_LEN); + const isHeader = createType === HeaderFooterType.DEFAULT_HEADER || createType === HeaderFooterType.FIRST_PAGE_HEADER || createType === HeaderFooterType.EVEN_PAGE_HEADER; + const insertAction = jsonX.insertOp([isHeader ? 'headers' : 'footers', firstSegmentId], { + [isHeader ? 'headerId' : 'footerId']: firstSegmentId, + body: getEmptyHeaderFooterBody(), + }); + + actions!.push(insertAction!); + + // Also need to create an empty footer if you create a header, and vice versa. They are always created in pairs. + const secondSegmentId = Tools.generateRandomId(ID_LEN); + const insertPairAction = jsonX.insertOp([isHeader ? 'footers' : 'headers', secondSegmentId], { + [isHeader ? 'footers' : 'headers']: secondSegmentId, + body: getEmptyHeaderFooterBody(), + }); + actions!.push(insertPairAction!); + + let key = 'defaultHeaderId'; + let pairKey = 'defaultFooterId'; + + switch (createType) { + case HeaderFooterType.DEFAULT_HEADER: + key = 'defaultHeaderId'; + pairKey = 'defaultFooterId'; + break; + case HeaderFooterType.DEFAULT_FOOTER: + key = 'defaultFooterId'; + pairKey = 'defaultHeaderId'; + break; + case HeaderFooterType.FIRST_PAGE_HEADER: + key = 'firstPageHeaderId'; + pairKey = 'firstPageFooterId'; + break; + case HeaderFooterType.FIRST_PAGE_FOOTER: + key = 'firstPageFooterId'; + pairKey = 'firstPageHeaderId'; + break; + case HeaderFooterType.EVEN_PAGE_HEADER: + key = 'evenPageHeaderId'; + pairKey = 'evenPageFooterId'; + break; + case HeaderFooterType.EVEN_PAGE_FOOTER: + key = 'evenPageFooterId'; + pairKey = 'evenPageHeaderId'; + break; + default: + throw new Error(`Unknown header footer type: ${createType}`); + } + + for (const [k, id] of [[key, firstSegmentId], [pairKey, secondSegmentId]]) { + if (documentStyle[k as keyof IHeaderFooterProps] != null) { + const replaceAction = jsonX.replaceOp(['documentStyle', k], documentStyle[k as keyof IHeaderFooterProps], id); + actions!.push(replaceAction!); + } else { + const insertAction = jsonX.insertOp(['documentStyle', k], id); + actions!.push(insertAction!); + } + } + + return actions; +} + +interface IHeaderFooterProps { + marginHeader?: number; // marginHeader + marginFooter?: number; // marginFooter + useFirstPageHeaderFooter?: BooleanNumber; // useFirstPageHeaderFooter + evenAndOddHeaders?: BooleanNumber; // useEvenPageHeaderFooter, +} + +export interface ICoreHeaderFooterParams { + unitId: string; + createType?: HeaderFooterType; + segmentId?: string; + headerFooterProps?: IHeaderFooterProps; +} + +export const CoreHeaderFooterCommandId = 'doc.command.core-header-footer'; + +/** + * The command to update header and footer or create them. + */ +export const CoreHeaderFooterCommand: ICommand = { + id: CoreHeaderFooterCommandId, + type: CommandType.COMMAND, + + // eslint-disable-next-line max-lines-per-function + handler: async (accessor, params: ICoreHeaderFooterParams) => { + const commandService = accessor.get(ICommandService); + const univerInstanceService = accessor.get(IUniverInstanceService); + const renderManagerService = accessor.get(IRenderManagerService); + const { unitId, segmentId, createType, headerFooterProps } = params; + const docSkeletonManagerService = renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + const docViewModel = docSkeletonManagerService?.getViewModel(); + + if (docDataModel == null || docViewModel == null) { + return false; + } + + const editArea = docViewModel.getEditArea(); + + const { documentStyle } = docDataModel.getSnapshot(); + + const textRanges: ITextRangeWithStyle[] = [ + { + startOffset: 0, + endOffset: 0, + collapsed: true, + }, + ]; + + const doMutation: IMutationInfo = { + id: RichTextEditingMutation.id, + params: { + unitId, + actions: [], + textRanges, + debounce: true, + }, + }; + + const jsonX = JSONX.getInstance(); + const rawActions: JSONXActions = []; + + if (createType != null) { + createHeaderFooterAction(segmentId!, createType, documentStyle, rawActions); + } + + if (headerFooterProps != null) { + Object.keys(headerFooterProps).forEach((key) => { + const value = headerFooterProps[key as keyof IHeaderFooterProps]; + const oldValue = documentStyle[key as keyof IHeaderFooterProps]; + let action; + if (oldValue === undefined) { + action = jsonX.insertOp(['documentStyle', key], value); + } else { + action = jsonX.replaceOp(['documentStyle', key], oldValue, value); + } + + rawActions.push(action!); + + // need create first page header/footer if useFirstPageHeaderFooter is true and firstPageHeaderId is not set. + if (key === 'useFirstPageHeaderFooter' && value === BooleanNumber.TRUE && !documentStyle.firstPageHeaderId) { + const headerFooterType = editArea === DocumentEditArea.HEADER ? HeaderFooterType.FIRST_PAGE_HEADER : HeaderFooterType.FIRST_PAGE_FOOTER; + createHeaderFooterAction(segmentId!, headerFooterType, documentStyle, rawActions); + } else if (key === 'evenAndOddHeaders' && value === BooleanNumber.TRUE && !documentStyle.evenPageHeaderId) { + const headerFooterType = editArea === DocumentEditArea.HEADER ? HeaderFooterType.EVEN_PAGE_HEADER : HeaderFooterType.EVEN_PAGE_FOOTER; + createHeaderFooterAction(segmentId!, headerFooterType, documentStyle, rawActions); + } + }); + } + + if (rawActions.length === 0) { + return false; + } + + doMutation.params.actions = rawActions.reduce((acc, cur) => { + return JSONX.compose(acc, cur as JSONXActions); + }, null as JSONXActions); + + const result = commandService.syncExecuteCommand< + IRichTextEditingMutationParams, + IRichTextEditingMutationParams + >(doMutation.id, doMutation.params); + + return Boolean(result); + }, +}; diff --git a/packages/docs-ui/src/commands/commands/header-footer.command.ts b/packages/docs-ui/src/commands/commands/header-footer.command.ts deleted file mode 100644 index 11756fd10d6..00000000000 --- a/packages/docs-ui/src/commands/commands/header-footer.command.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { BooleanNumber, ICommand, IMutationInfo, JSONXActions } from '@univerjs/core'; -import { CommandType, ICommandService, IUniverInstanceService, JSONX, Tools } from '@univerjs/core'; -import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import { RichTextEditingMutation } from '@univerjs/docs'; -import type { ITextRangeWithStyle } from '@univerjs/engine-render'; -import { HeaderFooterType } from '../../controllers/doc-header-footer.controller'; - -function getEmptyHeaderFooterBody() { - return { - dataStream: '\r\n', - textRuns: [], - paragraphs: [ - { - startIndex: 0, - spaceAbove: 0, - lineSpacing: 1.5, - spaceBelow: 0, - }, - ], - sectionBreaks: [ - { - startIndex: 1, - }, - ], - }; -} - -interface IHeaderFooterProps { - marginHeader?: number; // marginHeader - marginFooter?: number; // marginFooter - useFirstPageHeaderFooter?: BooleanNumber; // useFirstPageHeaderFooter - evenAndOddHeaders?: BooleanNumber; // useEvenPageHeaderFooter, -} - -export interface ICoreHeaderFooterParams { - unitId: string; - createType?: HeaderFooterType; - segmentId?: string; - headerFooterProps?: IHeaderFooterProps; -} - -export const CoreHeaderFooterCommandId = 'doc.command.core-header-footer'; - -/** - * The command to update header and footer or create them. - */ -export const CoreHeaderFooterCommand: ICommand = { - id: CoreHeaderFooterCommandId, - type: CommandType.COMMAND, - // eslint-disable-next-line max-lines-per-function - handler: async (accessor, params: ICoreHeaderFooterParams) => { - const commandService = accessor.get(ICommandService); - const univerInstanceService = accessor.get(IUniverInstanceService); - const { unitId, segmentId, createType, headerFooterProps } = params; - const docDataModel = univerInstanceService.getUniverDocInstance(unitId); - - if (docDataModel == null) { - return false; - } - - const { documentStyle } = docDataModel.getSnapshot(); - - const textRanges: ITextRangeWithStyle[] = [ - { - startOffset: 0, - endOffset: 0, - collapsed: true, - }, - ]; - - const doMutation: IMutationInfo = { - id: RichTextEditingMutation.id, - params: { - unitId, - actions: [], - textRanges, - debounce: true, - }, - }; - - const jsonX = JSONX.getInstance(); - const rawActions: JSONXActions = []; - - if (createType != null) { - const ID_LEN = 6; - const headerFooterId = segmentId ?? Tools.generateRandomId(ID_LEN); - const isHeader = createType === HeaderFooterType.DEFAULT_HEADER || createType === HeaderFooterType.FIRST_PAGE_HEADER || createType === HeaderFooterType.EVEN_PAGE_HEADER; - const insertAction = jsonX.insertOp([isHeader ? 'headers' : 'footers', headerFooterId], { - [isHeader ? 'headerId' : 'footerId']: headerFooterId, - body: getEmptyHeaderFooterBody(), - }); - - rawActions.push(insertAction!); - - let key = 'defaultHeaderId'; - - switch (createType) { - case HeaderFooterType.DEFAULT_HEADER: - key = 'defaultHeaderId'; - break; - case HeaderFooterType.DEFAULT_FOOTER: - key = 'defaultFooterId'; - break; - case HeaderFooterType.FIRST_PAGE_HEADER: - key = 'firstPageHeaderId'; - break; - case HeaderFooterType.FIRST_PAGE_FOOTER: - key = 'firstPageFooterId'; - break; - case HeaderFooterType.EVEN_PAGE_HEADER: - key = 'evenPageHeaderId'; - break; - case HeaderFooterType.EVEN_PAGE_FOOTER: - key = 'evenPageFooterId'; - break; - default: - throw new Error(`Unknown header footer type: ${createType}`); - } - - if (documentStyle[key as keyof IHeaderFooterProps] != null) { - const replaceAction = jsonX.replaceOp(['documentStyle', key], documentStyle[key as keyof IHeaderFooterProps], headerFooterId); - rawActions.push(replaceAction!); - } else { - const insertAction = jsonX.insertOp(['documentStyle', key], headerFooterId); - rawActions.push(insertAction!); - } - } - - if (headerFooterProps != null) { - Object.keys(headerFooterProps).forEach((key) => { - const value = headerFooterProps[key as keyof IHeaderFooterProps]; - const oldValue = documentStyle[key as keyof IHeaderFooterProps]; - let action; - if (oldValue === undefined) { - action = jsonX.insertOp(['documentStyle', key], value); - } else { - action = jsonX.replaceOp(['documentStyle', key], oldValue, value); - } - - rawActions.push(action!); - - // TODO: Create a new header footer if the header footer is not created. - }); - } - - if (rawActions.length === 0) { - return false; - } - - doMutation.params.actions = rawActions.reduce((acc, cur) => { - return JSONX.compose(acc, cur as JSONXActions); - }, null as JSONXActions); - - const result = commandService.syncExecuteCommand< - IRichTextEditingMutationParams, - IRichTextEditingMutationParams - >(doMutation.id, doMutation.params); - - return Boolean(result); - }, -}; diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index 81c465ffdf5..d71648fc4b6 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -24,7 +24,7 @@ import { IEditorService } from '@univerjs/ui'; import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; -import { CoreHeaderFooterCommand } from '../commands/commands/header-footer.command'; +import { CoreHeaderFooterCommand } from '../commands/commands/doc-header-footer.command'; const HEADER_FOOTER_STROKE_COLOR = 'rgba(58, 96, 247, 1)'; const HEADER_FOOTER_FILL_COLOR = 'rgba(58, 96, 247, 0.08)'; @@ -205,10 +205,11 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu this._textSelectionRenderManager.setSegmentPage(-1); } else { if (createType != null) { - // TODO: create header or footer and set segment. - const segmentId = Tools.generateRandomId(6); + const SEGMENT_ID_LEN = 6; + const segmentId = Tools.generateRandomId(SEGMENT_ID_LEN); this._textSelectionRenderManager.setSegment(segmentId); this._textSelectionRenderManager.setSegmentPage(pageNumber); + await this._commandService.executeCommand(CoreHeaderFooterCommand.id, { unitId, createType, From 9e9edbf7d7e7a4bf7df98a286021d03e0f1ece51 Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 17:07:58 +0800 Subject: [PATCH 15/39] feat: ui of header & footer options --- packages/docs-ui/package.json | 6 ++- .../commands/doc-header-footer.command.ts | 14 +++++ .../doc-header-footer-panel.operation.ts | 51 ++++++++++++++++++ .../doc-header-footer.controller.ts | 22 ++++++-- .../src/controllers/doc-ui.controller.ts | 2 + packages/docs-ui/src/controllers/menu/menu.ts | 13 +++++ packages/docs-ui/src/locale/en-US.ts | 8 +++ packages/docs-ui/src/locale/ru-RU.ts | 8 +++ packages/docs-ui/src/locale/zh-CN.ts | 8 +++ .../panel/DocHeaderFooterOptions.tsx | 53 +++++++++++++++++++ .../panel/DocHeaderFooterPanel.tsx | 28 ++++++++++ .../header-footer/panel/component-name.ts | 17 ++++++ .../header-footer/panel/index.module.less | 25 +++++++++ pnpm-lock.yaml | 8 +++ 14 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts create mode 100644 packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx create mode 100644 packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx create mode 100644 packages/docs-ui/src/views/header-footer/panel/component-name.ts create mode 100644 packages/docs-ui/src/views/header-footer/panel/index.module.less diff --git a/packages/docs-ui/package.json b/packages/docs-ui/package.json index 85a98211f54..602dc588603 100644 --- a/packages/docs-ui/package.json +++ b/packages/docs-ui/package.json @@ -70,7 +70,8 @@ "@univerjs/docs": "workspace:*", "@univerjs/engine-render": "workspace:*", "@univerjs/ui": "workspace:*", - "@wendellhu/redi": "0.15.5", + "@wendellhu/redi": "0.15.4", + "clsx": ">=2.0.0", "react": "^16.9.0 || ^17.0.0 || ^18.0.0", "rxjs": ">=7.0.0" }, @@ -82,7 +83,8 @@ "@univerjs/engine-render": "workspace:*", "@univerjs/shared": "workspace:*", "@univerjs/ui": "workspace:*", - "@wendellhu/redi": "0.15.5", + "@wendellhu/redi": "0.15.4", + "clsx": "^2.1.1", "less": "^4.2.0", "react": "18.3.1", "rxjs": "^7.8.1", diff --git a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts index 7ae45ba0f45..71951da91bc 100644 --- a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts @@ -20,6 +20,7 @@ import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { DocumentEditArea, IRenderManagerService, type ITextRangeWithStyle } from '@univerjs/engine-render'; import { HeaderFooterType } from '../../controllers/doc-header-footer.controller'; +import { SidebarDocHeaderFooterPanelOperation } from '../operations/doc-header-footer-panel.operation'; function getEmptyHeaderFooterBody() { return { @@ -212,3 +213,16 @@ export const CoreHeaderFooterCommand: ICommand = { return Boolean(result); }, }; + +interface IOpenHeaderFooterPanelParams { } + +export const OpenHeaderFooterPanelCommand: ICommand = { + id: 'doc.command.open-header-footer-panel', + type: CommandType.COMMAND, + + handler: async (accessor, _params: IOpenHeaderFooterPanelParams) => { + const commandService = accessor.get(ICommandService); + + return commandService.executeCommand(SidebarDocHeaderFooterPanelOperation.id, { value: 'open' }); + }, +}; diff --git a/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts b/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts new file mode 100644 index 00000000000..3d9f62c594f --- /dev/null +++ b/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICommand } from '@univerjs/core'; +import { CommandType, LocaleService } from '@univerjs/core'; +import { ISidebarService } from '@univerjs/ui'; +import type { IAccessor } from '@wendellhu/redi'; +import { COMPONENT_DOC_HEADER_FOOTER_PANEL } from '../../views/header-footer/panel/component-name'; + +export interface IUIComponentCommandParams { + value: string; +} + +export const SidebarDocHeaderFooterPanelOperation: ICommand = { + id: 'sidebar.operation.doc-header-footer-panel', + type: CommandType.COMMAND, + handler: async (accessor: IAccessor, params: IUIComponentCommandParams) => { + const sidebarService = accessor.get(ISidebarService); + const localeService = accessor.get(LocaleService); + + switch (params.value) { + case 'open': + sidebarService.open({ + header: { title: localeService.t('headerFooter.panel') }, + children: { label: COMPONENT_DOC_HEADER_FOOTER_PANEL }, + onClose: () => {}, + width: 360, + }); + break; + case 'close': + default: + sidebarService.close(); + break; + } + return true; + }, +}; + diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index d71648fc4b6..e20b7fab3f2 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -20,11 +20,14 @@ import type { Documents, DocumentViewModel, IMouseEvent, IPageRenderConfig, IPat import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; -import { IEditorService } from '@univerjs/ui'; +import { ComponentManager, IEditorService } from '@univerjs/ui'; import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; -import { CoreHeaderFooterCommand } from '../commands/commands/doc-header-footer.command'; +import { CoreHeaderFooterCommand, OpenHeaderFooterPanelCommand } from '../commands/commands/doc-header-footer.command'; +import { COMPONENT_DOC_HEADER_FOOTER_PANEL } from '../views/header-footer/panel/component-name'; +import { DocHeaderFooterPanel } from '../views/header-footer/panel/DocHeaderFooterPanel'; +import { SidebarDocHeaderFooterPanelOperation } from '../commands/operations/doc-header-footer-panel.operation'; const HEADER_FOOTER_STROKE_COLOR = 'rgba(58, 96, 247, 1)'; const HEADER_FOOTER_FILL_COLOR = 'rgba(58, 96, 247, 0.08)'; @@ -129,7 +132,8 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, @ITextSelectionRenderManager private readonly _textSelectionRenderManager: ITextSelectionRenderManager, - @Inject(LocaleService) private readonly _localeService: LocaleService + @Inject(LocaleService) private readonly _localeService: LocaleService, + @Inject(ComponentManager) private readonly _componentManager: ComponentManager ) { super(); @@ -140,10 +144,20 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu this._init(); this._drawHeaderFooterLabel(); this._registerCommands(); + this._initCustomComponents(); } private _registerCommands() { - [CoreHeaderFooterCommand].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); + [ + CoreHeaderFooterCommand, + OpenHeaderFooterPanelCommand, + SidebarDocHeaderFooterPanelOperation, + ].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command))); + } + + private _initCustomComponents(): void { + const componentManager = this._componentManager; + this.disposeWithMe(componentManager.register(COMPONENT_DOC_HEADER_FOOTER_PANEL, DocHeaderFooterPanel)); } private _init() { diff --git a/packages/docs-ui/src/controllers/doc-ui.controller.ts b/packages/docs-ui/src/controllers/doc-ui.controller.ts index 9fb24de79e8..42bce879852 100644 --- a/packages/docs-ui/src/controllers/doc-ui.controller.ts +++ b/packages/docs-ui/src/controllers/doc-ui.controller.ts @@ -42,6 +42,7 @@ import { BulletListMenuItemFactory, FontFamilySelectorMenuItemFactory, FontSizeSelectorMenuItemFactory, + HeaderFooterMenuItemFactory, ItalicMenuItemFactory, OrderListMenuItemFactory, ResetBackgroundColorMenuItemFactory, @@ -93,6 +94,7 @@ export class DocUIController extends Disposable { FontSizeSelectorMenuItemFactory, FontFamilySelectorMenuItemFactory, TextColorSelectorMenuItemFactory, + HeaderFooterMenuItemFactory, AlignLeftMenuItemFactory, AlignCenterMenuItemFactory, AlignRightMenuItemFactory, diff --git a/packages/docs-ui/src/controllers/menu/menu.ts b/packages/docs-ui/src/controllers/menu/menu.ts index b68c9398f61..f69eac3614c 100644 --- a/packages/docs-ui/src/controllers/menu/menu.ts +++ b/packages/docs-ui/src/controllers/menu/menu.ts @@ -61,6 +61,7 @@ import { Observable } from 'rxjs'; import { COLOR_PICKER_COMPONENT } from '../../components/color-picker'; import { FONT_FAMILY_COMPONENT, FONT_FAMILY_ITEM_COMPONENT } from '../../components/font-family'; import { FONT_SIZE_COMPONENT } from '../../components/font-size'; +import { OpenHeaderFooterPanelCommand } from '../../commands/commands/doc-header-footer.command'; export function BoldMenuItemFactory(accessor: IAccessor): IMenuButtonItem { const commandService = accessor.get(ICommandService); @@ -402,6 +403,18 @@ export function TextColorSelectorMenuItemFactory(accessor: IAccessor): IMenuSele }; } +export function HeaderFooterMenuItemFactory(accessor: IAccessor): IMenuButtonItem { + return { + id: OpenHeaderFooterPanelCommand.id, + group: MenuGroup.TOOLBAR_OTHERS, + type: MenuItemType.BUTTON, + icon: 'FreezeRowSingle', + tooltip: 'toolbar.headerFooter', + positions: [MenuPosition.TOOLBAR_START], + hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC), + }; +} + export function AlignLeftMenuItemFactory(accessor: IAccessor): IMenuButtonItem { const commandService = accessor.get(ICommandService); diff --git a/packages/docs-ui/src/locale/en-US.ts b/packages/docs-ui/src/locale/en-US.ts index 69a7d6410f2..167e22b8478 100644 --- a/packages/docs-ui/src/locale/en-US.ts +++ b/packages/docs-ui/src/locale/en-US.ts @@ -43,10 +43,18 @@ const locale: typeof zhCN = { alignCenter: 'Align Center', alignRight: 'Align Right', alignJustify: 'Justify', + headerFooter: 'Header & Footer', }, headerFooter: { header: 'Header', footer: 'Footer', + panel: 'Header & Footer Settings', + firstPageCheckBox: 'Different first page', + oddEvenCheckBox: 'Different odd and even pages', + headerTopMargin: 'Header top margin(px)', + footerBottomMargin: 'Footer bottom margin(px)', + closeHeaderFooter: 'Close header & footer', + disableText: 'Header & footer settings are disabled', }, }; diff --git a/packages/docs-ui/src/locale/ru-RU.ts b/packages/docs-ui/src/locale/ru-RU.ts index b77f799d0d9..288016e8d61 100644 --- a/packages/docs-ui/src/locale/ru-RU.ts +++ b/packages/docs-ui/src/locale/ru-RU.ts @@ -43,10 +43,18 @@ const locale: typeof zhCN = { alignCenter: 'Выровнять по центру', alignRight: 'Выровнять по правому краю', alignJustify: 'Выровнять по ширине', + headerFooter: 'Header & Footer', }, headerFooter: { header: 'Header', footer: 'Footer', + panel: 'Header & Footer Settings', + firstPageCheckBox: 'Different first page', + oddEvenCheckBox: 'Different odd and even pages', + headerTopMargin: 'Header top margin(px)', + footerBottomMargin: 'Footer bottom margin(px)', + closeHeaderFooter: 'Close header & footer', + disableText: 'Header & footer settings are disabled', }, }; diff --git a/packages/docs-ui/src/locale/zh-CN.ts b/packages/docs-ui/src/locale/zh-CN.ts index c359f8c4775..c469d2a459f 100644 --- a/packages/docs-ui/src/locale/zh-CN.ts +++ b/packages/docs-ui/src/locale/zh-CN.ts @@ -41,10 +41,18 @@ const locale = { alignCenter: '居中对齐', alignRight: '右对齐', alignJustify: '两端对齐', + headerFooter: '页眉页脚', }, headerFooter: { header: '页眉', footer: '页脚', + panel: '页眉页脚设置', + firstPageCheckBox: '首页不同', + oddEvenCheckBox: '奇偶页不同', + headerTopMargin: '页眉顶端距离(px)', + footerBottomMargin: '页脚底端距离(px)', + closeHeaderFooter: '关闭页眉页脚', + disableText: '页眉页脚设置不可用', }, }; diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx new file mode 100644 index 00000000000..6f1fdb2d9e3 --- /dev/null +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx @@ -0,0 +1,53 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useDependency } from '@wendellhu/redi/react-bindings'; +import React from 'react'; +import { Button, Checkbox, InputNumber } from '@univerjs/design'; +import { LocaleService } from '@univerjs/core'; +import clsx from 'clsx'; +import styles from './index.module.less'; + +export const DocHeaderFooterOptions = () => { + const localeService = useDependency(LocaleService); + + return ( +
+
+
+ { }}>{localeService.t('headerFooter.firstPageCheckBox')} +
+
+ { }}>{localeService.t('headerFooter.oddEvenCheckBox')} +
+
+
+
+ {localeService.t('headerFooter.headerTopMargin')} + {}} className={styles.optionsInput} /> +
+
+ {localeService.t('headerFooter.footerBottomMargin')} + {}} className={styles.optionsInput} /> +
+
+
+ +
+
+ ); +}; + diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx new file mode 100644 index 00000000000..37efd412e2a --- /dev/null +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx @@ -0,0 +1,28 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import styles from './index.module.less'; +import { DocHeaderFooterOptions } from './DocHeaderFooterOptions'; + +export const DocHeaderFooterPanel = () => { + return ( +
+ +
+ ); +}; + diff --git a/packages/docs-ui/src/views/header-footer/panel/component-name.ts b/packages/docs-ui/src/views/header-footer/panel/component-name.ts new file mode 100644 index 00000000000..bb1010d1857 --- /dev/null +++ b/packages/docs-ui/src/views/header-footer/panel/component-name.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const COMPONENT_DOC_HEADER_FOOTER_PANEL = 'COMPONENT_DOC_HEADER_FOOTER_PANEL'; diff --git a/packages/docs-ui/src/views/header-footer/panel/index.module.less b/packages/docs-ui/src/views/header-footer/panel/index.module.less new file mode 100644 index 00000000000..c28be36d974 --- /dev/null +++ b/packages/docs-ui/src/views/header-footer/panel/index.module.less @@ -0,0 +1,25 @@ +.panel { + padding-top: 20px; + font-size: var(--font-size-sm); +} + +.options { + &-section { + margin-top: 10px; + padding-bottom: 10px; + } + + &-form-item { + display: block; + margin-bottom: 5px; + } + + &-input { + width: 80%; + margin-top: 5px; + } + + &-margin-setting { + display: flex; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19f2424ad8e..8eb3d9f0851 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -810,8 +810,16 @@ importers: specifier: workspace:* version: link:../ui '@wendellhu/redi': +<<<<<<< HEAD specifier: 0.15.5 version: 0.15.5 +======= + specifier: 0.15.4 + version: 0.15.4 + clsx: + specifier: ^2.1.1 + version: 2.1.1 +>>>>>>> 0b94ebac1 (feat: ui of header & footer options) less: specifier: ^4.2.0 version: 4.2.0 From 0bf92f613229926361c96a6ee60561c35c873dee Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 17:34:59 +0800 Subject: [PATCH 16/39] feat: and disable status --- .../panel/DocHeaderFooterPanel.tsx | 38 ++++++++++++++++++- .../docs/view-model/document-view-model.ts | 9 ++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx index 37efd412e2a..0da2db7d45f 100644 --- a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx @@ -14,14 +14,48 @@ * limitations under the License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useDependency } from '@wendellhu/redi/react-bindings'; +import { DocSkeletonManagerService } from '@univerjs/docs'; +import { IUniverInstanceService, LocaleService } from '@univerjs/core'; +import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render'; + import styles from './index.module.less'; import { DocHeaderFooterOptions } from './DocHeaderFooterOptions'; export const DocHeaderFooterPanel = () => { + const localeService = useDependency(LocaleService); + const renderManagerService = useDependency(IRenderManagerService); + const univerInstanceService = useDependency(IUniverInstanceService); + const documentDataModel = univerInstanceService.getCurrentUniverDocInstance()!; + const unitId = documentDataModel.getUnitId()!; + const docSkeletonManagerService = renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); + + const viewModel = docSkeletonManagerService!.getViewModel(); + const [isEditHeaderFooter, setIsEditHeaderFooter] = useState(true); + + useEffect(() => { + const editArea = viewModel.getEditArea(); + setIsEditHeaderFooter(editArea !== DocumentEditArea.BODY); + + const subscription = viewModel.editAreaChange$.subscribe((editArea) => { + if (editArea == null) { + return; + } + setIsEditHeaderFooter(editArea !== DocumentEditArea.BODY); + }); + + return () => { + subscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
- + {isEditHeaderFooter + ? + :
{localeService.t('headerFooter.disableText')}
}
); }; diff --git a/packages/engine-render/src/components/docs/view-model/document-view-model.ts b/packages/engine-render/src/components/docs/view-model/document-view-model.ts index 4722430925b..80e5e440cdb 100644 --- a/packages/engine-render/src/components/docs/view-model/document-view-model.ts +++ b/packages/engine-render/src/components/docs/view-model/document-view-model.ts @@ -18,6 +18,7 @@ import type { ICustomDecorationForInterceptor, ICustomRangeForInterceptor, IDocu import { DataStreamTreeNodeType, DataStreamTreeTokenType, DocumentDataModel, toDisposable } from '@univerjs/core'; import type { IDisposable } from '@wendellhu/redi'; +import { BehaviorSubject } from 'rxjs'; import { DataStreamTreeNode } from './data-stream-tree-node'; export interface ICustomRangeInterceptor { @@ -43,6 +44,9 @@ export class DocumentViewModel implements IDisposable { private _customRangeCurrentIndex = 0; private _editArea: DocumentEditArea = DocumentEditArea.BODY; + private readonly _editAreaChange$ = new BehaviorSubject>(null); + readonly editAreaChange$ = this._editAreaChange$.asObservable(); + headerTreeMap: Map = new Map(); footerTreeMap: Map = new Map(); @@ -76,7 +80,10 @@ export class DocumentViewModel implements IDisposable { } setEditArea(editArea: DocumentEditArea) { - this._editArea = editArea; + if (editArea !== this._editArea) { + this._editArea = editArea; + this._editAreaChange$.next(editArea); + } } getPositionInParent() { From 98e0c50302bff2946938303a992fb164408e97f5 Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 21:06:22 +0800 Subject: [PATCH 17/39] feat: close header and footer --- .../commands/doc-header-footer.command.ts | 29 +++- .../doc-header-footer.controller.ts | 6 +- .../panel/DocHeaderFooterOptions.tsx | 157 +++++++++++++++++- .../panel/DocHeaderFooterPanel.tsx | 2 +- .../services/doc-skeleton-manager.service.ts | 16 +- 5 files changed, 185 insertions(+), 25 deletions(-) diff --git a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts index 71951da91bc..890d9488c71 100644 --- a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts @@ -17,7 +17,7 @@ import type { ICommand, IMutationInfo, JSONXActions } from '@univerjs/core'; import { BooleanNumber, CommandType, ICommandService, IUniverInstanceService, JSONX, Tools } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { DocSkeletonManagerService, RichTextEditingMutation, TextSelectionManagerService } from '@univerjs/docs'; import { DocumentEditArea, IRenderManagerService, type ITextRangeWithStyle } from '@univerjs/engine-render'; import { HeaderFooterType } from '../../controllers/doc-header-footer.controller'; import { SidebarDocHeaderFooterPanelOperation } from '../operations/doc-header-footer-panel.operation'; @@ -107,7 +107,7 @@ function createHeaderFooterAction(segmentId: string, createType: HeaderFooterTyp return actions; } -interface IHeaderFooterProps { +export interface IHeaderFooterProps { marginHeader?: number; // marginHeader marginFooter?: number; // marginFooter useFirstPageHeaderFooter?: BooleanNumber; // useFirstPageHeaderFooter @@ -135,6 +135,7 @@ export const CoreHeaderFooterCommand: ICommand = { const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); const renderManagerService = accessor.get(IRenderManagerService); + const textSelectionManagerService = accessor.get(TextSelectionManagerService); const { unitId, segmentId, createType, headerFooterProps } = params; const docSkeletonManagerService = renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); const docDataModel = univerInstanceService.getUniverDocInstance(unitId); @@ -148,13 +149,25 @@ export const CoreHeaderFooterCommand: ICommand = { const { documentStyle } = docDataModel.getSnapshot(); - const textRanges: ITextRangeWithStyle[] = [ - { + const activeRange = textSelectionManagerService.getActiveRange(); + const isUpdateMargin = headerFooterProps?.marginFooter != null || headerFooterProps?.marginHeader != null; + let textRanges: ITextRangeWithStyle[] = []; + + if (activeRange && isUpdateMargin) { + const { startOffset, endOffset, collapsed, style } = activeRange; + textRanges = [{ + startOffset, + endOffset, + collapsed, + style, + }]; + } else { + textRanges = [{ startOffset: 0, endOffset: 0, collapsed: true, - }, - ]; + }]; + } const doMutation: IMutationInfo = { id: RichTextEditingMutation.id, @@ -177,6 +190,10 @@ export const CoreHeaderFooterCommand: ICommand = { Object.keys(headerFooterProps).forEach((key) => { const value = headerFooterProps[key as keyof IHeaderFooterProps]; const oldValue = documentStyle[key as keyof IHeaderFooterProps]; + if (value === oldValue) { + return; + } + let action; if (oldValue === undefined) { action = jsonX.insertOp(['documentStyle', key], value); diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index e20b7fab3f2..5d1f6ed0dec 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -21,7 +21,7 @@ import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, P import { Inject } from '@wendellhu/redi'; import { ComponentManager, IEditorService } from '@univerjs/ui'; -import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; +import { DocSkeletonManagerService, neoGetDocObject, TextSelectionManagerService } from '@univerjs/docs'; import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; import { CoreHeaderFooterCommand, OpenHeaderFooterPanelCommand } from '../commands/commands/doc-header-footer.command'; @@ -132,6 +132,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, @ITextSelectionRenderManager private readonly _textSelectionRenderManager: ITextSelectionRenderManager, + @Inject(TextSelectionManagerService) private readonly _textSelectionManagerService: TextSelectionManagerService, @Inject(LocaleService) private readonly _localeService: LocaleService, @Inject(ComponentManager) private readonly _componentManager: ComponentManager ) { @@ -231,7 +232,8 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu }); } else if (headerFooterId != null) { this._textSelectionRenderManager.setSegment(headerFooterId); - // TODO: set selection to header or footer. + this._textSelectionRenderManager.setSegmentPage(pageNumber); + this._textSelectionRenderManager.setCursorManually(offsetX, offsetY); } } })); diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx index 6f1fdb2d9e3..f4031ff089a 100644 --- a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx @@ -15,37 +15,178 @@ */ import { useDependency } from '@wendellhu/redi/react-bindings'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Checkbox, InputNumber } from '@univerjs/design'; -import { LocaleService } from '@univerjs/core'; +import { BooleanNumber, ICommandService, IUniverInstanceService, LocaleService, Tools } from '@univerjs/core'; import clsx from 'clsx'; +import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager } from '@univerjs/engine-render'; +import { DocSkeletonManagerService, TextSelectionManagerService } from '@univerjs/docs'; +import { CoreHeaderFooterCommandId, type IHeaderFooterProps } from '../../../commands/commands/doc-header-footer.command'; import styles from './index.module.less'; -export const DocHeaderFooterOptions = () => { +export interface IDocHeaderFooterOptionsProps { + unitId: string; +} + +export const DocHeaderFooterOptions = (props: IDocHeaderFooterOptionsProps) => { const localeService = useDependency(LocaleService); + const univerInstanceService = useDependency(IUniverInstanceService); + const renderManagerService = useDependency(IRenderManagerService); + const commandService = useDependency(ICommandService); + const textSelectionRenderService = useDependency(ITextSelectionRenderManager); + const textSelectionManagerService = useDependency(TextSelectionManagerService); + const { unitId } = props; + + const [options, setOptions] = useState({}); + + const handleCheckboxChange = (val: boolean, type: 'useFirstPageHeaderFooter' | 'evenAndOddHeaders') => { + setOptions((prev) => ({ + ...prev, + [type]: val ? BooleanNumber.TRUE : BooleanNumber.FALSE, + })); + + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + const documentStyle = docDataModel?.getSnapshot().documentStyle; + const docSkeletonManagerService = renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); + const viewModel = docSkeletonManagerService?.getViewModel(); + + if (documentStyle == null || viewModel == null) { + return; + } + + const editArea = viewModel.getEditArea(); + + let needCreateHeaderFooter = false; + if (type === 'useFirstPageHeaderFooter' && val === true) { + if (editArea === DocumentEditArea.HEADER && !documentStyle.firstPageHeaderId) { + needCreateHeaderFooter = true; + } else if (editArea === DocumentEditArea.FOOTER && !documentStyle.firstPageFooterId) { + needCreateHeaderFooter = true; + } + } + + if (type === 'evenAndOddHeaders' && val === true) { + if (editArea === DocumentEditArea.HEADER && !documentStyle.evenPageHeaderId) { + needCreateHeaderFooter = true; + } else if (editArea === DocumentEditArea.FOOTER && !documentStyle.evenPageFooterId) { + needCreateHeaderFooter = true; + } + } + + if (needCreateHeaderFooter) { + const SEGMENT_ID_LEN = 6; + const segmentId = Tools.generateRandomId(SEGMENT_ID_LEN); + // Set segment id first, then exec command. + textSelectionRenderService.setSegment(segmentId); + commandService.executeCommand(CoreHeaderFooterCommandId, { + unitId, + segmentId, + headerFooterProps: { + [type]: val ? BooleanNumber.TRUE : BooleanNumber.FALSE, + }, + }); + } else { + commandService.executeCommand(CoreHeaderFooterCommandId, { + unitId, + headerFooterProps: { + [type]: val ? BooleanNumber.TRUE : BooleanNumber.FALSE, + }, + }); + } + }; + + const handleMarginChange = (val: number, type: 'marginHeader' | 'marginFooter') => { + setOptions((prev) => ({ + ...prev, + [type]: val, + })); + + commandService.executeCommand(CoreHeaderFooterCommandId, { + unitId, + headerFooterProps: { + [type]: val, + }, + }); + }; + + const closeHeaderFooter = () => { + const docSkeletonManagerService = renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); + const skeleton = docSkeletonManagerService?.getSkeleton(); + const viewModel = docSkeletonManagerService?.getViewModel(); + const render = renderManagerService.getRenderById(unitId); + + if (render == null || viewModel == null || skeleton == null) { + return; + } + + // TODO: @JOCS, these codes bellow should be automatically executed? + textSelectionManagerService.replaceTextRanges([]); // Clear text selection. + textSelectionRenderService.setSegment(''); + textSelectionRenderService.setSegmentPage(-1); + viewModel.setEditArea(DocumentEditArea.BODY); + skeleton.calculate(); + render.mainComponent?.makeDirty(true); + }; + + useEffect(() => { + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + const documentStyle = docDataModel?.getSnapshot().documentStyle; + + if (documentStyle) { + const { marginHeader, marginFooter, useFirstPageHeaderFooter, evenAndOddHeaders } = documentStyle; + + setOptions({ + marginHeader, + marginFooter, + useFirstPageHeaderFooter, + evenAndOddHeaders, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [unitId]); return (
- { }}>{localeService.t('headerFooter.firstPageCheckBox')} + { handleCheckboxChange(val as boolean, 'useFirstPageHeaderFooter'); }} + > + {localeService.t('headerFooter.firstPageCheckBox')} +
- { }}>{localeService.t('headerFooter.oddEvenCheckBox')} + { handleCheckboxChange(val as boolean, 'evenAndOddHeaders'); }} + > + {localeService.t('headerFooter.oddEvenCheckBox')} +
{localeService.t('headerFooter.headerTopMargin')} - {}} className={styles.optionsInput} /> + { handleMarginChange(val as number, 'marginHeader'); }} + className={styles.optionsInput} + />
{localeService.t('headerFooter.footerBottomMargin')} - {}} className={styles.optionsInput} /> + { handleMarginChange(val as number, 'marginFooter'); }} + className={styles.optionsInput} + />
- +
); diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx index 0da2db7d45f..2e31273aca0 100644 --- a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterPanel.tsx @@ -54,7 +54,7 @@ export const DocHeaderFooterPanel = () => { return (
{isEditHeaderFooter - ? + ? :
{localeService.t('headerFooter.disableText')}
}
); diff --git a/packages/docs/src/services/doc-skeleton-manager.service.ts b/packages/docs/src/services/doc-skeleton-manager.service.ts index 79332b1a6b8..134e86d72ff 100644 --- a/packages/docs/src/services/doc-skeleton-manager.service.ts +++ b/packages/docs/src/services/doc-skeleton-manager.service.ts @@ -64,6 +64,14 @@ export class DocSkeletonManagerService extends RxDisposable implements IRenderMo this._currentSkeleton$.complete(); } + getSkeleton(): DocumentSkeleton { + return this._skeleton; + } + + getViewModel(): DocumentViewModel { + return this._docViewModel; + } + private _init() { const documentDataModel = this._context.unit; this._update(documentDataModel); @@ -98,14 +106,6 @@ export class DocSkeletonManagerService extends RxDisposable implements IRenderMo this._currentViewModel$.next(this._docViewModel); } - getSkeleton(): DocumentSkeleton { - return this._skeleton; - } - - getViewModel(): DocumentViewModel { - return this._docViewModel; - } - private _buildSkeleton(documentViewModel: DocumentViewModel) { return DocumentSkeleton.create(documentViewModel, this._localeService); } From 440cb38616284e6a89f5e20738ca1e8558755bba Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 21:33:33 +0800 Subject: [PATCH 18/39] fix: page number --- .../components/docs/layout/block/paragraph/layout-ruler.ts | 2 +- .../src/components/docs/layout/doc-skeleton.ts | 6 +++--- .../engine-render/src/components/docs/layout/model/page.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts index 0d07ccf42ad..ac7d4154fd4 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts @@ -787,7 +787,7 @@ function _pageOperator( const curSkeletonPage: IDocumentSkeletonPage = getLastPage(pages); const { skeHeaders, skeFooters } = paragraphConfig; - pages.push(createSkeletonPage(ctx, sectionBreakConfig, { skeHeaders, skeFooters }, curSkeletonPage?.pageNumber)); + pages.push(createSkeletonPage(ctx, sectionBreakConfig, { skeHeaders, skeFooters }, curSkeletonPage?.pageNumber + 1)); _columnOperator(ctx, glyphGroup, pages, sectionBreakConfig, paragraphConfig, paragraphStart, breakPointType, defaultSpanLineHeight); } diff --git a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts index bfd96af1720..a5d8cdbf827 100644 --- a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts +++ b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts @@ -732,7 +732,7 @@ export class DocumentSkeleton extends Skeleton { for (let i = startSectionIndex, len = viewModel.children.length; i < len; i++) { const sectionNode = viewModel.children[i]; const sectionBreakConfig = prepareSectionBreakConfig(ctx, i); - const { sectionType, columnProperties, columnSeparatorType, sectionTypeNext } = sectionBreakConfig; + const { sectionType, columnProperties, columnSeparatorType, sectionTypeNext, pageNumberStart = 1 } = sectionBreakConfig; let curSkeletonPage = getLastPage(allSkeletonPages); let isContinuous = false; @@ -748,7 +748,7 @@ export class DocumentSkeleton extends Skeleton { ctx, sectionBreakConfig, skeletonResourceReference, - curSkeletonPage?.pageNumber + curSkeletonPage?.pageNumber ?? pageNumberStart ); } @@ -792,7 +792,7 @@ export class DocumentSkeleton extends Skeleton { updateBlockIndex(skeleton.pages); setPageParent(skeleton.pages, skeleton); - // console.log(skeleton); + return skeleton; } } diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index 707bd8ea39d..c2299b17243 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -15,7 +15,7 @@ */ import type { Nullable } from '@univerjs/core'; -import { PageOrientType } from '@univerjs/core'; +import { BooleanNumber, PageOrientType } from '@univerjs/core'; import type { IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage, @@ -83,10 +83,10 @@ export function createSkeletonPage( let headerId = defaultHeaderId ?? ''; let footerId = defaultFooterId ?? ''; - if (pageNumber === pageNumberStart && useFirstPageHeaderFooter) { + if (pageNumber === pageNumberStart && useFirstPageHeaderFooter === BooleanNumber.TRUE) { headerId = firstPageHeaderId ?? ''; footerId = firstPageFooterId ?? ''; - } else if (pageNumber % 2 === 0 && evenAndOddHeaders) { + } else if (pageNumber % 2 === 0 && evenAndOddHeaders === BooleanNumber.TRUE) { headerId = evenPageHeaderId ?? ''; footerId = evenPageFooterId ?? ''; } From 36ff9fad5650ba12a2512476a4b6dc62ad553667 Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 21:41:51 +0800 Subject: [PATCH 19/39] fix: fix single click --- .../docs-ui/src/controllers/doc-header-footer.controller.ts | 1 + .../render-controllers/text-selection.render-controller.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index 5d1f6ed0dec..b67a41b4cb3 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -218,6 +218,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu if (editArea === DocumentEditArea.BODY) { this._textSelectionRenderManager.setSegment(''); this._textSelectionRenderManager.setSegmentPage(-1); + this._textSelectionRenderManager.setCursorManually(offsetX, offsetY); } else { if (createType != null) { const SEGMENT_ID_LEN = 6; diff --git a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts index fcecb3414ba..199ae789d7f 100644 --- a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts @@ -107,7 +107,7 @@ export class DocTextSelectionRenderController extends Disposable implements IRen const viewModel = this._docSkeletonManagerService.getViewModel(); const preEditArea = viewModel.getEditArea(); - if (preEditArea !== DocumentEditArea.BODY && editArea !== preEditArea) { + if (preEditArea !== DocumentEditArea.BODY && editArea !== DocumentEditArea.BODY && editArea !== preEditArea) { viewModel.setEditArea(editArea); } } From 845bda99e08962ea738a5b599c43b080d4f3724c Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 21:54:53 +0800 Subject: [PATCH 20/39] fix: fix single click --- .../docs-ui/src/controllers/doc-header-footer.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index b67a41b4cb3..4d49d4289fd 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -178,7 +178,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu const docObject = neoGetDocObject(this._context); const { document } = docObject; - this.disposeWithMe(document.onDblclickObserver.add(async (evt: IPointerEvent | IMouseEvent) => { + this.disposeWithMe(document.onDblclick$.subscribeEvent(async (evt: IPointerEvent | IMouseEvent) => { if (this._isEditorReadOnly(unitId)) { return; } @@ -278,7 +278,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu this.disposeWithMe( toDisposable( - docsComponent.onPageRenderObservable.add((config: IPageRenderConfig) => { + docsComponent.pageRender$.subscribe((config: IPageRenderConfig) => { const viewModel = this._docSkeletonManagerService.getViewModel(); const isEditBody = viewModel.getEditArea() === DocumentEditArea.BODY; if (this._editorService.isEditor(unitId) || isEditBody) { From 20e739922529a5440e8128258fe1c137931f764c Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 21:58:49 +0800 Subject: [PATCH 21/39] fix: fix single click --- packages/engine-render/src/components/docs/document.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index 6a2decaecba..b2871150ffb 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -17,7 +17,7 @@ import './extensions'; import { CellValueType, HorizontalAlign, VerticalAlign, WrapStrategy } from '@univerjs/core'; -import type { IDocumentRenderConfig, Nullable } from '@univerjs/core'; +import type { IDocumentRenderConfig, IScale, Nullable } from '@univerjs/core'; import { Subject } from 'rxjs'; import { calculateRectRotate, getRotateOffsetAndFarthestHypotenuse } from '../../basics/draw'; @@ -32,7 +32,6 @@ import type { Scene } from '../../scene'; import type { ComponentExtension, IExtensionConfig } from '../extension'; import { DocumentsSpanAndLineExtensionRegistry } from '../extension'; import { VERTICAL_ROTATE_ANGLE } from '../../basics/text-rotation'; -import type { IScale } from '../../../../core/lib/types'; import { Liquid } from './liquid'; import type { IDocumentsConfig, IPageMarginLayout } from './doc-component'; import { DocComponent } from './doc-component'; From cdfc96efb5746ed4f946c0b44f124d562717ae8d Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 28 Jun 2024 22:14:43 +0800 Subject: [PATCH 22/39] fix: shortcuts --- .../commands/operations/select-all.operation.ts | 17 +++++++++++------ .../src/controllers/move-cursor.controller.ts | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/docs/src/commands/operations/select-all.operation.ts b/packages/docs/src/commands/operations/select-all.operation.ts index 1e815333563..59486a438ad 100644 --- a/packages/docs/src/commands/operations/select-all.operation.ts +++ b/packages/docs/src/commands/operations/select-all.operation.ts @@ -27,12 +27,17 @@ export const SelectAllOperation: ICommand = { handler: async (accessor) => { const univerInstanceService = accessor.get(IUniverInstanceService); const textSelectionManagerService = accessor.get(TextSelectionManagerService); - - const currentDoc = univerInstanceService.getCurrentUniverDocInstance(); - if (!currentDoc) return false; - - const prevBody = currentDoc.getSnapshot().body; - if (prevBody == null) return false; + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + const activeTextRange = textSelectionManagerService.getActiveRange(); + if (docDataModel == null || activeTextRange == null) { + return false; + } + + const { segmentId } = activeTextRange; + const prevBody = docDataModel.getSelfOrHeaderFooterModel(segmentId).getSnapshot().body; + if (prevBody == null) { + return false; + } const textRanges = [ { diff --git a/packages/docs/src/controllers/move-cursor.controller.ts b/packages/docs/src/controllers/move-cursor.controller.ts index ac01412a202..69f90c02b62 100644 --- a/packages/docs/src/controllers/move-cursor.controller.ts +++ b/packages/docs/src/controllers/move-cursor.controller.ts @@ -138,7 +138,7 @@ export class MoveCursorController extends Disposable { : endOffset; let focusOffset = collapsed ? endOffset : rangeDirection === RANGE_DIRECTION.FORWARD ? endOffset : startOffset; - const dataStreamLength = docDataModel.getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; + const dataStreamLength = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; if (direction === Direction.LEFT || direction === Direction.RIGHT) { const preGlyph = skeleton.findNodeByCharIndex(focusOffset - 1, segmentId, segmentPage); @@ -217,7 +217,7 @@ export class MoveCursorController extends Disposable { const { startOffset, endOffset, style, collapsed, segmentId, startNodePosition, endNodePosition, segmentPage } = activeRange; - const dataStreamLength = docDataModel.getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; + const dataStreamLength = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()!.dataStream.length ?? Number.POSITIVE_INFINITY; if (direction === Direction.LEFT || direction === Direction.RIGHT) { let cursor; From 6ecf29424708fa10993e790c9d653e870bc33200 Mon Sep 17 00:00:00 2001 From: jocs Date: Sat, 29 Jun 2024 19:57:35 +0800 Subject: [PATCH 23/39] fix: panel width --- .../commands/operations/doc-header-footer-panel.operation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts b/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts index 3d9f62c594f..f13110f15ad 100644 --- a/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts +++ b/packages/docs-ui/src/commands/operations/doc-header-footer-panel.operation.ts @@ -37,7 +37,7 @@ export const SidebarDocHeaderFooterPanelOperation: ICommand = { header: { title: localeService.t('headerFooter.panel') }, children: { label: COMPONENT_DOC_HEADER_FOOTER_PANEL }, onClose: () => {}, - width: 360, + width: 400, }); break; case 'close': From bb10ce38992980d95f4b2d5d6b7787db752b64f2 Mon Sep 17 00:00:00 2001 From: jocs Date: Sat, 29 Jun 2024 20:37:01 +0800 Subject: [PATCH 24/39] fix: clipboard --- .../services/clipboard/clipboard.service.ts | 15 ++++-- .../text-selection-render-manager.ts | 53 +++++++++++-------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/docs-ui/src/services/clipboard/clipboard.service.ts b/packages/docs-ui/src/services/clipboard/clipboard.service.ts index 8cb4fd9fa43..2344f50e2e6 100644 --- a/packages/docs-ui/src/services/clipboard/clipboard.service.ts +++ b/packages/docs-ui/src/services/clipboard/clipboard.service.ts @@ -236,14 +236,21 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ private _getDocumentBodyInRanges(): IDocumentBody[] { const ranges = this._textSelectionManagerService.getSelections(); - const doc = this._univerInstanceService.getCurrentUniverDocInstance(); + const activeRange = this._textSelectionManagerService.getActiveRange(); + const docDataModel = this._univerInstanceService.getCurrentUniverDocInstance(); const results: IDocumentBody[] = []; - const body = doc?.getBody(); + const body = docDataModel?.getBody(); - if (ranges == null || !doc || !body) { + if (ranges == null || docDataModel == null || body == null) { return results; } + if (activeRange == null) { + return results; + } + + const { segmentId } = activeRange; + for (const range of ranges) { const { startOffset, endOffset, collapsed } = range; @@ -256,7 +263,7 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ } const deleteRange = getDeleteSelection({ startOffset, endOffset, collapsed }, body); - const docBody = doc.sliceBody(deleteRange.startOffset, deleteRange.endOffset); + const docBody = docDataModel.getSelfOrHeaderFooterModel(segmentId).sliceBody(deleteRange.startOffset, deleteRange.endOffset); if (docBody == null) { continue; } diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index cc4dcb4e03f..7c3138de96b 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -287,6 +287,35 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel this._updateInputPosition(); } + setCursorManually(evtOffsetX: number, evtOffsetY: number) { + const startNode = this._findNodeByCoord(evtOffsetX, evtOffsetY); + + const position = this._getNodePosition(startNode); + + if (position == null) { + this._removeAllTextRanges(); + + return; + } + + if (startNode?.node.streamType === DataStreamTreeTokenType.PARAGRAPH) { + position.isBack = true; + } + + // TODO: @Jocs It's better to create a new textRange after remove all text ranges? because segment id will change. + this._updateTextRangeAnchorPosition(position); + + this._activeSelectionRefresh(); + + this._textSelectionInner$.next({ + textRanges: this._getAllTextRanges(), + segmentId: this._currentSegmentId, + segmentPage: this._currentSegmentPage, + style: this._selectionStyle, + isEditing: false, + }); + } + // Sync canvas selection to dom selection. sync() { this._updateInputPosition(); @@ -426,26 +455,6 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel this.addTextRanges(textRanges, false); } - setCursorManually(evtOffsetX: number, evtOffsetY: number) { - const startNode = this._findNodeByCoord(evtOffsetX, evtOffsetY); - - const position = this._getNodePosition(startNode); - - if (position == null) { - this._removeAllTextRanges(); - - return; - } - - if (startNode?.node.streamType === DataStreamTreeTokenType.PARAGRAPH) { - position.isBack = true; - } - - this._updateTextRangeAnchorPosition(position); - - this._activeSelectionRefresh(); - } - // Handle pointer down. eventTrigger(evt: IPointerEvent | IMouseEvent) { if (!this._scene || !this._isSelectionEnabled) { @@ -467,11 +476,11 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel } const { segmentId, segmentPage } = startNode; - if (segmentId !== this._currentSegmentId) { + if (segmentId && this._currentSegmentId && segmentId !== this._currentSegmentId) { this.setSegment(segmentId); } - if (segmentPage !== this._currentSegmentPage) { + if (segmentId && segmentPage !== this._currentSegmentPage) { this.setSegmentPage(segmentPage); } From e03652211c7003448923be996951f91173793566 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 14:45:46 +0800 Subject: [PATCH 25/39] fix: header footer in zen mode --- .../src/data/docs/default-document-data-cn.ts | 69 +------------------ .../docs/default-document-data-dreamer.ts | 2 + .../src/data/docs/default-document-data-en.ts | 2 + .../src/data/docs/default-document-data.ts | 2 + .../data/docs/default-document.data-simple.ts | 3 +- .../src/types/interfaces/i-document-data.ts | 7 ++ .../doc-header-footer.controller.ts | 15 +++- packages/docs-ui/src/controllers/menu/menu.ts | 7 +- .../editor-container/EditorContainer.tsx | 6 +- .../src/views/formula-bar/FormulaBar.tsx | 3 +- .../src/controllers/zen-editor.controller.ts | 2 + packages/slides/src/basics/demo-data.ts | 2 + .../ui/src/common/menu-hidden-observable.ts | 31 ++++++++- packages/ui/src/index.ts | 2 +- packages/ui/src/utils/cell.ts | 1 + 15 files changed, 76 insertions(+), 78 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index b5627963c6c..33e6277f8e0 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -15,75 +15,9 @@ */ import type { IDocumentData } from '@univerjs/core'; -import { BooleanNumber, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType, WrapTextType } from '@univerjs/core'; +import { BooleanNumber, DocumentFlavor, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType, WrapTextType } from '@univerjs/core'; import { ptToPixel } from '@univerjs/engine-render'; -function getDefaultHeaderFooterBody(type: 'header' | 'footer') { - return { - dataStream: type === 'header' ? '苍茫夜色\r作者:朱自清\rToday Office\r我是页眉页眉\r\n' : '苍茫月色\r作者:朱自清\rToday Office\r我是页脚页脚\r\n', - textRuns: [ - { - st: 0, - ed: 4, - ts: { - fs: 10, - ff: 'Microsoft YaHei', - cl: { - rgb: 'rgb(155, 155, 0)', - }, - bl: BooleanNumber.TRUE, - ul: { - s: BooleanNumber.TRUE, - }, - }, - }, - { - st: 5, - ed: 31, - ts: { - fs: 10, - ff: 'Times New Roman', - cl: { - rgb: 'rgb(30, 30, 30)', - }, - bl: BooleanNumber.FALSE, - }, - }, - ], - paragraphs: [ - { - startIndex: 4, - spaceAbove: 0, - lineSpacing: 1.5, - spaceBelow: 0, - }, - { - startIndex: 11, - spaceAbove: 0, - lineSpacing: 1.5, - spaceBelow: 0, - }, - { - startIndex: 24, - spaceAbove: 0, - lineSpacing: 1.5, - spaceBelow: 0, - }, - { - startIndex: 31, - spaceAbove: 0, - lineSpacing: 1.5, - spaceBelow: 0, - }, - ], - sectionBreaks: [ - { - startIndex: 32, - }, - ], - }; -} - export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { id: 'd', drawings: { @@ -851,6 +785,7 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { width: ptToPixel(595), height: ptToPixel(842), }, + documentFlavor: DocumentFlavor.TRADITIONAL, marginTop: ptToPixel(50), marginBottom: ptToPixel(50), marginRight: ptToPixel(50), diff --git a/examples/src/data/docs/default-document-data-dreamer.ts b/examples/src/data/docs/default-document-data-dreamer.ts index aa36784a3a0..215f58b7595 100644 --- a/examples/src/data/docs/default-document-data-dreamer.ts +++ b/examples/src/data/docs/default-document-data-dreamer.ts @@ -18,6 +18,7 @@ import type { IDocumentData } from '@univerjs/core'; import { BooleanNumber, ColumnSeparatorType, + DocumentFlavor, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, @@ -275,6 +276,7 @@ export const DEFAULT_DOCUMENT_DATA_DREAMER: IDocumentData = { width: ptToPixel(595), height: ptToPixel(842), }, + documentFlavor: DocumentFlavor.TRADITIONAL, marginTop: ptToPixel(50), marginBottom: ptToPixel(50), marginRight: ptToPixel(50), diff --git a/examples/src/data/docs/default-document-data-en.ts b/examples/src/data/docs/default-document-data-en.ts index be17c866492..8afaa07db1f 100644 --- a/examples/src/data/docs/default-document-data-en.ts +++ b/examples/src/data/docs/default-document-data-en.ts @@ -18,6 +18,7 @@ import type { IDocumentData } from '@univerjs/core'; import { BooleanNumber, ColumnSeparatorType, + DocumentFlavor, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, @@ -445,6 +446,7 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = { width: ptToPixel(595), height: ptToPixel(842), }, + documentFlavor: DocumentFlavor.TRADITIONAL, marginTop: ptToPixel(50), marginBottom: ptToPixel(50), marginRight: ptToPixel(50), diff --git a/examples/src/data/docs/default-document-data.ts b/examples/src/data/docs/default-document-data.ts index e71dba10393..80409152497 100644 --- a/examples/src/data/docs/default-document-data.ts +++ b/examples/src/data/docs/default-document-data.ts @@ -19,6 +19,7 @@ import { BaselineOffset, BooleanNumber, ColumnSeparatorType, + DocumentFlavor, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, @@ -203,6 +204,7 @@ export const DEFAULT_DOCUMENT_DATA: IDocumentData = { width: 594.3, height: 840.51, }, + documentFlavor: DocumentFlavor.TRADITIONAL, marginTop: 72, marginBottom: 72, marginRight: 90, diff --git a/examples/src/data/docs/default-document.data-simple.ts b/examples/src/data/docs/default-document.data-simple.ts index 0161a04f533..771fbce8482 100644 --- a/examples/src/data/docs/default-document.data-simple.ts +++ b/examples/src/data/docs/default-document.data-simple.ts @@ -15,7 +15,7 @@ */ import type { IDocumentData } from '@univerjs/core'; -import { BooleanNumber } from '@univerjs/core'; +import { BooleanNumber, DocumentFlavor } from '@univerjs/core'; export const DEFAULT_DOCUMENT_DATA_SIMPLE: IDocumentData = { id: 'default-document-id', @@ -89,6 +89,7 @@ export const DEFAULT_DOCUMENT_DATA_SIMPLE: IDocumentData = { width: 595, height: 842, }, + documentFlavor: DocumentFlavor.TRADITIONAL, marginTop: 50, marginBottom: 50, marginRight: 40, diff --git a/packages/core/src/types/interfaces/i-document-data.ts b/packages/core/src/types/interfaces/i-document-data.ts index ca17b6be2da..5f5cd656a96 100644 --- a/packages/core/src/types/interfaces/i-document-data.ts +++ b/packages/core/src/types/interfaces/i-document-data.ts @@ -333,6 +333,11 @@ export interface IHeaderAndFooterBase { evenAndOddHeaders?: BooleanNumber; // useEvenPageHeaderFooter, } +export enum DocumentFlavor { + TRADITIONAL, + MODERN, +} + /** * Basics properties of doc style */ @@ -342,6 +347,8 @@ export interface IDocStyleBase extends IMargin { pageOrient?: PageOrientType; + documentFlavor?: DocumentFlavor; // DocumentFlavor: TRADITIONAL, MODERN + marginHeader?: number; // marginHeader marginFooter?: number; // marginFooter diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index 4d49d4289fd..d862c9fdf70 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -15,13 +15,13 @@ */ import type { DocumentDataModel } from '@univerjs/core'; -import { BooleanNumber, Disposable, ICommandService, IUniverInstanceService, LocaleService, toDisposable, Tools } from '@univerjs/core'; +import { BooleanNumber, Disposable, DocumentFlavor, ICommandService, IUniverInstanceService, LocaleService, toDisposable, Tools } from '@univerjs/core'; import type { Documents, DocumentViewModel, IMouseEvent, IPageRenderConfig, IPathProps, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; import { ComponentManager, IEditorService } from '@univerjs/ui'; -import { DocSkeletonManagerService, neoGetDocObject, TextSelectionManagerService } from '@univerjs/docs'; +import { DocSkeletonManagerService, neoGetDocObject } from '@univerjs/docs'; import type { Nullable } from 'vitest'; import { TextBubbleShape } from '../views/header-footer/text-bubble'; import { CoreHeaderFooterCommand, OpenHeaderFooterPanelCommand } from '../commands/commands/doc-header-footer.command'; @@ -132,7 +132,6 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, @ITextSelectionRenderManager private readonly _textSelectionRenderManager: ITextSelectionRenderManager, - @Inject(TextSelectionManagerService) private readonly _textSelectionManagerService: TextSelectionManagerService, @Inject(LocaleService) private readonly _localeService: LocaleService, @Inject(ComponentManager) private readonly _componentManager: ComponentManager ) { @@ -142,6 +141,16 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu } private _initialize() { + // FIXME: @Jocs, NO need to register this controller in Modern Document??? + const docDataModel = this._context.unit; + + const documentFlavor = docDataModel.getSnapshot().documentStyle.documentFlavor; + + // Only traditional document support header/footer. + if (documentFlavor !== DocumentFlavor.TRADITIONAL) { + return; + } + this._init(); this._drawHeaderFooterLabel(); this._registerCommands(); diff --git a/packages/docs-ui/src/controllers/menu/menu.ts b/packages/docs-ui/src/controllers/menu/menu.ts index f69eac3614c..0a56a472e88 100644 --- a/packages/docs-ui/src/controllers/menu/menu.ts +++ b/packages/docs-ui/src/controllers/menu/menu.ts @@ -50,13 +50,14 @@ import type { IMenuButtonItem, IMenuSelectorItem } from '@univerjs/ui'; import { FONT_FAMILY_LIST, FONT_SIZE_LIST, + getHeaderFooterMenuHiddenObservable, getMenuHiddenObservable, MenuGroup, MenuItemType, MenuPosition, } from '@univerjs/ui'; import type { IAccessor } from '@wendellhu/redi'; -import { Observable } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; import { COLOR_PICKER_COMPONENT } from '../../components/color-picker'; import { FONT_FAMILY_COMPONENT, FONT_FAMILY_ITEM_COMPONENT } from '../../components/font-family'; @@ -411,7 +412,9 @@ export function HeaderFooterMenuItemFactory(accessor: IAccessor): IMenuButtonIte icon: 'FreezeRowSingle', tooltip: 'toolbar.headerFooter', positions: [MenuPosition.TOOLBAR_START], - hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC), + hidden$: combineLatest(getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC), getHeaderFooterMenuHiddenObservable(accessor), (one, two) => { + return one || two; + }), }; } diff --git a/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx b/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx index c8f376bcd36..f15573f6ee8 100644 --- a/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx +++ b/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx @@ -15,7 +15,7 @@ */ import type { IDocumentData } from '@univerjs/core'; -import { DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, IContextService } from '@univerjs/core'; +import { DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DocumentFlavor, IContextService } from '@univerjs/core'; import { useDependency } from '@wendellhu/redi/react-bindings'; import React, { useEffect, useState } from 'react'; @@ -67,7 +67,9 @@ export const EditorContainer: React.FC = () => { }, ], }, - documentStyle: {}, + documentStyle: { + documentFlavor: DocumentFlavor.MODERN, + }, }; useEffect(() => { diff --git a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx index 49999d01597..3f158556709 100644 --- a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx +++ b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx @@ -15,7 +15,7 @@ */ import type { Nullable, Workbook } from '@univerjs/core'; -import { BooleanNumber, DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, HorizontalAlign, IPermissionService, IUniverInstanceService, Rectangle, ThemeService, UniverInstanceType, VerticalAlign, WrapStrategy } from '@univerjs/core'; +import { BooleanNumber, DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DocumentFlavor, HorizontalAlign, IPermissionService, IUniverInstanceService, Rectangle, ThemeService, UniverInstanceType, VerticalAlign, WrapStrategy } from '@univerjs/core'; import { DeviceInputEventType } from '@univerjs/engine-render'; import { CheckMarkSingle, CloseSingle, DropdownSingle, FxSingle } from '@univerjs/icons'; import { KeyCode, ProgressBar, TextEditor } from '@univerjs/ui'; @@ -115,6 +115,7 @@ export function FormulaBar() { width: Number.POSITIVE_INFINITY, height: Number.POSITIVE_INFINITY, }, + documentFlavor: DocumentFlavor.MODERN, marginTop: 5, marginBottom: 5, marginRight: 0, diff --git a/packages/sheets-zen-editor/src/controllers/zen-editor.controller.ts b/packages/sheets-zen-editor/src/controllers/zen-editor.controller.ts index 91289b7a12f..e477ec837f2 100644 --- a/packages/sheets-zen-editor/src/controllers/zen-editor.controller.ts +++ b/packages/sheets-zen-editor/src/controllers/zen-editor.controller.ts @@ -19,6 +19,7 @@ import { DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + DocumentFlavor, ICommandService, IUndoRedoService, IUniverInstanceService, @@ -92,6 +93,7 @@ export class ZenEditorController extends RxDisposable { width: 595, height: 842, }, + documentFlavor: DocumentFlavor.MODERN, marginTop: 50, marginBottom: 50, marginRight: 40, diff --git a/packages/slides/src/basics/demo-data.ts b/packages/slides/src/basics/demo-data.ts index 9f85a66fd2f..05d52211867 100644 --- a/packages/slides/src/basics/demo-data.ts +++ b/packages/slides/src/basics/demo-data.ts @@ -19,6 +19,7 @@ import { BaselineOffset, BooleanNumber, ColumnSeparatorType, + DocumentFlavor, DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, @@ -203,6 +204,7 @@ export const docsDemoData: IDocumentData = { width: 594.3, height: 840.51, }, + documentFlavor: DocumentFlavor.MODERN, marginTop: 72, marginBottom: 72, marginRight: 90, diff --git a/packages/ui/src/common/menu-hidden-observable.ts b/packages/ui/src/common/menu-hidden-observable.ts index ee4acfc32ff..d84fb022a03 100644 --- a/packages/ui/src/common/menu-hidden-observable.ts +++ b/packages/ui/src/common/menu-hidden-observable.ts @@ -15,7 +15,7 @@ */ import type { UniverInstanceType } from '@univerjs/core'; -import { IUniverInstanceService } from '@univerjs/core'; +import { DocumentFlavor, IUniverInstanceService } from '@univerjs/core'; import type { IAccessor } from '@wendellhu/redi'; import { Observable } from 'rxjs'; @@ -47,3 +47,32 @@ export function getMenuHiddenObservable( return () => subscription.unsubscribe(); }); } + +export function getHeaderFooterMenuHiddenObservable( + accessor: IAccessor +): Observable { + const univerInstanceService = accessor.get(IUniverInstanceService); + + return new Observable((subscriber) => { + const subscription = univerInstanceService.focused$.subscribe((unitId) => { + if (unitId == null) { + return subscriber.next(true); + } + const docDataModel = univerInstanceService.getUniverDocInstance(unitId); + const documentFlavor = docDataModel?.getSnapshot().documentStyle.documentFlavor; + + subscriber.next(documentFlavor !== DocumentFlavor.TRADITIONAL); + }); + + const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + + if (docDataModel == null) { + return subscriber.next(true); + } + + const documentFlavor = docDataModel?.getSnapshot().documentStyle.documentFlavor; + subscriber.next(documentFlavor !== DocumentFlavor.TRADITIONAL); + + return () => subscription.unsubscribe(); + }); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 63e000efc84..dcbd93972e2 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -15,7 +15,7 @@ */ export * from './common'; -export { getMenuHiddenObservable } from './common/menu-hidden-observable'; +export { getMenuHiddenObservable, getHeaderFooterMenuHiddenObservable } from './common/menu-hidden-observable'; export * from './components'; export { t } from './components/hooks/locale'; export { useObservable } from './components/hooks/observable'; diff --git a/packages/ui/src/utils/cell.ts b/packages/ui/src/utils/cell.ts index a3e120f3af0..7ccca7f2f9d 100644 --- a/packages/ui/src/utils/cell.ts +++ b/packages/ui/src/utils/cell.ts @@ -41,6 +41,7 @@ export const DEFAULT_BACKGROUND_COLOR_RGB = 'rgb(0,0,0)'; * @param $dom * @returns */ +// eslint-disable-next-line max-lines-per-function export function handleDomToJson($dom: HTMLElement): IDocumentData | string { let nodeList = $dom.childNodes; // skip container itself From 7d1d24dbee6baf56ca7d45c41c5292ef3db5552e Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 15:24:22 +0800 Subject: [PATCH 26/39] fix: footer header margin --- .../commands/doc-header-footer.command.ts | 31 ++++++------------- .../panel/DocHeaderFooterOptions.tsx | 12 +++++-- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts index 890d9488c71..1019e07acb5 100644 --- a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts @@ -17,7 +17,7 @@ import type { ICommand, IMutationInfo, JSONXActions } from '@univerjs/core'; import { BooleanNumber, CommandType, ICommandService, IUniverInstanceService, JSONX, Tools } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import { DocSkeletonManagerService, RichTextEditingMutation, TextSelectionManagerService } from '@univerjs/docs'; +import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { DocumentEditArea, IRenderManagerService, type ITextRangeWithStyle } from '@univerjs/engine-render'; import { HeaderFooterType } from '../../controllers/doc-header-footer.controller'; import { SidebarDocHeaderFooterPanelOperation } from '../operations/doc-header-footer-panel.operation'; @@ -135,7 +135,6 @@ export const CoreHeaderFooterCommand: ICommand = { const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); const renderManagerService = accessor.get(IRenderManagerService); - const textSelectionManagerService = accessor.get(TextSelectionManagerService); const { unitId, segmentId, createType, headerFooterProps } = params; const docSkeletonManagerService = renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); const docDataModel = univerInstanceService.getUniverDocInstance(unitId); @@ -149,26 +148,12 @@ export const CoreHeaderFooterCommand: ICommand = { const { documentStyle } = docDataModel.getSnapshot(); - const activeRange = textSelectionManagerService.getActiveRange(); const isUpdateMargin = headerFooterProps?.marginFooter != null || headerFooterProps?.marginHeader != null; - let textRanges: ITextRangeWithStyle[] = []; - - if (activeRange && isUpdateMargin) { - const { startOffset, endOffset, collapsed, style } = activeRange; - textRanges = [{ - startOffset, - endOffset, - collapsed, - style, - }]; - } else { - textRanges = [{ - startOffset: 0, - endOffset: 0, - collapsed: true, - }]; - } - + const textRanges: ITextRangeWithStyle[] = [{ + startOffset: 0, + endOffset: 0, + collapsed: true, + }]; const doMutation: IMutationInfo = { id: RichTextEditingMutation.id, params: { @@ -179,6 +164,10 @@ export const CoreHeaderFooterCommand: ICommand = { }, }; + if (isUpdateMargin) { + doMutation.params.noNeedSetTextRange = true; + } + const jsonX = JSONX.getInstance(); const rawActions: JSONXActions = []; diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx index f4031ff089a..64776028912 100644 --- a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx @@ -95,18 +95,22 @@ export const DocHeaderFooterOptions = (props: IDocHeaderFooterOptionsProps) => { } }; - const handleMarginChange = (val: number, type: 'marginHeader' | 'marginFooter') => { + const handleMarginChange = async (val: number, type: 'marginHeader' | 'marginFooter') => { setOptions((prev) => ({ ...prev, [type]: val, })); - commandService.executeCommand(CoreHeaderFooterCommandId, { + await commandService.executeCommand(CoreHeaderFooterCommandId, { unitId, headerFooterProps: { [type]: val, }, }); + + // To make sure input always has focus. + textSelectionRenderService.removeAllTextRanges(); + textSelectionRenderService.blur(); }; const closeHeaderFooter = () => { @@ -169,6 +173,8 @@ export const DocHeaderFooterOptions = (props: IDocHeaderFooterOptionsProps) => {
{localeService.t('headerFooter.headerTopMargin')} { handleMarginChange(val as number, 'marginHeader'); }} @@ -178,6 +184,8 @@ export const DocHeaderFooterOptions = (props: IDocHeaderFooterOptionsProps) => {
{localeService.t('headerFooter.footerBottomMargin')} { handleMarginChange(val as number, 'marginFooter'); }} From 2a88990015525af0ff701be2e791dba88a501718 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 16:06:55 +0800 Subject: [PATCH 27/39] fix: background --- .../doc-header-footer.controller.ts | 125 +++++++++++------- .../src/components/docs/document.ts | 43 +++--- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index d862c9fdf70..f6bfe9d282b 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -17,7 +17,7 @@ import type { DocumentDataModel } from '@univerjs/core'; import { BooleanNumber, Disposable, DocumentFlavor, ICommandService, IUniverInstanceService, LocaleService, toDisposable, Tools } from '@univerjs/core'; import type { Documents, DocumentViewModel, IMouseEvent, IPageRenderConfig, IPathProps, IPointerEvent, IRenderContext, IRenderModule, RenderComponentType } from '@univerjs/engine-render'; -import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, PageLayoutType, Path, Vector2 } from '@univerjs/engine-render'; +import { DocumentEditArea, IRenderManagerService, ITextSelectionRenderManager, PageLayoutType, Path, Rect, Vector2 } from '@univerjs/engine-render'; import { Inject } from '@wendellhu/redi'; import { ComponentManager, IEditorService } from '@univerjs/ui'; @@ -267,6 +267,8 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu // eslint-disable-next-line max-lines-per-function private _drawHeaderFooterLabel() { const localeService = this._localeService; + + // eslint-disable-next-line max-lines-per-function this._renderManagerService.currentRender$.subscribe((unitId) => { if (unitId == null) { return; @@ -287,59 +289,92 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu this.disposeWithMe( toDisposable( + // eslint-disable-next-line max-lines-per-function docsComponent.pageRender$.subscribe((config: IPageRenderConfig) => { - const viewModel = this._docSkeletonManagerService.getViewModel(); - const isEditBody = viewModel.getEditArea() === DocumentEditArea.BODY; - if (this._editorService.isEditor(unitId) || isEditBody) { + if (this._editorService.isEditor(unitId)) { return; } - - // Draw page borders + const viewModel = this._docSkeletonManagerService.getViewModel(); + const editArea = viewModel.getEditArea(); + const isEditBody = editArea === DocumentEditArea.BODY; const { page, pageLeft, pageTop, ctx } = config; - const { pageWidth, pageHeight, marginTop, marginBottom } = page; + // Draw header footer label. ctx.save(); - ctx.translate(pageLeft - 0.5, pageTop - 0.5); - const headerPathConfigIPathProps = { - dataArray: [{ - command: 'M', - points: [0, marginTop], - }, { - command: 'L', - points: [pageWidth, marginTop], - }] as unknown as IPathProps['dataArray'], - strokeWidth: 1, - stroke: HEADER_FOOTER_STROKE_COLOR, - }; - - const footerPathConfigIPathProps = { - dataArray: [{ - command: 'M', - points: [0, pageHeight - marginBottom], - }, { - command: 'L', - points: [pageWidth, pageHeight - marginBottom], - }] as unknown as IPathProps['dataArray'], - strokeWidth: 1, - stroke: HEADER_FOOTER_STROKE_COLOR, - }; - - Path.drawWith(ctx, headerPathConfigIPathProps); - Path.drawWith(ctx, footerPathConfigIPathProps); - - ctx.translate(0, marginTop + 1); - TextBubbleShape.drawWith(ctx, { - text: localeService.t('headerFooter.header'), - color: HEADER_FOOTER_FILL_COLOR, - }); - ctx.translate(0, pageHeight - marginTop - marginBottom); - TextBubbleShape.drawWith(ctx, { - text: localeService.t('headerFooter.footer'), - color: HEADER_FOOTER_FILL_COLOR, - }); + // Cover header and footer. + if (isEditBody) { + Rect.drawWith(ctx, { + left: 0, + top: 0, + width: pageWidth, + height: marginTop, + fill: 'rgba(255, 255, 255, 0.5)', + }); + ctx.save(); + ctx.translate(0, pageHeight - marginBottom); + Rect.drawWith(ctx, { + left: 0, + top: 0, + width: pageWidth, + height: marginBottom, + fill: 'rgba(255, 255, 255, 0.5)', + }); + ctx.restore(); + } else { // Cover body. + ctx.save(); + ctx.translate(0, marginTop); + Rect.drawWith(ctx, { + left: 0, + top: marginTop, + width: pageWidth, + height: pageHeight - marginTop - marginBottom, + fill: 'rgba(255, 255, 255, 0.5)', + }); + ctx.restore(); + } + + if (!isEditBody) { + const headerPathConfigIPathProps = { + dataArray: [{ + command: 'M', + points: [0, marginTop], + }, { + command: 'L', + points: [pageWidth, marginTop], + }] as unknown as IPathProps['dataArray'], + strokeWidth: 1, + stroke: HEADER_FOOTER_STROKE_COLOR, + }; + + const footerPathConfigIPathProps = { + dataArray: [{ + command: 'M', + points: [0, pageHeight - marginBottom], + }, { + command: 'L', + points: [pageWidth, pageHeight - marginBottom], + }] as unknown as IPathProps['dataArray'], + strokeWidth: 1, + stroke: HEADER_FOOTER_STROKE_COLOR, + }; + + Path.drawWith(ctx, headerPathConfigIPathProps); + Path.drawWith(ctx, footerPathConfigIPathProps); + + ctx.translate(0, marginTop + 1); + TextBubbleShape.drawWith(ctx, { + text: localeService.t('headerFooter.header'), + color: HEADER_FOOTER_FILL_COLOR, + }); + ctx.translate(0, pageHeight - marginTop - marginBottom); + TextBubbleShape.drawWith(ctx, { + text: localeService.t('headerFooter.footer'), + color: HEADER_FOOTER_FILL_COLOR, + }); + } ctx.restore(); }) ) diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index b2871150ffb..ae6ef86c876 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -18,7 +18,6 @@ import './extensions'; import { CellValueType, HorizontalAlign, VerticalAlign, WrapStrategy } from '@univerjs/core'; import type { IDocumentRenderConfig, IScale, Nullable } from '@univerjs/core'; - import { Subject } from 'rxjs'; import { calculateRectRotate, getRotateOffsetAndFarthestHypotenuse } from '../../basics/draw'; import type { IDocumentSkeletonGlyph, IDocumentSkeletonLine, IDocumentSkeletonPage } from '../../basics/i-document-skeleton-cached'; @@ -228,19 +227,12 @@ export class Documents extends DocComponent { ); } - this._pageRender$.next({ - page, - pageLeft, - pageTop, - ctx, - }); - this._startRotation(ctx, finalAngle); - if (!isEditBody) { - ctx.save(); - ctx.globalAlpha = 0.5; - } + // if (!isEditBody) { + // ctx.save(); + // ctx.globalAlpha = 0.5; + // } for (const section of sections) { const { columns } = section; @@ -437,9 +429,9 @@ export class Documents extends DocComponent { } } - if (!isEditBody) { - ctx.restore(); - } + // if (!isEditBody) { + // ctx.restore(); + // } this._resetRotation(ctx, finalAngle); @@ -465,6 +457,13 @@ export class Documents extends DocComponent { ); } + this._pageRender$.next({ + page, + pageLeft, + pageTop, + ctx, + }); + const { x, y } = this._drawLiquid.translatePage( page, this.pageLayoutType, @@ -494,10 +493,10 @@ export class Documents extends DocComponent { const editArea = this.getSkeleton()?.getViewModel().getEditArea(); const isEditHeaderFooter = editArea === DocumentEditArea.HEADER || editArea === DocumentEditArea.FOOTER; - if (!isEditHeaderFooter) { - ctx.save(); - ctx.globalAlpha = 0.5; - } + // if (!isEditHeaderFooter) { + // ctx.save(); + // ctx.globalAlpha = 0.5; + // } const { sections } = page; for (const section of sections) { @@ -642,9 +641,9 @@ export class Documents extends DocComponent { this._drawLiquid.translateRestore(); } - if (!isEditHeaderFooter) { - ctx.restore(); - } + // if (!isEditHeaderFooter) { + // ctx.restore(); + // } } private _horizontalHandler( From 266f690e041176fe5ba812557865cd47a013a611 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 17:34:19 +0800 Subject: [PATCH 28/39] fix: list --- .../src/data/docs/default-document-data-cn.ts | 22 ++----------------- .../layout/block/paragraph/layout-ruler.ts | 2 +- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/examples/src/data/docs/default-document-data-cn.ts b/examples/src/data/docs/default-document-data-cn.ts index 33e6277f8e0..7840deb81e0 100644 --- a/examples/src/data/docs/default-document-data-cn.ts +++ b/examples/src/data/docs/default-document-data-cn.ts @@ -163,26 +163,8 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { 'shapeTest4', 'shapeTest5', ], - headers: { - // defaultHeaderId: { - // headerId: 'defaultHeaderId', - // body: getDefaultHeaderFooterBody('header'), - // }, - // evenHeaderId: { - // }, - // firstPageHeaderId: { - // } - }, - footers: { - // defaultFooterId: { - // footerId: 'defaultFooterId', - // body: getDefaultHeaderFooterBody('footer'), - // }, - // evenFooterId: { - // }, - // firstPageFooterId: { - // } - }, + headers: {}, + footers: {}, body: { dataStream: '荷塘月色\r\r作者:朱自清\r\r这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。\r\r沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁的。路图片一\b是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。\r\r路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常的自己,到了另一个世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话\b现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。\r\r曲曲折折的荷塘上面,弥望的是田田的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星\b又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉的流水,遮住了,不能见一些颜色;而叶子却更见风致了。\r\r月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样\b又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲上奏着的名曲。\r\r荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的\b是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。\r\r忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:\r\r于是妖童女,荡舟心许;鷁首徐回,兼传羽杯;櫂将移而藻挂,船欲动而萍开。尔其纤腰束素,迁延顾步;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾。\r\r可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。\r\r于是又记起,《西洲曲》里的句子:\r\r采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。\r\r今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。\r\r一九二七年七月,北京清华园。\r\r\r\r《荷塘月色》语言朴素典雅,准确生动,贮满诗意,满溢着朱自清的散文语言一贯有朴素的美,不用浓墨重彩,画的是淡墨水彩。\r\r朱自清先生一笔写景一笔说情,看起来松散不知所云,可仔细体会下,就能感受到先生在字里行间表述出的苦闷,而随之读者也被先生的文字所感染,被带进了他当时那苦闷而无法明喻的心情。这就是优异散文的必须品质之一。\r\r扩展资料:\r一首长诗《毁灭》奠定了朱自清在文坛新诗人的地位,而《桨声灯影里的秦淮河》则被公认为白话美文的典范。朱自清用白话美文向复古派宣战,有力地回击了复古派“白话不能作美文”之说,他是“五四”新文学运动的开拓者之一。\r\r朱自清的美文影响了一代又一代人。作家贾平凹说:来到扬州,第一个想到的人是朱自清,他是知识分子中最最了不起的人物。\r\r实际上,朱自清的写作路程是非常曲折的,他早期的时候大多数作品都是诗歌,但是他的诗歌和我国古代诗人的诗有很大区别,他的诗是用白话文写的,这其实也是他写作的惯用风格。\r\r后来,朱自清开始写一些关于社会的文章,因为那个时候社会比较混乱,这时候的作品大多抨击社会的黑暗面,文体风格大多硬朗,基调伉俪。到了后期,大多是写关于山水的文章,这类文章的写作格调大多以清丽雅致为主。\r\r朱自清的写作风格虽然在不同的时期随着他的人生阅历和社会形态的不同而发生着变化,但是他文章的主基调是没有变的,他这一生,所写的所有文章风格上都有一个非常显著的特点,那就是简约平淡,他不是类似古代花间词派的诗人们,不管是他的诗词还是他的文章从来都不用过于华丽的辞藻,他崇尚的是平淡。\r\r英国友人戴立克试过英译朱自清几篇散文,译完一读显得单薄,远远不如原文流利。他不服气,改用稍微古奥的英文重译,好多了:“那是说,朱先生外圆内方,文字尽管浅白,心思却很深沉,译笔只好朝深处经营。”朱自清的很多文章,譬如《背影》《祭亡妇》,读来自有一番只可意会不可言传的东西。\r\r平淡就是朱自清的写作风格。他不是豪放派的作家,他在创作的时候钟情于清新的风格,给人耳目一新的感觉。在他的文章中包含了他对生活的向往,由此可见他的写作风格和他待人处事的态度也是有几分相似的。他的文章非常优美,但又不会让人觉得狭隘,给人一种豁达渊博的感觉,这就是朱自清的写作风格,更是朱自清的为人品质。\r\r写有《荷塘月色》《背影》等名篇的著名散文家朱自清先生,不仅自己一生风骨正气,还用无形的家风涵养子孙。良好的家风家规意蕴深远,催人向善,是凝聚情感、涵养德行、砥砺成才的人生信条。“北有朱自清,南有朱物华,一文一武,一南一北,双星闪耀”,这是中国知识界、教育界对朱家两兄弟的赞誉。\r\r朱自清性格温和,为人和善,对待年轻人平易近人,是个平和的人。他取字“佩弦”,意思要像弓弦那样将自己绷紧,给人的感觉是自我要求高,偶尔有呆气。朱自清教学负责,对学生要求严格,修他的课的学生都受益不少。\r\r1948 年 6 月,患胃病多年的朱自清,在《抗议美国扶日政策并拒绝领取美援面粉宣言》上,一丝不苟地签下了自己的名字。随后,朱自清还将面粉配购证以及面粉票退了回去。1948 年 8 月 12 日,朱自清因不堪胃病折磨,离开人世。在新的时代即将到来时,朱自清却匆匆地离人们远去。他为人们留下了无数经典的诗歌和文字,还有永不屈服的精神。\r\r朱自清没有豪言壮语,他只是用坚定的行动、朴实的语言,向世人展示了中国知识分子在祖国危难之际坚定的革命性,体现了中国人的骨气,表现了无比高贵的民族气节,呈现了人生最有价值的一面,谱就了生命中最华丽的乐章。\r\r他以“自清”为名,自勉在困境中不丧志;他身患重病,至死拒领美援面粉,其气节令世人感佩;他的《背影》《荷塘月色》《匆匆》脍炙人口;他的文字追求“真”,没有半点矫饰,却蕴藏着动人心弦的力量。\r\r朱自清不但在文学创作方面有很高的造诣,也是一名革命民主主义战士,在反饥饿、反内战的斗争中,他始终保持着一个正直的爱国知识分子的气节和情操。毛泽东对朱自清宁肯饿死不领美国“救济粉”的精神给予称赞,赞扬他“表现了我们民族的英雄气概”。\r\n', diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts index ac7d4154fd4..aaaafd5fd62 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/layout-ruler.ts @@ -70,7 +70,7 @@ export function layoutParagraph( if (paragraphConfig.bulletSkeleton) { const { bulletSkeleton, paragraphStyle = {} } = paragraphConfig; // 如果是一个段落的开头,需要加入bullet - const { gridType = GridType.LINES, charSpace = 0, defaultTabStop = 1 } = sectionBreakConfig; + const { gridType = GridType.LINES, charSpace = 0, defaultTabStop = 10.5 } = sectionBreakConfig; const { snapToGrid = BooleanNumber.TRUE } = paragraphStyle; From 6427367b691a0b3ca6b59636bd7774be0ae54b46 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 17:42:08 +0800 Subject: [PATCH 29/39] fix: ui --- .../src/commands/commands/doc-header-footer.command.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts index 1019e07acb5..8342d3ca0de 100644 --- a/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-header-footer.command.ts @@ -25,7 +25,13 @@ import { SidebarDocHeaderFooterPanelOperation } from '../operations/doc-header-f function getEmptyHeaderFooterBody() { return { dataStream: '\r\n', - textRuns: [], + textRuns: [{ + st: 0, + ed: 0, + ts: { + fs: 9, // The default header footer text size. + }, + }], paragraphs: [ { startIndex: 0, From 343362569973adaa425d39762747d0ba861f6ed6 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 18:56:13 +0800 Subject: [PATCH 30/39] fix: ui --- .../src/components/docs/document.ts | 46 +++++++++---------- .../src/components/docs/layout/model/page.ts | 15 +++--- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index ae6ef86c876..aefb9a48b8b 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -36,7 +36,6 @@ import type { IDocumentsConfig, IPageMarginLayout } from './doc-component'; import { DocComponent } from './doc-component'; import { DOCS_EXTENSION_TYPE } from './doc-extension'; import type { DocumentSkeleton } from './layout/doc-skeleton'; -import { DocumentEditArea } from './view-model/document-view-model'; export interface IPageRenderConfig { page: IDocumentSkeletonPage; @@ -121,8 +120,6 @@ export class Documents extends DocComponent { return; } - const isEditBody = this.getSkeleton()?.getViewModel().getEditArea() === DocumentEditArea.BODY; - this._drawLiquid.reset(); const { pages, skeHeaders, skeFooters } = skeletonData; @@ -223,17 +220,14 @@ export class Documents extends DocComponent { centerAngle, vertexAngle, renderConfig, - parentScale + parentScale, + page, + true ); } this._startRotation(ctx, finalAngle); - // if (!isEditBody) { - // ctx.save(); - // ctx.globalAlpha = 0.5; - // } - for (const section of sections) { const { columns } = section; @@ -429,10 +423,6 @@ export class Documents extends DocComponent { } } - // if (!isEditBody) { - // ctx.restore(); - // } - this._resetRotation(ctx, finalAngle); const footerSkeletonPage = skeFooters.get(footerId)?.get(pageWidth); @@ -453,7 +443,9 @@ export class Documents extends DocComponent { centerAngle, vertexAngle, renderConfig, - parentScale + parentScale, + page, + false ); } @@ -485,19 +477,15 @@ export class Documents extends DocComponent { centerAngle: number, vertexAngle: number, renderConfig: IDocumentRenderConfig, - parentScale: IScale + parentScale: IScale, + parentPage: IDocumentSkeletonPage, + isHeader = true ) { if (this._drawLiquid == null) { return; } - const editArea = this.getSkeleton()?.getViewModel().getEditArea(); - const isEditHeaderFooter = editArea === DocumentEditArea.HEADER || editArea === DocumentEditArea.FOOTER; - - // if (!isEditHeaderFooter) { - // ctx.save(); - // ctx.globalAlpha = 0.5; - // } const { sections } = page; + const { y: originY } = this._drawLiquid; for (const section of sections) { const { columns } = section; @@ -536,8 +524,20 @@ export class Documents extends DocComponent { } } else { this._drawLiquid.translateSave(); - this._drawLiquid.translateLine(line, true); + const { y } = this._drawLiquid; + + if (isHeader) { + if ((y - originY + alignOffset.y) > (parentPage.pageHeight - 100) / 2) { + this._drawLiquid.translateRestore(); + continue; + } + } else { + if ((y - originY + alignOffset.y) < (parentPage.pageHeight - 100) / 2 + 100) { + this._drawLiquid.translateRestore(); + continue; + } + } const divideLength = divides.length; diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index c2299b17243..e0bac0d8710 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -68,7 +68,6 @@ export function createSkeletonPage( page.pageNumber = pageNumber; page.pageNumberStart = pageNumberStart; page.renderConfig = renderConfig; - page.marginLeft = marginLeft; page.marginRight = marginRight; page.breakType = breakType; @@ -126,8 +125,8 @@ export function createSkeletonPage( page.originMarginTop = marginTop; page.originMarginBottom = marginBottom; - page.marginTop = _getVerticalMargin(marginTop, marginHeader, header); - page.marginBottom = _getVerticalMargin(marginBottom, marginFooter, footer); + page.marginTop = _getVerticalMargin(marginTop, header, pageHeight); + page.marginBottom = _getVerticalMargin(marginBottom, footer, pageHeight); const sections = page.sections; const lastSection = sections[sections.length - 1]; @@ -234,14 +233,18 @@ function _createSkeletonHeaderFooter( function _getVerticalMargin( marginTB: number, - marginHF: number, - headerOrFooter: Nullable + headerOrFooter: Nullable, + pageHeight: number ) { if (!headerOrFooter || headerOrFooter.sections[0].columns[0].lines.length === 0) { return marginTB; } - return Math.max(marginTB, (headerOrFooter.marginTop + headerOrFooter.height + headerOrFooter.marginBottom || 0)); + const HeaderFooterPageHeight = headerOrFooter.height + headerOrFooter.marginTop + headerOrFooter.marginBottom; + // Content height should be at least 100px. + const maxMargin = (pageHeight - 100) / 2; + + return Math.min(maxMargin, Math.max(marginTB, HeaderFooterPageHeight)); } function __getHeaderMarginTop(marginTop: number, marginHeader: number, height: number) { From 197b08b6dc95f829c18203c7b35e910c89a1f186 Mon Sep 17 00:00:00 2001 From: jocs Date: Mon, 1 Jul 2024 19:37:22 +0800 Subject: [PATCH 31/39] fix: clipboard --- .../docs-ui/src/controllers/clipboard.controller.ts | 1 + .../src/services/clipboard/clipboard.service.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/docs-ui/src/controllers/clipboard.controller.ts b/packages/docs-ui/src/controllers/clipboard.controller.ts index 7c54691e38c..b94fe16aae5 100644 --- a/packages/docs-ui/src/controllers/clipboard.controller.ts +++ b/packages/docs-ui/src/controllers/clipboard.controller.ts @@ -60,6 +60,7 @@ export class DocClipboardController extends RxDisposable { const clipboardEvent = config!.event as ClipboardEvent; const htmlContent = clipboardEvent.clipboardData?.getData('text/html'); const textContent = clipboardEvent.clipboardData?.getData('text/plain'); + this._docClipboardService.legacyPaste(htmlContent, textContent); }); } diff --git a/packages/docs-ui/src/services/clipboard/clipboard.service.ts b/packages/docs-ui/src/services/clipboard/clipboard.service.ts index 2344f50e2e6..387471990ab 100644 --- a/packages/docs-ui/src/services/clipboard/clipboard.service.ts +++ b/packages/docs-ui/src/services/clipboard/clipboard.service.ts @@ -91,7 +91,9 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ } try { - this._setClipboardData(documentBodyList); + const activeRange = this._textSelectionManagerService.getActiveRange(); + const isCopyInHeaderFooter = !!activeRange?.segmentId; + this._setClipboardData(documentBodyList, !isCopyInHeaderFooter); } catch (e) { this._logService.error('[DocClipboardService] copy failed', e); return false; @@ -112,6 +114,7 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ async legacyPaste(html?: string, text?: string): Promise { const body = this._generateBodyFromHtmlAndText(html, text); + return this._paste(body); } @@ -205,7 +208,7 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ } } - private async _setClipboardData(documentBodyList: IDocumentBody[]): Promise { + private async _setClipboardData(documentBodyList: IDocumentBody[], needCache = true): Promise { const copyId = genId(); const text = documentBodyList.length > 1 @@ -213,8 +216,8 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ : documentBodyList[0].dataStream; let html = this._umdToHtml.convert(documentBodyList); - // Only cache copy content when the range is 1. - if (documentBodyList.length === 1) { + // Only cache copy content when the range is 1. + if (documentBodyList.length === 1 && needCache) { html = html.replace(/(<[a-z]+)/, (_p0, p1) => `${p1} data-copy-id="${copyId}"`); copyContentCache.set(copyId, documentBodyList[0]); } From 6c6ca424a6c5ae27d47a3635a6e81192d0ebc004 Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 2 Jul 2024 11:26:19 +0800 Subject: [PATCH 32/39] fix: edit footer --- packages/engine-render/src/components/docs/document.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index aefb9a48b8b..d99fe641f52 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -533,7 +533,7 @@ export class Documents extends DocComponent { continue; } } else { - if ((y - originY + alignOffset.y) < (parentPage.pageHeight - 100) / 2 + 100) { + if ((y - originY + alignOffset.y + lineHeight) < (parentPage.pageHeight - 100) / 2 + 100) { this._drawLiquid.translateRestore(); continue; } From c6eff2bfb7c562bcc3ba7791c731bfa3f0f77259 Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 2 Jul 2024 15:20:19 +0800 Subject: [PATCH 33/39] fix: footer header height --- .../src/components/docs/layout/model/page.ts | 9 +++++++-- .../text-selection-render-manager.ts | 18 +++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/engine-render/src/components/docs/layout/model/page.ts b/packages/engine-render/src/components/docs/layout/model/page.ts index e0bac0d8710..bbb5fd45204 100644 --- a/packages/engine-render/src/components/docs/layout/model/page.ts +++ b/packages/engine-render/src/components/docs/layout/model/page.ts @@ -29,6 +29,10 @@ import type { ILayoutContext } from '../tools'; import { updateBlockIndex } from '../tools'; import { createSkeletonSection } from './section'; +function getHeaderFooterMaxHeight(pageHeight: number) { + return (pageHeight - 100) / 2; +} + // 新增数据结构框架 // 判断奇数和偶数页码 export function createSkeletonPage( @@ -193,13 +197,14 @@ function _createSkeletonHeaderFooter( marginHeader = 0, marginFooter = 0, } = sectionBreakConfig; const pageWidth = pageSize?.width || Number.POSITIVE_INFINITY; + const pageHeight = pageSize?.height || Number.POSITIVE_INFINITY; const headerFooterConfig: ISectionBreakConfig = { lists, footerTreeMap, headerTreeMap, pageSize: { width: pageWidth - marginLeft - marginRight, - height: Number.POSITIVE_INFINITY, + height: getHeaderFooterMaxHeight(pageHeight) - (isHeader ? marginHeader : marginFooter) - 5, }, localeService, drawings, @@ -242,7 +247,7 @@ function _getVerticalMargin( const HeaderFooterPageHeight = headerOrFooter.height + headerOrFooter.marginTop + headerOrFooter.marginBottom; // Content height should be at least 100px. - const maxMargin = (pageHeight - 100) / 2; + const maxMargin = getHeaderFooterMaxHeight(pageHeight); return Math.min(maxMargin, Math.max(marginTB, HeaderFooterPageHeight)); } diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index 7c3138de96b..32c3ca9ecea 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -263,24 +263,28 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel } addTextRanges(ranges: ISuccinctTextRangeParam[], isEditing = true) { - const { _scene: scene, _docSkeleton: docSkeleton } = this; + const { + _scene: scene, _docSkeleton: docSkeleton, _document: document, + _currentSegmentId: segmentId, _currentSegmentPage: segmentPage, + _selectionStyle: style, + } = this; for (const range of ranges) { const textSelection = cursorConvertToTextRange(scene!, { style: this._selectionStyle, ...range, - segmentId: this._currentSegmentId, - segmentPage: this._currentSegmentPage, - }, docSkeleton!, this._document!); + segmentId, + segmentPage, + }, docSkeleton!, document!); this._add(textSelection); } this._textSelectionInner$.next({ textRanges: this._getAllTextRanges(), - segmentId: this._currentSegmentId, - segmentPage: this._currentSegmentPage, - style: this._selectionStyle, + segmentId, + segmentPage, + style, isEditing, }); From 1dd20946af49f2e957e2d78728f2c1eb430e77ae Mon Sep 17 00:00:00 2001 From: jocs Date: Tue, 2 Jul 2024 19:40:12 +0800 Subject: [PATCH 34/39] fix: 1444 --- .../panel/DocHeaderFooterOptions.tsx | 15 ++++++++++++++- .../text-selection-render-manager.ts | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx index 64776028912..a0ab8711e40 100644 --- a/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx +++ b/packages/docs-ui/src/views/header-footer/panel/DocHeaderFooterOptions.tsx @@ -57,12 +57,18 @@ export const DocHeaderFooterOptions = (props: IDocHeaderFooterOptionsProps) => { const editArea = viewModel.getEditArea(); let needCreateHeaderFooter = false; + const segmentPage = textSelectionRenderService.getSegmentPage(); + let needChangeSegmentId = false; if (type === 'useFirstPageHeaderFooter' && val === true) { if (editArea === DocumentEditArea.HEADER && !documentStyle.firstPageHeaderId) { needCreateHeaderFooter = true; } else if (editArea === DocumentEditArea.FOOTER && !documentStyle.firstPageFooterId) { needCreateHeaderFooter = true; } + + if (needCreateHeaderFooter && segmentPage === 0) { + needChangeSegmentId = true; + } } if (type === 'evenAndOddHeaders' && val === true) { @@ -71,13 +77,20 @@ export const DocHeaderFooterOptions = (props: IDocHeaderFooterOptionsProps) => { } else if (editArea === DocumentEditArea.FOOTER && !documentStyle.evenPageFooterId) { needCreateHeaderFooter = true; } + + if (needCreateHeaderFooter && segmentPage % 2 === 1) { + needChangeSegmentId = true; + } } if (needCreateHeaderFooter) { const SEGMENT_ID_LEN = 6; const segmentId = Tools.generateRandomId(SEGMENT_ID_LEN); // Set segment id first, then exec command. - textSelectionRenderService.setSegment(segmentId); + if (needChangeSegmentId) { + textSelectionRenderService.setSegment(segmentId); + } + commandService.executeCommand(CoreHeaderFooterCommandId, { unitId, segmentId, diff --git a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts index 32c3ca9ecea..fe1a28582a9 100644 --- a/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts +++ b/packages/engine-render/src/components/docs/text-selection/text-selection-render-manager.ts @@ -137,6 +137,7 @@ export interface ITextSelectionRenderManager { disableSelection(): void; setSegment(id: string): void; setSegmentPage(pageIndex: number): void; + getSegmentPage(): number; setStyle(style: ITextSelectionStyle): void; resetStyle(): void; removeAllTextRanges(): void; @@ -246,6 +247,10 @@ export class TextSelectionRenderManager extends RxDisposable implements ITextSel this._currentSegmentPage = pageIndex; } + getSegmentPage() { + return this._currentSegmentPage; + } + setStyle(style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE) { this._selectionStyle = style; } From 5acf53671e7a6c1370f8f00714ec30ac5ae94ae9 Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 5 Jul 2024 16:08:09 +0800 Subject: [PATCH 35/39] fix: rebase --- packages/docs-ui/package.json | 4 ++-- .../doc-header-footer.controller.ts | 2 +- .../text-selection.render-controller.ts | 2 +- .../commands/commands/core-editing.command.ts | 12 ++++++++--- .../src/commands/commands/delete.command.ts | 20 ++++++++++++------- .../commands/commands/ime-input.command.ts | 11 +++++++--- pnpm-lock.yaml | 5 ----- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/docs-ui/package.json b/packages/docs-ui/package.json index 602dc588603..a3ad40d73ca 100644 --- a/packages/docs-ui/package.json +++ b/packages/docs-ui/package.json @@ -70,7 +70,7 @@ "@univerjs/docs": "workspace:*", "@univerjs/engine-render": "workspace:*", "@univerjs/ui": "workspace:*", - "@wendellhu/redi": "0.15.4", + "@wendellhu/redi": "0.15.5", "clsx": ">=2.0.0", "react": "^16.9.0 || ^17.0.0 || ^18.0.0", "rxjs": ">=7.0.0" @@ -83,7 +83,7 @@ "@univerjs/engine-render": "workspace:*", "@univerjs/shared": "workspace:*", "@univerjs/ui": "workspace:*", - "@wendellhu/redi": "0.15.4", + "@wendellhu/redi": "0.15.5", "clsx": "^2.1.1", "less": "^4.2.0", "react": "18.3.1", diff --git a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts index f6bfe9d282b..4dae1ab2848 100644 --- a/packages/docs-ui/src/controllers/doc-header-footer.controller.ts +++ b/packages/docs-ui/src/controllers/doc-header-footer.controller.ts @@ -259,7 +259,7 @@ export class DocHeaderFooterController extends Disposable implements IRenderModu return; } - const originCoord = activeViewport.getRelativeVector(Vector2.FromArray([evtOffsetX, evtOffsetY])); + const originCoord = activeViewport.transformVector2SceneCoord(Vector2.FromArray([evtOffsetX, evtOffsetY])); return documentTransform.clone().invert().applyPoint(originCoord); } diff --git a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts index 199ae789d7f..b5428bfbbcc 100644 --- a/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/text-selection.render-controller.ts @@ -164,7 +164,7 @@ export class DocTextSelectionRenderController extends Disposable implements IRen return; } - const originCoord = activeViewport.getRelativeVector(Vector2.FromArray([evtOffsetX, evtOffsetY])); + const originCoord = activeViewport.transformVector2SceneCoord(Vector2.FromArray([evtOffsetX, evtOffsetY])); return documentTransform.clone().invert().applyPoint(originCoord); } diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index 8d669c5350b..396d9876110 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -49,6 +49,7 @@ export const EditorInsertTextCommandId = 'doc.command.insert-text'; export const InsertCommand: ICommand = { id: EditorInsertTextCommandId, type: CommandType.COMMAND, + // eslint-disable-next-line max-lines-per-function handler: async (accessor, params: IInsertCommandParams) => { const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); @@ -60,9 +61,14 @@ export const InsertCommand: ICommand = { } const textSelectionManagerService = accessor.get(TextSelectionManagerService); - const originBody = docDataModel?.getBody(); const activeRange = textSelectionManagerService.getActiveRange(); + if (activeRange == null) { + return false; + } + + const originBody = docDataModel.getSelfOrHeaderFooterModel(activeRange.segmentId).getBody(); + if (!originBody) { return false; } @@ -72,7 +78,7 @@ export const InsertCommand: ICommand = { { startOffset: startOffset + body.dataStream.length, endOffset: startOffset + body.dataStream.length, - style: activeRange?.style, + style: activeRange.style, collapsed, }, ]; @@ -165,7 +171,7 @@ export const DeleteCommand: ICommand = { return false; } - const body = documentDataModel.getBody(); + const body = documentDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); if (!body) { return false; } diff --git a/packages/docs/src/commands/commands/delete.command.ts b/packages/docs/src/commands/commands/delete.command.ts index 4572bee4054..7ad37f8b84d 100644 --- a/packages/docs/src/commands/commands/delete.command.ts +++ b/packages/docs/src/commands/commands/delete.command.ts @@ -161,8 +161,7 @@ export const DeleteLeftCommand: ICommand = { let result = true; const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - const body = docDataModel?.getBody(); - if (!docDataModel || !body) { + if (!docDataModel) { return false; } @@ -175,15 +174,21 @@ export const DeleteLeftCommand: ICommand = { return false; } + const { segmentId, style, segmentPage } = activeRange; + const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); + + if (body == null) { + return false; + } + const actualRange = getDeleteSelection(activeRange, body); const { startOffset, collapsed } = actualRange; - const { segmentId, style, segmentPage } = activeRange; const curGlyph = skeleton.findNodeByCharIndex(startOffset, segmentId, segmentPage); // is in bullet list? const isBullet = hasListGlyph(curGlyph); // is in indented paragraph? - const isIndent = isIndentByGlyph(curGlyph, docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()); + const isIndent = isIndentByGlyph(curGlyph, body); let cursor = startOffset; @@ -316,16 +321,17 @@ export const DeleteRightCommand: ICommand = { return false; } - const body = docDataModel?.getBody(); + const { segmentId, style, segmentPage } = activeRange; + + const body = docDataModel?.getSelfOrHeaderFooterModel(segmentId).getBody(); if (!docDataModel || !body) { return false; } const actualRange = getInsertSelection(activeRange, body); const { startOffset, collapsed } = actualRange; - const { segmentId, style, segmentPage } = activeRange; // No need to delete when the cursor is at the last position of the last paragraph. - if (startOffset === docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()!.dataStream.length - 2 && collapsed) { + if (startOffset === body.dataStream.length - 2 && collapsed) { return true; } diff --git a/packages/docs/src/commands/commands/ime-input.command.ts b/packages/docs/src/commands/commands/ime-input.command.ts index 397e4480525..f9ebdc2e6fc 100644 --- a/packages/docs/src/commands/commands/ime-input.command.ts +++ b/packages/docs/src/commands/commands/ime-input.command.ts @@ -52,14 +52,19 @@ export const IMEInputCommand: ICommand = { } const previousActiveRange = imeInputManagerService.getActiveRange(); - const body = docDataModel?.getBody(); - if (!previousActiveRange || !body) { + if (!previousActiveRange) { return false; } + const { startOffset, style, segmentId } = previousActiveRange; + const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); + + if (body == null) { + return false; + } + const insertRange = getInsertSelection(previousActiveRange, body); Object.assign(previousActiveRange, insertRange); - const { startOffset, style, segmentId } = previousActiveRange; const len = newText.length; const textRanges: ITextRangeWithStyle[] = [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb3d9f0851..333383975d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -810,16 +810,11 @@ importers: specifier: workspace:* version: link:../ui '@wendellhu/redi': -<<<<<<< HEAD specifier: 0.15.5 version: 0.15.5 -======= - specifier: 0.15.4 - version: 0.15.4 clsx: specifier: ^2.1.1 version: 2.1.1 ->>>>>>> 0b94ebac1 (feat: ui of header & footer options) less: specifier: ^4.2.0 version: 4.2.0 From 0b78420a0be45279de294f06584c9beb7b5c37c3 Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 5 Jul 2024 16:23:52 +0800 Subject: [PATCH 36/39] fix: rebase --- .../commands/commands/core-editing.command.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index 396d9876110..1d2dc59271c 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -153,28 +153,18 @@ export interface IDeleteCommandParams { export const DeleteCommand: ICommand = { id: 'doc.command.delete-text', type: CommandType.COMMAND, - // eslint-disable-next-line max-lines-per-function + handler: async (accessor, params: IDeleteCommandParams) => { const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); - const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); - - if (docDataModel == null) { - return false; - } - const { range, segmentId, unitId, direction, len = 1 } = params; - - const { startOffset } = range; - const documentDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); - if (!documentDataModel) { + const docDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); + const body = docDataModel?.getSelfOrHeaderFooterModel(segmentId).getBody(); + if (docDataModel == null || body == null) { return false; } - const body = documentDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); - if (!body) { - return false; - } + const { startOffset } = range; const dataStream = body.dataStream; const start = direction === DeleteDirection.LEFT ? startOffset - len : startOffset; const end = start + len - 1; From 3b8eeb188651726f9ff09f5c33c74990050243f7 Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 5 Jul 2024 16:41:46 +0800 Subject: [PATCH 37/39] fix: rebase --- .../docs/src/commands/commands/core-editing.command.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index 1d2dc59271c..6ea646bb825 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -215,13 +215,6 @@ export const DeleteCommand: ICommand = { cursor = deleteIndex + 1; } - textX.push({ - t: TextXActionType.DELETE, - len, - line: 0, - segmentId, - }); - const path = getRichTextEditPath(docDataModel, segmentId); doMutation.params.actions = jsonX.editOp(textX.serialize(), path); From f2fdc833cd7207dedc7200d87b29b835814d47ae Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 5 Jul 2024 16:47:34 +0800 Subject: [PATCH 38/39] fix: only traditional doc show margin --- .../src/components/docs/doc-background.ts | 95 ++++++++++--------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/packages/engine-render/src/components/docs/doc-background.ts b/packages/engine-render/src/components/docs/doc-background.ts index b2f8d91d182..43be40b11df 100644 --- a/packages/engine-render/src/components/docs/doc-background.ts +++ b/packages/engine-render/src/components/docs/doc-background.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { DocumentFlavor } from '@univerjs/core'; import type { IViewportInfo } from '../../basics/vector2'; import type { UniverRenderingContext } from '../../context'; import type { IPathProps } from '../../shape'; @@ -44,11 +45,13 @@ export class DocBackground extends DocComponent { override draw(ctx: UniverRenderingContext, bounds?: IViewportInfo) { const skeletonData = this.getSkeleton()?.getSkeletonData(); + const docDataModel = this.getSkeleton()?.getViewModel().getDataModel(); - if (skeletonData == null) { + if (skeletonData == null || docDataModel == null) { return; } + const documentFlavor = docDataModel.getSnapshot().documentStyle.documentFlavor; this._drawLiquid.reset(); const { pages } = skeletonData; @@ -87,51 +90,53 @@ export class DocBackground extends DocComponent { zIndex: 3, }; - const IDENTIFIER_WIDTH = 15; - const marginIdentification: IPathProps = { - dataArray: [{ - command: 'M', - points: [marginLeft - IDENTIFIER_WIDTH, originMarginTop], - }, { - command: 'L', - points: [marginLeft, originMarginTop], - }, { - command: 'L', - points: [marginLeft, originMarginTop - IDENTIFIER_WIDTH], - }, { - command: 'M', - points: [pageWidth - marginRight + IDENTIFIER_WIDTH, originMarginTop], - }, { - command: 'L', - points: [pageWidth - marginRight, originMarginTop], - }, { - command: 'L', - points: [pageWidth - marginRight, originMarginTop - IDENTIFIER_WIDTH], - }, { - command: 'M', - points: [marginLeft - IDENTIFIER_WIDTH, pageHeight - originMarginBottom], - }, { - command: 'L', - points: [marginLeft, pageHeight - originMarginBottom], - }, { - command: 'L', - points: [marginLeft, pageHeight - originMarginBottom + IDENTIFIER_WIDTH], - }, { - command: 'M', - points: [pageWidth - marginRight + IDENTIFIER_WIDTH, pageHeight - originMarginBottom], - }, { - command: 'L', - points: [pageWidth - marginRight, pageHeight - originMarginBottom], - }, { - command: 'L', - points: [pageWidth - marginRight, pageHeight - originMarginBottom + IDENTIFIER_WIDTH], - }] as unknown as IPathProps['dataArray'], - strokeWidth: 1.5, - stroke: MARGIN_STROKE_COLOR, - }; - Rect.drawWith(ctx, backgroundOptions); - Path.drawWith(ctx, marginIdentification); + + if (documentFlavor === DocumentFlavor.TRADITIONAL) { + const IDENTIFIER_WIDTH = 15; + const marginIdentification: IPathProps = { + dataArray: [{ + command: 'M', + points: [marginLeft - IDENTIFIER_WIDTH, originMarginTop], + }, { + command: 'L', + points: [marginLeft, originMarginTop], + }, { + command: 'L', + points: [marginLeft, originMarginTop - IDENTIFIER_WIDTH], + }, { + command: 'M', + points: [pageWidth - marginRight + IDENTIFIER_WIDTH, originMarginTop], + }, { + command: 'L', + points: [pageWidth - marginRight, originMarginTop], + }, { + command: 'L', + points: [pageWidth - marginRight, originMarginTop - IDENTIFIER_WIDTH], + }, { + command: 'M', + points: [marginLeft - IDENTIFIER_WIDTH, pageHeight - originMarginBottom], + }, { + command: 'L', + points: [marginLeft, pageHeight - originMarginBottom], + }, { + command: 'L', + points: [marginLeft, pageHeight - originMarginBottom + IDENTIFIER_WIDTH], + }, { + command: 'M', + points: [pageWidth - marginRight + IDENTIFIER_WIDTH, pageHeight - originMarginBottom], + }, { + command: 'L', + points: [pageWidth - marginRight, pageHeight - originMarginBottom], + }, { + command: 'L', + points: [pageWidth - marginRight, pageHeight - originMarginBottom + IDENTIFIER_WIDTH], + }] as unknown as IPathProps['dataArray'], + strokeWidth: 1.5, + stroke: MARGIN_STROKE_COLOR, + }; + Path.drawWith(ctx, marginIdentification); + } ctx.restore(); const { x, y } = this._drawLiquid.translatePage( From 56cb8039404ae7c3f17d1ff18c20eb89f67fe25b Mon Sep 17 00:00:00 2001 From: jocs Date: Fri, 5 Jul 2024 17:10:35 +0800 Subject: [PATCH 39/39] fix: test --- .../docs/data-model/text-x/__tests__/invert.spec.ts | 1 + .../docs/src/commands/commands/core-editing.command.ts | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts index 516fe42e416..9374b4f44f1 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts @@ -198,6 +198,7 @@ describe('test TextX static methods invert and makeInvertible', () => { bl: BooleanNumber.TRUE, }, }], + customRanges: [], customDecorations: [], }, coverType: UpdateDocsAttributeType.COVER, diff --git a/packages/docs/src/commands/commands/core-editing.command.ts b/packages/docs/src/commands/commands/core-editing.command.ts index 6ea646bb825..c631354c9c3 100644 --- a/packages/docs/src/commands/commands/core-editing.command.ts +++ b/packages/docs/src/commands/commands/core-editing.command.ts @@ -49,7 +49,7 @@ export const EditorInsertTextCommandId = 'doc.command.insert-text'; export const InsertCommand: ICommand = { id: EditorInsertTextCommandId, type: CommandType.COMMAND, - // eslint-disable-next-line max-lines-per-function + handler: async (accessor, params: IInsertCommandParams) => { const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); @@ -63,11 +63,7 @@ export const InsertCommand: ICommand = { const textSelectionManagerService = accessor.get(TextSelectionManagerService); const activeRange = textSelectionManagerService.getActiveRange(); - if (activeRange == null) { - return false; - } - - const originBody = docDataModel.getSelfOrHeaderFooterModel(activeRange.segmentId).getBody(); + const originBody = docDataModel.getSelfOrHeaderFooterModel(activeRange?.segmentId ?? '').getBody(); if (!originBody) { return false; @@ -78,7 +74,7 @@ export const InsertCommand: ICommand = { { startOffset: startOffset + body.dataStream.length, endOffset: startOffset + body.dataStream.length, - style: activeRange.style, + style: activeRange?.style, collapsed, }, ];