feat: support doc heading (#4875)

Co-authored-by: GitHub Actions <actions@github.com>
This commit is contained in:
ZhangWei
2025-03-26 20:18:39 +08:00
committed by GitHub
co-authored by GitHub Actions
parent 060afe5759
commit b605b83030
30 changed files with 557 additions and 73 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

@@ -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: [
{
@@ -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 = {
@@ -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;
};
@@ -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<IParagraph> = 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;
}
+16
View File
@@ -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, Nullable<ITextStyle>> = {
[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,
};
+1 -1
View File
@@ -32,7 +32,7 @@ export function getTextRunAtPosition(
const retTextRun: ITextRun = {
st: 0,
ed: 0,
ts: isCellEditor ? {} : defaultStyle,
ts: {},
};
if (isFormula) {
@@ -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<Pick<IParagraphStyle, 'hanging' | 'horizontalAlign' | 'spaceBelow' | 'spaceAbove' | 'indentEnd' | 'indentStart' | 'lineSpacing' | 'indentFirstLine' | 'snapToGrid' | 'spacingRule'>>;
@@ -46,7 +45,7 @@ export const DocParagraphSettingCommand: ICommand<IDocParagraphSettingCommandPar
const unitId = docDataModel.getUnitId();
const allParagraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs ?? [];
const paragraphs = getParagraphsInRanges(docRanges, allParagraphs) ?? [];
const paragraphs = BuildTextUtils.range.getParagraphsInRanges(docRanges, allParagraphs) ?? [];
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
id: RichTextEditingMutation.id,
@@ -67,7 +67,7 @@ export const ListOperationCommand: ICommand<IListOperationCommandParams> = {
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<IChangeListTypeCommandParams> = {
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<IChangeListNestingLevelComm
return false;
}
const currentParagraphs = getParagraphsInRange(activeRange, paragraphs);
const currentParagraphs = BuildTextUtils.range.getParagraphsInRange(activeRange, paragraphs);
const unitId = docDataModel.getUnitId();
const jsonX = JSONX.getInstance();
@@ -472,29 +472,8 @@ export const QuickListCommand: ICommand<IQuickListCommandParams> = {
},
};
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++) {
@@ -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<IAlignOperationCommandParams> = {
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);
@@ -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<ISetParagraphNamedStyleCommandParams> = {
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<DocumentDataModel>(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<IRichTextEditingMutationParams> = {
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);
},
};
@@ -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,
+63 -3
View File
@@ -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<number> {
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<string> {
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,
},
};
+3 -1
View File
@@ -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)));
+1 -2
View File
@@ -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
@@ -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;
};
@@ -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,
@@ -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 (
<div
className="univer-overflow-hidden univer-truncate univer-text-[13px]"
>
{viewValue}
</div>
);
};
export const COMMON_LABEL_COMPONENT = 'UI_PLUGIN_COMMON_LABEL_COMPONENT';
@@ -15,4 +15,4 @@
*/
export { FontSize } from './FontSize';
export { FONT_SIZE_LIST } from './interface';
export { FONT_SIZE_LIST, HEADING_LIST } from './interface';
@@ -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<string> {
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,
},
];
@@ -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 (
<span
className="univer-text-[13px]"
style={{
fontSize: style?.fs,
fontWeight: style?.bl ? 'bold' : 'normal',
color: style?.cl?.rgb ?? undefined,
}}
>
{localeService.t(text)}
</span>
);
};
export const HEADING_ITEM_COMPONENT = 'UI_COMPONENT_HEADING_ITEM';
+2
View File
@@ -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';
@@ -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);
}
+14
View File
@@ -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',
+14
View File
@@ -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: 'درج',
+14
View File
@@ -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',
+14
View File
@@ -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: 'Вставка',
+14
View File
@@ -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',
+14
View File
@@ -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: '插入',
+14
View File
@@ -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: '插入',