diff --git a/e2e/visual-comparison/docs/docs-visual-comparison.spec.ts-snapshots/default-doc-ci-chromium-linux.png b/e2e/visual-comparison/docs/docs-visual-comparison.spec.ts-snapshots/default-doc-ci-chromium-linux.png index 7165766b7d..6b7f42347e 100644 Binary files a/e2e/visual-comparison/docs/docs-visual-comparison.spec.ts-snapshots/default-doc-ci-chromium-linux.png and b/e2e/visual-comparison/docs/docs-visual-comparison.spec.ts-snapshots/default-doc-ci-chromium-linux.png differ diff --git a/mockdata/src/docs/default-document-data-cn.ts b/mockdata/src/docs/default-document-data-cn.ts index 3780368cf8..d49d37e7e5 100644 --- a/mockdata/src/docs/default-document-data-cn.ts +++ b/mockdata/src/docs/default-document-data-cn.ts @@ -99,18 +99,6 @@ export const DEFAULT_DOCUMENT_DATA_CN: IDocumentData = { bl: BooleanNumber.TRUE, }, }, - { - st: 14, - ed: 3064, - ts: { - fs: 12, - ff: 'Microsoft YaHei', - cl: { - rgb: 'rgb(30, 30, 30)', - }, - bl: BooleanNumber.FALSE, - }, - }, ], paragraphs: [ { diff --git a/packages/core/src/docs/data-model/text-x/build-utils/index.ts b/packages/core/src/docs/data-model/text-x/build-utils/index.ts index 8d4d7e10a6..b09c25a6e8 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/index.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/index.ts @@ -17,9 +17,9 @@ import { addCustomDecorationTextX, deleteCustomDecorationTextX } from './custom-decoration'; import { copyCustomRange, getCustomRangesInterestsWithSelection, isIntersecting } from './custom-range'; import { addDrawing } from './drawings'; -import { changeParagraphBulletNestLevel, setParagraphBullet, switchParagraphBullet, toggleChecklistParagraph } from './paragraph'; +import { changeParagraphBulletNestLevel, setParagraphBullet, setParagraphStyle, switchParagraphBullet, toggleChecklistParagraph } from './paragraph'; import { fromPlainText, getPlainText, isEmptyDocument } from './parse'; -import { isSegmentIntersects, makeSelection, normalizeSelection } from './selection'; +import { getParagraphsInRange, getParagraphsInRanges, isSegmentIntersects, makeSelection, normalizeSelection } from './selection'; import { addCustomRangeTextX, deleteCustomRangeTextX, deleteSelectionTextX, replaceSelectionTextRuns, replaceSelectionTextX, retainSelectionTextX } from './text-x-utils'; export class BuildTextUtils { @@ -47,6 +47,8 @@ export class BuildTextUtils { static range = { isIntersects: isSegmentIntersects, + getParagraphsInRange, + getParagraphsInRanges, }; static transform = { @@ -62,6 +64,9 @@ export class BuildTextUtils { toggleChecklist: toggleChecklistParagraph, changeNestLevel: changeParagraphBulletNestLevel, }, + style: { + set: setParagraphStyle, + }, }; static drawing = { diff --git a/packages/core/src/docs/data-model/text-x/build-utils/paragraph.ts b/packages/core/src/docs/data-model/text-x/build-utils/paragraph.ts index 120859329f..056a3fc27b 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/paragraph.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/paragraph.ts @@ -14,13 +14,15 @@ * limitations under the License. */ -import type { ICustomTable, IParagraph } from '../../../../types/interfaces'; +import type { ITextRange } from '../../../../sheets/typedef'; +import type { ICustomTable, IParagraph, IParagraphStyle, ITextStyle } from '../../../../types/interfaces'; import type { DocumentDataModel } from '../../document-data-model'; import { MemoryCursor } from '../../../../common/memory-cursor'; import { Tools, UpdateDocsAttributeType } from '../../../../shared'; import { PRESET_LIST_TYPE, PresetListType } from '../../preset-list-type'; import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; +import { getParagraphsInRanges } from './selection'; export interface ISwitchParagraphBulletParams { paragraphs: IParagraph[]; @@ -241,7 +243,7 @@ export function hasParagraphInTable(paragraph: IParagraph, tables: ICustomTable[ } export const changeParagraphBulletNestLevel = (params: IChangeParagraphBulletNestLevelParams) => { - const { paragraphs: currentParagraphs, segmentId, document: docDataModel, type } = params; + const { paragraphs: currentParagraphs, document: docDataModel, type } = params; const memoryCursor = new MemoryCursor(); memoryCursor.reset(); @@ -305,3 +307,73 @@ export const changeParagraphBulletNestLevel = (params: IChangeParagraphBulletNes return textX; }; + +interface ISetParagraphStyleParams { + textRanges: readonly ITextRange[]; + segmentId?: string; + document: DocumentDataModel; + style: IParagraphStyle; + paragraphTextRun?: ITextStyle; +} + +export const setParagraphStyle = (params: ISetParagraphStyleParams) => { + const { textRanges, segmentId, document: docDataModel, style, paragraphTextRun } = params; + const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs ?? []; + const currentParagraphs = getParagraphsInRanges(textRanges, paragraphs); + const memoryCursor = new MemoryCursor(); + const textX = new TextX(); + currentParagraphs.sort((a, b) => a.startIndex - b.startIndex); + const start = Math.max(0, currentParagraphs[0].paragraphStart - 1); + + if (start > 0) { + textX.push({ + t: TextXActionType.RETAIN, + len: start - memoryCursor.cursor, + }); + memoryCursor.moveCursorTo(start); + } + + for (const paragraph of currentParagraphs) { + const { startIndex, paragraphStyle = {} } = paragraph; + const len = startIndex - memoryCursor.cursor; + textX.push({ + t: TextXActionType.RETAIN, + len, + ...paragraphTextRun + ? { + body: { + dataStream: '', + textRuns: [{ + ts: paragraphTextRun, + st: 0, + ed: len, + }], + }, + coverType: UpdateDocsAttributeType.REPLACE, + } + : null, + }); + + textX.push({ + t: TextXActionType.RETAIN, + len: 1, + body: { + dataStream: '', + paragraphs: [ + { + startIndex: 0, + paragraphStyle: { + ...paragraphStyle, + ...style, + }, + }, + ], + }, + coverType: UpdateDocsAttributeType.REPLACE, + }); + + memoryCursor.moveCursorTo(startIndex + 1); + } + + return textX; +}; diff --git a/packages/core/src/docs/data-model/text-x/build-utils/selection.ts b/packages/core/src/docs/data-model/text-x/build-utils/selection.ts index 6289bdd679..3341e1c6ce 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/selection.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/selection.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import type { Nullable } from '../../../../shared'; import type { ITextRange } from '../../../../sheets/typedef'; +import type { IParagraph, IParagraphRange } from '../../../../types/interfaces'; export function makeSelection(startOffset: number, endOffset?: number): ITextRange { if (typeof endOffset === 'undefined') { @@ -43,3 +45,46 @@ export function normalizeSelection(selection: ITextRange): ITextRange { export function isSegmentIntersects(start: number, end: number, start2: number, end2: number) { return Math.max(start, start2) <= Math.min(end, end2); } + +export function getParagraphsInRange(activeRange: ITextRange, paragraphs: IParagraph[]) { + const { startOffset, endOffset } = activeRange; + const results: IParagraphRange[] = []; + + let start = -1; + + for (let i = 0; i < paragraphs.length; i++) { + const paragraph = paragraphs[i]; + const prevParagraph: Nullable = paragraphs[i - 1]; + const { startIndex } = paragraph; + + if ((startOffset > start && startOffset <= startIndex) || (endOffset > start && endOffset <= startIndex)) { + results.push({ + ...paragraph, + paragraphStart: (prevParagraph?.startIndex ?? -1) + 1, + paragraphEnd: paragraph.startIndex, + }); + } else if (startIndex >= startOffset && startIndex <= endOffset) { + results.push({ + ...paragraph, + paragraphStart: (prevParagraph?.startIndex ?? -1) + 1, + paragraphEnd: paragraph.startIndex, + }); + } + + start = startIndex; + } + + return results; +} + +export function getParagraphsInRanges(ranges: readonly ITextRange[], paragraphs: IParagraph[]) { + const results: IParagraphRange[] = []; + + for (const range of ranges) { + const ps = getParagraphsInRange(range, paragraphs); + + results.push(...ps); + } + + return results; +} diff --git a/packages/core/src/types/const/const.ts b/packages/core/src/types/const/const.ts index ed3c7bd479..eebc487649 100644 --- a/packages/core/src/types/const/const.ts +++ b/packages/core/src/types/const/const.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import type { Nullable } from '../../shared'; +import type { ITextStyle } from '../interfaces'; import { DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_ZEN_EDITOR_UNIT_ID_KEY } from '../../common/const'; import { BooleanNumber, @@ -22,6 +24,7 @@ import { VerticalAlign, WrapStrategy, } from '../enum'; +import { NamedStyleType } from '../interfaces'; /** * Used as an illegal range array return value @@ -175,3 +178,16 @@ export const DEFAULT_SLIDE = { }; export const SHEET_EDITOR_UNITS = [DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_ZEN_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY]; + +export const NAMED_STYLE_MAP: Record> = { + [NamedStyleType.HEADING_1]: { fs: 20, bl: 1 }, // Heading 1: 20pt, bold + [NamedStyleType.HEADING_2]: { fs: 18, bl: 1 }, // Heading 2: 18pt, bold + [NamedStyleType.HEADING_3]: { fs: 16, bl: 1 }, // Heading 3: 16pt, bold + [NamedStyleType.HEADING_4]: { fs: 14, bl: 1 }, // Heading 4: 14pt, bold + [NamedStyleType.HEADING_5]: { fs: 12, bl: 1 }, // Heading 5: 12pt, bold + [NamedStyleType.HEADING_6]: { fs: 11, bl: 1 }, // Heading 6: 11pt, bold + [NamedStyleType.NORMAL_TEXT]: { fs: 11 }, // Normal text: 11pt + [NamedStyleType.TITLE]: { fs: 26, bl: 1 }, // Title: 26pt, bold + [NamedStyleType.SUBTITLE]: { fs: 15, cl: { rgb: '#999999' } }, // Subtitle: 15pt + [NamedStyleType.NAMED_STYLE_TYPE_UNSPECIFIED]: null, +}; diff --git a/packages/docs-ui/src/basics/paragraph.ts b/packages/docs-ui/src/basics/paragraph.ts index 8905a20e3b..b3ebab39a2 100644 --- a/packages/docs-ui/src/basics/paragraph.ts +++ b/packages/docs-ui/src/basics/paragraph.ts @@ -32,7 +32,7 @@ export function getTextRunAtPosition( const retTextRun: ITextRun = { st: 0, ed: 0, - ts: isCellEditor ? {} : defaultStyle, + ts: {}, }; if (isFormula) { diff --git a/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts b/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts index 4a2a9d2218..57a266d618 100644 --- a/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts +++ b/packages/docs-ui/src/commands/commands/doc-paragraph-setting.command.ts @@ -16,10 +16,9 @@ import type { DocumentDataModel, IAccessor, ICommand, IMutationInfo, IParagraphStyle } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import { CommandType, ICommandService, IUniverInstanceService, JSONX, MemoryCursor, TextX, TextXActionType, UniverInstanceType, UpdateDocsAttributeType } from '@univerjs/core'; +import { BuildTextUtils, CommandType, ICommandService, IUniverInstanceService, JSONX, MemoryCursor, TextX, TextXActionType, UniverInstanceType, UpdateDocsAttributeType } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { getRichTextEditPath } from '../util'; -import { getParagraphsInRanges } from './list.command'; export interface IDocParagraphSettingCommandParams { paragraph: Partial>; @@ -46,7 +45,7 @@ export const DocParagraphSettingCommand: ICommand = { id: RichTextEditingMutation.id, diff --git a/packages/docs-ui/src/commands/commands/list.command.ts b/packages/docs-ui/src/commands/commands/list.command.ts index c9dfc55a74..c10393d7bb 100644 --- a/packages/docs-ui/src/commands/commands/list.command.ts +++ b/packages/docs-ui/src/commands/commands/list.command.ts @@ -67,7 +67,7 @@ export const ListOperationCommand: ICommand = { return false; } - const currentParagraphs = getParagraphsInRanges(docRanges, paragraphs); + const currentParagraphs = BuildTextUtils.range.getParagraphsInRanges(docRanges, paragraphs); const unitId = docDataModel.getUnitId(); @@ -129,7 +129,7 @@ export const ChangeListTypeCommand: ICommand = { return false; } - const currentParagraphs = getParagraphsInRanges(selections, paragraphs); + const currentParagraphs = BuildTextUtils.range.getParagraphsInRanges(selections, paragraphs); const unitId = docDataModel.getUnitId(); const textX = BuildTextUtils.paragraph.bullet.set({ @@ -199,7 +199,7 @@ export const ChangeListNestingLevelCommand: ICommand = { }, }; -export function getParagraphsInRange(activeRange: ITextRangeWithStyle, paragraphs: IParagraph[]) { - const { startOffset, endOffset } = activeRange; - const results: IParagraph[] = []; - - let start = -1; - - for (const paragraph of paragraphs) { - const { startIndex } = paragraph; - - if ((startOffset > start && startOffset <= startIndex) || (endOffset > start && endOffset <= startIndex)) { - results.push(paragraph); - } else if (startIndex >= startOffset && startIndex <= endOffset) { - results.push(paragraph); - } - - start = startIndex; - } - - return results; -} - export function getParagraphsRelative(ranges: ITextRangeWithStyle[], paragraphs: IParagraph[]) { - const selectionParagraphs = getParagraphsInRanges(ranges, paragraphs); + const selectionParagraphs: IParagraph[] = BuildTextUtils.range.getParagraphsInRanges(ranges, paragraphs); const startIndex = paragraphs.indexOf(selectionParagraphs[0]); const endIndex = paragraphs.indexOf(selectionParagraphs[selectionParagraphs.length - 1]); if (selectionParagraphs[0].bullet) { @@ -519,18 +498,6 @@ export function getParagraphsRelative(ranges: ITextRangeWithStyle[], paragraphs: return selectionParagraphs; } -export function getParagraphsInRanges(ranges: ITextRangeWithStyle[], paragraphs: IParagraph[]) { - const results: IParagraph[] = []; - - for (const range of ranges) { - const ps = getParagraphsInRange(range, paragraphs); - - results.push(...ps); - } - - return results; -} - export function findNearestSectionBreak(currentIndex: number, sectionBreaks: ISectionBreak[]) { const sortedSectionBreaks = sectionBreaks.sort(sortRulesFactory('startIndex')); for (let i = 0; i < sortedSectionBreaks.length; i++) { diff --git a/packages/docs-ui/src/commands/commands/paragraph-align.command.ts b/packages/docs-ui/src/commands/commands/paragraph-align.command.ts index a627f607a0..72c03f997d 100644 --- a/packages/docs-ui/src/commands/commands/paragraph-align.command.ts +++ b/packages/docs-ui/src/commands/commands/paragraph-align.command.ts @@ -18,6 +18,7 @@ import type { ICommand, IMutationInfo, IParagraphStyle } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import { + BuildTextUtils, CommandType, HorizontalAlign, ICommandService, @@ -30,7 +31,6 @@ import { } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { getRichTextEditPath } from '../util'; -import { getParagraphsInRanges } from './list.command'; interface IAlignOperationCommandParams { alignType: HorizontalAlign; @@ -67,7 +67,7 @@ export const AlignOperationCommand: ICommand = { return false; } - const currentParagraphs = getParagraphsInRanges(allRanges, paragraphs); + const currentParagraphs = BuildTextUtils.range.getParagraphsInRanges(allRanges, paragraphs); const unitId = docDataModel.getUnitId(); const isAlreadyAligned = currentParagraphs.every((paragraph) => paragraph.paragraphStyle?.horizontalAlign === alignType); diff --git a/packages/docs-ui/src/commands/commands/set-heading.command.ts b/packages/docs-ui/src/commands/commands/set-heading.command.ts new file mode 100644 index 0000000000..1b0629d2d1 --- /dev/null +++ b/packages/docs-ui/src/commands/commands/set-heading.command.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * 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, ICommand, IMutationInfo } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import { BuildTextUtils, CommandType, generateRandomId, ICommandService, IUniverInstanceService, JSONX, NamedStyleType, UniverInstanceType } from '@univerjs/core'; +import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { getRichTextEditPath } from '../util'; + +export interface ISetParagraphNamedStyleCommandParams { + value: NamedStyleType; +} + +export const SetParagraphNamedStyleCommand: ICommand = { + id: 'doc.command.set-paragraph-named-style', + type: CommandType.COMMAND, + handler(accessor, params) { + if (!params) { + return false; + } + + const univerInstanceService = accessor.get(IUniverInstanceService); + const documentDataModel = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC); + if (!documentDataModel) { + return false; + } + const unitId = documentDataModel.getUnitId(); + const selectionService = accessor.get(DocSelectionManagerService); + const selections = selectionService.getTextRanges({ unitId, subUnitId: unitId })?.filter((i) => !i.segmentId); + if (!selections?.length) { + return false; + } + + const textX = BuildTextUtils.paragraph.style.set({ + document: documentDataModel, + textRanges: selections, + style: { + namedStyleType: params.value, + headingId: !params.value || params.value === NamedStyleType.NORMAL_TEXT ? undefined : generateRandomId(6), + }, + paragraphTextRun: {}, + + }); + + const doMutation: IMutationInfo = { + id: RichTextEditingMutation.id, + params: { + actions: [], + textRanges: selections, + unitId, + }, + }; + + const jsonX = JSONX.getInstance(); + const path = getRichTextEditPath(documentDataModel); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); + const commandService = accessor.get(ICommandService); + const result = commandService.syncExecuteCommand(doMutation.id, doMutation.params); + return Boolean(result); + }, +}; diff --git a/packages/docs-ui/src/controllers/menu.schema.ts b/packages/docs-ui/src/controllers/menu.schema.ts index 3fee8cd082..1cf881e35a 100644 --- a/packages/docs-ui/src/controllers/menu.schema.ts +++ b/packages/docs-ui/src/controllers/menu.schema.ts @@ -24,6 +24,7 @@ import { ResetInlineFormatTextBackgroundColorCommand, SetInlineFormatBoldCommand import { BulletListCommand, CheckListCommand, OrderListCommand } from '../commands/commands/list.command'; import { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignRightCommand } from '../commands/commands/paragraph-align.command'; +import { SetParagraphNamedStyleCommand } from '../commands/commands/set-heading.command'; import { SwitchDocModeCommand } from '../commands/commands/switch-doc-mode.command'; import { DocTableDeleteColumnsCommand, DocTableDeleteRowsCommand, DocTableDeleteTableCommand } from '../commands/commands/table/doc-table-delete.command'; import { DocTableInsertColumnLeftCommand, DocTableInsertColumnRightCommand, DocTableInsertRowAboveCommand, DocTableInsertRowBellowCommand } from '../commands/commands/table/doc-table-insert.command'; @@ -60,6 +61,7 @@ import { FontFamilySelectorMenuItemFactory, FontSizeSelectorMenuItemFactory, HeaderFooterMenuItemFactory, + HeadingSelectorMenuItemFactory, HorizontalLineFactory, InsertTableMenuFactory, ItalicMenuItemFactory, @@ -100,6 +102,10 @@ export const menuSchema: MenuSchemaType = { order: 5, menuItemFactory: SuperscriptMenuItemFactory, }, + [SetParagraphNamedStyleCommand.id]: { + order: 5.5, + menuItemFactory: HeadingSelectorMenuItemFactory, + }, [SetInlineFormatFontSizeCommand.id]: { order: 6, menuItemFactory: FontSizeSelectorMenuItemFactory, diff --git a/packages/docs-ui/src/controllers/menu/menu.ts b/packages/docs-ui/src/controllers/menu/menu.ts index c467faf76b..3c1f825afd 100644 --- a/packages/docs-ui/src/controllers/menu/menu.ts +++ b/packages/docs-ui/src/controllers/menu/menu.ts @@ -21,12 +21,15 @@ import type { Subscription } from 'rxjs'; import { BaselineOffset, BooleanNumber, + BuildTextUtils, DEFAULT_STYLES, DOCS_ZEN_EDITOR_UNIT_ID_KEY, DocumentFlavor, HorizontalAlign, ICommandService, IUniverInstanceService, + NAMED_STYLE_MAP, + NamedStyleType, ThemeService, UniverInstanceType, } from '@univerjs/core'; @@ -38,9 +41,12 @@ import { } from '@univerjs/docs'; import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render'; import { + COMMON_LABEL_COMPONENT, FONT_FAMILY_LIST, FONT_SIZE_LIST, getMenuHiddenObservable, + HEADING_ITEM_COMPONENT, + HEADING_LIST, MenuItemType, } from '@univerjs/ui'; @@ -48,8 +54,9 @@ import { combineLatest, map, Observable } from 'rxjs'; import { OpenHeaderFooterPanelCommand } from '../../commands/commands/doc-header-footer.command'; import { HorizontalLineCommand } from '../../commands/commands/doc-horizontal-line.command'; import { getStyleInTextRange, ResetInlineFormatTextBackgroundColorCommand, SetInlineFormatBoldCommand, SetInlineFormatCommand, SetInlineFormatFontFamilyCommand, SetInlineFormatFontSizeCommand, SetInlineFormatItalicCommand, SetInlineFormatStrikethroughCommand, SetInlineFormatSubscriptCommand, SetInlineFormatSuperscriptCommand, SetInlineFormatTextBackgroundColorCommand, SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand } from '../../commands/commands/inline-format.command'; -import { BulletListCommand, CheckListCommand, getParagraphsInRange, OrderListCommand } from '../../commands/commands/list.command'; +import { BulletListCommand, CheckListCommand, OrderListCommand } from '../../commands/commands/list.command'; import { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignOperationCommand, AlignRightCommand } from '../../commands/commands/paragraph-align.command'; +import { SetParagraphNamedStyleCommand } from '../../commands/commands/set-heading.command'; import { SwitchDocModeCommand } from '../../commands/commands/switch-doc-mode.command'; import { DocCreateTableOperation } from '../../commands/operations/doc-create-table.operation'; import { getCommandSkeleton } from '../../commands/util'; @@ -530,6 +537,55 @@ export function FontSizeSelectorMenuItemFactory(accessor: IAccessor): IMenuSelec }; } +export function HeadingSelectorMenuItemFactory(accessor: IAccessor): IMenuSelectorItem { + const commandService = accessor.get(ICommandService); + + return { + id: SetParagraphNamedStyleCommand.id, + type: MenuItemType.SELECTOR, + tooltip: 'toolbar.heading.tooltip', + label: { + name: COMMON_LABEL_COMPONENT, + props: { + selections: HEADING_LIST, + }, + }, + selections: HEADING_LIST.map((item) => ({ + label: { + name: HEADING_ITEM_COMPONENT, + props: { + value: item.value, + text: item.label, + }, + }, + value: item.value, + })), + value$: new Observable((subscriber) => { + const DEFAULT_TYPE = NamedStyleType.NORMAL_TEXT; + const disposable = commandService.onCommandExecuted((c) => { + const id = c.id; + + if (id === SetTextSelectionsOperation.id || id === SetInlineFormatFontSizeCommand.id) { + const paragraph = getParagraphStyleAtCursor(accessor); + if (paragraph == null) { + subscriber.next(DEFAULT_TYPE); + return; + } + + const namedStyleType = paragraph.paragraphStyle?.namedStyleType ?? DEFAULT_TYPE; + subscriber.next(namedStyleType); + } + }); + + subscriber.next(DEFAULT_TYPE); + + return disposable.dispose; + }), + disabled$: disableMenuWhenNoDocRange(accessor), + hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC), + }; +} + export function TextColorSelectorMenuItemFactory(accessor: IAccessor): IMenuSelectorItem { const commandService = accessor.get(ICommandService); const themeService = accessor.get(ThemeService); @@ -774,7 +830,7 @@ const listValueFactory$ = (accessor: IAccessor) => { if (range) { const doc = docDataModel.getSelfOrHeaderFooterModel(range?.segmentId); - const paragraphs = getParagraphsInRange(range, doc.getBody()?.paragraphs ?? []); + const paragraphs = BuildTextUtils.range.getParagraphsInRange(range, doc.getBody()?.paragraphs ?? []); let listType: string | undefined; if (paragraphs.every((p) => { if (!listType) { @@ -939,11 +995,13 @@ function getFontStyleAtCursor(accessor: IAccessor) { const defaultTextStyle = docMenuStyleService.getDefaultStyle(); const cacheStyle = docMenuStyleService.getStyleCache() ?? {}; - + const paragraph = getParagraphStyleAtCursor(accessor); + const namedStyle = paragraph?.paragraphStyle?.namedStyleType ? NAMED_STYLE_MAP[paragraph?.paragraphStyle?.namedStyleType] : null; if (docDataModel == null || activeRange == null) { return { ts: { ...defaultTextStyle, + ...namedStyle, ...cacheStyle, }, }; @@ -956,6 +1014,7 @@ function getFontStyleAtCursor(accessor: IAccessor) { return { ts: { ...defaultTextStyle, + ...namedStyle, ...cacheStyle, }, }; @@ -966,6 +1025,7 @@ function getFontStyleAtCursor(accessor: IAccessor) { return { ts: { ...curTextStyle, + ...namedStyle, ...cacheStyle, }, }; diff --git a/packages/docs-ui/src/docs-ui-plugin.ts b/packages/docs-ui/src/docs-ui-plugin.ts index 0c5ef5754b..95a8c5634f 100644 --- a/packages/docs-ui/src/docs-ui-plugin.ts +++ b/packages/docs-ui/src/docs-ui-plugin.ts @@ -73,6 +73,7 @@ import { import { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignOperationCommand, AlignRightCommand } from './commands/commands/paragraph-align.command'; import { CoverContentCommand, ReplaceContentCommand, ReplaceSelectionCommand, ReplaceSnapshotCommand, ReplaceTextRunsCommand } from './commands/commands/replace-content.command'; import { SetDocZoomRatioCommand } from './commands/commands/set-doc-zoom-ratio.command'; +import { SetParagraphNamedStyleCommand } from './commands/commands/set-heading.command'; import { SwitchDocModeCommand } from './commands/commands/switch-doc-mode.command'; import { CreateDocTableCommand } from './commands/commands/table/doc-table-create.command'; import { DocTableDeleteColumnsCommand, DocTableDeleteRowsCommand, DocTableDeleteTableCommand } from './commands/commands/table/doc-table-delete.command'; @@ -262,8 +263,9 @@ export class UniverDocsUIPlugin extends Plugin { ReplaceTextRunsCommand, ReplaceSelectionCommand, InsertCustomRangeCommand, + SetParagraphNamedStyleCommand, ].forEach((e) => { - this._commandService.registerCommand(e); + this.disposeWithMe(this._commandService.registerCommand(e)); }); [DocCopyCommand, DocCutCommand, DocPasteCommand].forEach((command) => this.disposeWithMe(this._commandService.registerMultipleCommand(command))); diff --git a/packages/docs-ui/src/index.ts b/packages/docs-ui/src/index.ts index 5b43109c05..72d2fccf7b 100644 --- a/packages/docs-ui/src/index.ts +++ b/packages/docs-ui/src/index.ts @@ -95,7 +95,6 @@ export { SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand, } from './commands/commands/inline-format.command'; -export { getParagraphsInRange, getParagraphsInRanges } from './commands/commands/list.command'; export { BulletListCommand, ChangeListNestingLevelCommand, @@ -150,5 +149,5 @@ export { type IMoveCursorOperationParams, MoveSelectionOperation } from './comma export { MoveCursorOperation } from './commands/operations/doc-cursor.operation'; export { DocSelectAllCommand } from './commands/commands/doc-select-all.command'; export { type ISetDocZoomRatioOperationParams, SetDocZoomRatioOperation } from './commands/operations/set-doc-zoom-ratio.operation'; - +export { SetParagraphNamedStyleCommand } from './commands/commands/set-heading.command'; // #endregion diff --git a/packages/docs-ui/src/views/paragraph-setting/hook/utils.ts b/packages/docs-ui/src/views/paragraph-setting/hook/utils.ts index ffa9712f56..6256625062 100644 --- a/packages/docs-ui/src/views/paragraph-setting/hook/utils.ts +++ b/packages/docs-ui/src/views/paragraph-setting/hook/utils.ts @@ -16,7 +16,7 @@ import type { DocumentDataModel, IParagraph, ISectionBreak } from '@univerjs/core'; import type { IDocParagraphSettingCommandParams } from '../../../commands/commands/doc-paragraph-setting.command'; -import { ICommandService, IUniverInstanceService, SpacingRule, UniverInstanceType } from '@univerjs/core'; +import { BuildTextUtils, ICommandService, IUniverInstanceService, SpacingRule, UniverInstanceType } from '@univerjs/core'; import { DocSelectionManagerService, DocSkeletonManagerService } from '@univerjs/docs'; import { getNumberUnitValue, IRenderManagerService } from '@univerjs/engine-render'; import { useDependency } from '@univerjs/ui'; @@ -24,7 +24,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; import { bufferTime, filter, map } from 'rxjs/operators'; import { DocParagraphSettingCommand } from '../../../commands/commands/doc-paragraph-setting.command'; -import { findNearestSectionBreak, getParagraphsInRanges } from '../../../commands/commands/list.command'; +import { findNearestSectionBreak } from '../../../commands/commands/list.command'; import { DocParagraphSettingController } from '../../../controllers/doc-paragraph-setting.controller'; const useDocRanges = () => { @@ -59,7 +59,7 @@ export const useCurrentParagraph = () => { const segmentId = docRanges[0].segmentId; const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs ?? []; - const currentParagraphs = getParagraphsInRanges(docRanges, paragraphs) ?? []; + const currentParagraphs = BuildTextUtils.range.getParagraphsInRanges(docRanges, paragraphs) ?? []; return currentParagraphs; }; diff --git a/packages/engine-render/src/components/docs/layout/tools.ts b/packages/engine-render/src/components/docs/layout/tools.ts index ef6ca3ad8c..a4920abb9e 100644 --- a/packages/engine-render/src/components/docs/layout/tools.ts +++ b/packages/engine-render/src/components/docs/layout/tools.ts @@ -38,8 +38,8 @@ import type { IDocumentSkeletonSection, ISkeletonResourceReference, } from '../../../basics/i-document-skeleton-cached'; - import type { IDocsConfig, IParagraphConfig, ISectionBreakConfig } from '../../../basics/interfaces'; + import type { DataStreamTreeNode } from '../view-model/data-stream-tree-node'; import type { DocumentViewModel } from '../view-model/document-view-model'; import type { Hyphen } from './hyphenation/hyphen'; @@ -54,6 +54,7 @@ import { GridType, HorizontalAlign, mergeWith, + NAMED_STYLE_MAP, NumberUnitType, ObjectMatrix, ObjectRelativeFromH, @@ -851,7 +852,7 @@ export function getFontCreateConfig( const customRange = viewModel.getCustomRange(index + startIndex); const showCustomRange = customRange && (customRange.show !== false); const customRangeStyle = showCustomRange ? getCustomRangeStyle(customRange) : null; - const hasAddonStyle = showCustomRange || showCustomDecoration || !!bullet; + const hasAddonStyle = showCustomRange || showCustomDecoration || !!bullet || paragraphStyle?.namedStyleType; const { st, ed } = textRun; let { ts: textStyle = {} } = textRun; const cache = fontCreateConfigCache.getValue(st, ed); @@ -859,11 +860,14 @@ export function getFontCreateConfig( return cache; } - const { snapToGrid = BooleanNumber.TRUE } = paragraphStyle; + const { snapToGrid = BooleanNumber.TRUE, namedStyleType } = paragraphStyle; const bulletTextStyle = bullet ? getBulletParagraphTextStyle(bullet, viewModel) : null; + // Apply named style if it exists + const namedStyle = namedStyleType ? NAMED_STYLE_MAP[namedStyleType] : null; textStyle = { ...documentTextStyle, + ...namedStyle, ...textStyle, ...customDecorationStyle, ...customRangeStyle, diff --git a/packages/ui/src/components/common-label/index.tsx b/packages/ui/src/components/common-label/index.tsx new file mode 100644 index 0000000000..fa1e08bf09 --- /dev/null +++ b/packages/ui/src/components/common-label/index.tsx @@ -0,0 +1,46 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * 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 { LocaleService } from '@univerjs/core'; +import { useMemo } from 'react'; + +import { useDependency } from '../../utils/di'; + +export interface ICommonLabelProps { + value: string | number; + selections: { value: string | number; label: string }[]; +} + +export const CommonLabel = (props: ICommonLabelProps) => { + const { value, selections } = props; + + const localeService = useDependency(LocaleService); + + const viewValue = useMemo(() => { + if (value == null) return ''; + return localeService.t(selections.find((item) => item.value === value)?.label ?? ''); + }, [value, selections]); + + return ( +
+ {viewValue} +
+ ); +}; + +export const COMMON_LABEL_COMPONENT = 'UI_PLUGIN_COMMON_LABEL_COMPONENT'; diff --git a/packages/ui/src/components/font-size/index.ts b/packages/ui/src/components/font-size/index.ts index 9481eed9d6..8d17dd4302 100644 --- a/packages/ui/src/components/font-size/index.ts +++ b/packages/ui/src/components/font-size/index.ts @@ -15,4 +15,4 @@ */ export { FontSize } from './FontSize'; -export { FONT_SIZE_LIST } from './interface'; +export { FONT_SIZE_LIST, HEADING_LIST } from './interface'; diff --git a/packages/ui/src/components/font-size/interface.ts b/packages/ui/src/components/font-size/interface.ts index b0bc42bab6..6f97562b7d 100644 --- a/packages/ui/src/components/font-size/interface.ts +++ b/packages/ui/src/components/font-size/interface.ts @@ -16,6 +16,7 @@ import type { Observable } from 'rxjs'; import type { ICustomComponentProps } from '../../services/menu/menu'; +import { NamedStyleType } from '@univerjs/core'; export interface IFontSizeProps extends ICustomComponentProps { value: string; @@ -92,3 +93,42 @@ export const FONT_SIZE_LIST = [ value: 72, }, ]; + +export const HEADING_LIST = [ + { + label: 'toolbar.heading.normal', + value: NamedStyleType.NORMAL_TEXT, + }, + { + label: 'toolbar.heading.title', + value: NamedStyleType.TITLE, + }, + { + label: 'toolbar.heading.subTitle', + value: NamedStyleType.SUBTITLE, + }, + { + label: 'toolbar.heading.1', + value: NamedStyleType.HEADING_1, + }, + { + label: 'toolbar.heading.2', + value: NamedStyleType.HEADING_2, + }, + { + label: 'toolbar.heading.3', + value: NamedStyleType.HEADING_3, + }, + { + label: 'toolbar.heading.4', + value: NamedStyleType.HEADING_4, + }, + { + label: 'toolbar.heading.5', + value: NamedStyleType.HEADING_5, + }, + { + label: 'toolbar.heading.6', + value: NamedStyleType.HEADING_6, + }, +]; diff --git a/packages/ui/src/components/heading-item/index.tsx b/packages/ui/src/components/heading-item/index.tsx new file mode 100644 index 0000000000..945293199b --- /dev/null +++ b/packages/ui/src/components/heading-item/index.tsx @@ -0,0 +1,41 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * 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 { NamedStyleType } from '@univerjs/core'; +import { LocaleService, NAMED_STYLE_MAP } from '@univerjs/core'; +import { useDependency } from '../../utils/di'; + +export const HeadingItem = (props: { value: NamedStyleType; text: string }) => { + const { value, text } = props; + const style = NAMED_STYLE_MAP[value]; + + const localeService = useDependency(LocaleService); + + return ( + + {localeService.t(text)} + + ); +}; + +export const HEADING_ITEM_COMPONENT = 'UI_COMPONENT_HEADING_ITEM'; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index cf44ee89f0..3b24503efa 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +export * from './common-label'; export * from './custom-label'; export * from './font-family'; export * from './font-size'; +export * from './heading-item'; export { useScrollYOverContainer } from './hooks/layout'; export { type ISliderProps, Slider } from './slider'; diff --git a/packages/ui/src/controllers/ui/ui-desktop.controller.tsx b/packages/ui/src/controllers/ui/ui-desktop.controller.tsx index 715b97e62b..081bc7821a 100644 --- a/packages/ui/src/controllers/ui/ui-desktop.controller.tsx +++ b/packages/ui/src/controllers/ui/ui-desktop.controller.tsx @@ -20,6 +20,9 @@ import type { IWorkbenchOptions } from './ui.controller'; import { Inject, Injector, IUniverInstanceService, LifecycleService, toDisposable } from '@univerjs/core'; import { render as createRoot, unmount } from '@univerjs/design'; import { IRenderManagerService } from '@univerjs/engine-render'; +import { ComponentManager } from '../../common'; +import { HEADING_ITEM_COMPONENT, HeadingItem } from '../../components'; +import { COMMON_LABEL_COMPONENT, CommonLabel } from '../../components/common-label'; import { ILayoutService } from '../../services/layout/layout.service'; import { IMenuManagerService } from '../../services/menu/menu-manager.service'; import { BuiltInUIPart, IUIPartsService } from '../../services/parts/parts.service'; @@ -40,16 +43,33 @@ export class DesktopUIController extends SingleUnitUIController { @ILayoutService layoutService: ILayoutService, @IUniverInstanceService instanceService: IUniverInstanceService, @IMenuManagerService menuManagerService: IMenuManagerService, - @IUIPartsService uiPartsService: IUIPartsService + @IUIPartsService uiPartsService: IUIPartsService, + @Inject(ComponentManager) private readonly _componentManager: ComponentManager ) { super(injector, instanceService, layoutService, lifecycleService, renderManagerService); menuManagerService.mergeMenu(menuSchema); this._initBuiltinComponents(uiPartsService); + this._registerComponents(); this._bootstrapWorkbench(); } + private _registerComponents() { + this.disposeWithMe( + this._componentManager.register( + COMMON_LABEL_COMPONENT, + CommonLabel + ) + ); + this.disposeWithMe( + this._componentManager.register( + HEADING_ITEM_COMPONENT, + HeadingItem + ) + ); + } + override bootstrap(callback: (contentElement: HTMLElement, containerElement: HTMLElement) => void): IDisposable { return bootstrap(this._injector, this._config, callback); } diff --git a/packages/ui/src/locale/en-US.ts b/packages/ui/src/locale/en-US.ts index 71df3af619..787c8b0fc4 100644 --- a/packages/ui/src/locale/en-US.ts +++ b/packages/ui/src/locale/en-US.ts @@ -17,6 +17,20 @@ import type zhCN from './zh-CN'; const locale: typeof zhCN = { + toolbar: { + heading: { + normal: 'Normal', + title: 'Title', + subTitle: 'Sub Title', + 1: 'Heading 1', + 2: 'Heading 2', + 3: 'Heading 3', + 4: 'Heading 4', + 5: 'Heading 5', + 6: 'Heading 6', + tooltip: 'Set Heading', + }, + }, ribbon: { start: 'Start', insert: 'Insert', diff --git a/packages/ui/src/locale/fa-IR.ts b/packages/ui/src/locale/fa-IR.ts index d427e78a38..251ef08158 100644 --- a/packages/ui/src/locale/fa-IR.ts +++ b/packages/ui/src/locale/fa-IR.ts @@ -17,6 +17,20 @@ import type zhCN from './zh-CN'; const locale: typeof zhCN = { + toolbar: { + heading: { + normal: 'متن عادی', + title: 'عنوان', + subTitle: 'زیر عنوان', + 1: 'عنوان 1', + 2: 'عنوان 2', + 3: 'عنوان 3', + 4: 'عنوان 4', + 5: 'عنوان 5', + 6: 'عنوان 6', + tooltip: 'تنظیم عنوان', + }, + }, ribbon: { start: 'شروع', insert: 'درج', diff --git a/packages/ui/src/locale/fr-FR.ts b/packages/ui/src/locale/fr-FR.ts index 6051258e48..64587b9518 100644 --- a/packages/ui/src/locale/fr-FR.ts +++ b/packages/ui/src/locale/fr-FR.ts @@ -17,6 +17,20 @@ import type enUS from './en-US'; const locale: typeof enUS = { + toolbar: { + heading: { + normal: 'Normal', + title: 'Titre', + subTitle: 'Sous-titre', + 1: 'Titre 1', + 2: 'Titre 2', + 3: 'Titre 3', + 4: 'Titre 4', + 5: 'Titre 5', + 6: 'Titre 6', + tooltip: 'Définir un titre', + }, + }, ribbon: { start: 'Démarrer', insert: 'Insérer', diff --git a/packages/ui/src/locale/ru-RU.ts b/packages/ui/src/locale/ru-RU.ts index 787a296e90..89b9a2f52b 100644 --- a/packages/ui/src/locale/ru-RU.ts +++ b/packages/ui/src/locale/ru-RU.ts @@ -17,6 +17,20 @@ import type zhCN from './zh-CN'; const locale: typeof zhCN = { + toolbar: { + heading: { + normal: 'Обычный текст', + title: 'Заголовок', + subTitle: 'Подзаголовок', + 1: 'Заголовок 1', + 2: 'Заголовок 2', + 3: 'Заголовок 3', + 4: 'Заголовок 4', + 5: 'Заголовок 5', + 6: 'Заголовок 6', + tooltip: 'Установить заголовок', + }, + }, ribbon: { start: 'Начало', insert: 'Вставка', diff --git a/packages/ui/src/locale/vi-VN.ts b/packages/ui/src/locale/vi-VN.ts index 0ac5812020..4a2c3f9e34 100644 --- a/packages/ui/src/locale/vi-VN.ts +++ b/packages/ui/src/locale/vi-VN.ts @@ -17,6 +17,20 @@ import type zhCN from './zh-CN'; const locale: typeof zhCN = { + toolbar: { + heading: { + normal: 'Văn bản', + title: 'Tiêu đề', + subTitle: 'Tiêu đề phụ', + 1: 'Tiêu đề 1', + 2: 'Tiêu đề 2', + 3: 'Tiêu đề 3', + 4: 'Tiêu đề 4', + 5: 'Tiêu đề 5', + 6: 'Tiêu đề 6', + tooltip: 'Đặt tiêu đề', + }, + }, ribbon: { start: 'Bắt đầu', insert: 'Chèn', diff --git a/packages/ui/src/locale/zh-CN.ts b/packages/ui/src/locale/zh-CN.ts index 266ce08e56..1818c2624a 100644 --- a/packages/ui/src/locale/zh-CN.ts +++ b/packages/ui/src/locale/zh-CN.ts @@ -15,6 +15,20 @@ */ const locale = { + toolbar: { + heading: { + normal: '正文', + title: '标题', + subTitle: '副标题', + 1: '标题1', + 2: '标题2', + 3: '标题3', + 4: '标题4', + 5: '标题5', + 6: '标题6', + tooltip: '设置标题', + }, + }, ribbon: { start: '开始', insert: '插入', diff --git a/packages/ui/src/locale/zh-TW.ts b/packages/ui/src/locale/zh-TW.ts index b3e2a07035..582e36ba89 100644 --- a/packages/ui/src/locale/zh-TW.ts +++ b/packages/ui/src/locale/zh-TW.ts @@ -17,6 +17,20 @@ import type zhCN from './zh-CN'; const locale: typeof zhCN = { + toolbar: { + heading: { + normal: '正文', + title: '標題', + subTitle: '副標題', + 1: '標題 1', + 2: '標題 2', + 3: '標題 3', + 4: '標題 4', + 5: '標題 5', + 6: '標題 6', + tooltip: '設定標題', + }, + }, ribbon: { start: '開始', insert: '插入',