diff --git a/packages/core/src/docs/data-model/text-x/utils.ts b/packages/core/src/docs/data-model/text-x/utils.ts index 201f6f11dc7..9b083e5a591 100644 --- a/packages/core/src/docs/data-model/text-x/utils.ts +++ b/packages/core/src/docs/data-model/text-x/utils.ts @@ -18,6 +18,7 @@ import type { ICustomBlock, ICustomDecoration, ICustomRange, IDocumentBody, IPar import type { IRetainAction } from './action-types'; import { UpdateDocsAttributeType } from '../../../shared/command-enum'; import { Tools } from '../../../shared/tools'; +import { normalizeTextRuns } from './apply-utils/common'; import { coverTextRuns } from './apply-utils/update-apply'; export enum SliceBodyType { @@ -65,14 +66,16 @@ export function getBodySlice( } } - docBody.textRuns = newTextRuns.map((tr) => { - const { st, ed } = tr; - return { - ...tr, - st: st - startOffset, - ed: ed - startOffset, - }; - }); + docBody.textRuns = normalizeTextRuns( + newTextRuns.map((tr) => { + const { st, ed } = tr; + return { + ...tr, + st: st - startOffset, + ed: ed - startOffset, + }; + }) + ); } else if (returnEmptyArray) { // In the case of no style before, add the style, removeTextRuns will be empty, // in this case, you need to add an empty textRun for undo. diff --git a/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts b/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts index 48e8308b9fe..997efe84503 100644 --- a/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts +++ b/packages/docs-ui/src/commands/commands/__tests__/inline-format.command.spec.ts @@ -74,6 +74,7 @@ describe('Test inline format commands', () => { startOffset: 0, endOffset: 5, collapsed: false, + isActive: true, }, ]); diff --git a/packages/docs-ui/src/commands/commands/inline-format.command.ts b/packages/docs-ui/src/commands/commands/inline-format.command.ts index a2dfd23848e..9636d4fe331 100644 --- a/packages/docs-ui/src/commands/commands/inline-format.command.ts +++ b/packages/docs-ui/src/commands/commands/inline-format.command.ts @@ -17,15 +17,19 @@ import type { DocumentDataModel, ICommand, IDocumentBody, IMutationInfo, IStyleBase, ITextDecoration, ITextRun, + ITextStyle, + Nullable, } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { ITextRangeWithStyle } from '@univerjs/engine-render'; import { BaselineOffset, BooleanNumber, CommandType, DOC_RANGE_TYPE, + getBodySlice, ICommandService, IUniverInstanceService, JSONX, MemoryCursor, TextX, TextXActionType, + Tools, UniverInstanceType, } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; @@ -231,7 +235,7 @@ const COMMAND_ID_TO_FORMAT_KEY_MAP: Record = { export const SetInlineFormatCommand: ICommand = { id: 'doc.command.set-inline-format', type: CommandType.COMMAND, - // eslint-disable-next-line max-lines-per-function + // eslint-disable-next-line max-lines-per-function, complexity handler: async (accessor, params: ISetInlineFormatCommandParams) => { const { value, preCommandId } = params; const commandService = accessor.get(ICommandService); @@ -240,8 +244,9 @@ export const SetInlineFormatCommand: ICommand = { const docMenuStyleService = accessor.get(DocMenuStyleService); const docRanges = docSelectionManagerService.getDocRanges(); + const activeTextRange = docSelectionManagerService.getActiveTextRange(); - if (docRanges.length === 0) { + if (docRanges.length === 0 || activeTextRange == null) { return false; } @@ -252,6 +257,12 @@ export const SetInlineFormatCommand: ICommand = { return false; } + const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); + + if (body == null) { + return false; + } + const unitId = docDataModel.getUnitId(); let formatValue; @@ -263,10 +274,16 @@ export const SetInlineFormatCommand: ICommand = { case SetInlineFormatStrikethroughCommand.id: // fallthrough case SetInlineFormatSubscriptCommand.id: // fallthrough case SetInlineFormatSuperscriptCommand.id: { + const defaultStyle = docMenuStyleService.getDefaultStyle(); + const curTextStyle = getStyleInTextRange( + body, + activeTextRange, + defaultStyle + ); + formatValue = getReverseFormatValueInSelection( - docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()!.textRuns!, - preCommandId, - docRanges + curTextStyle, + preCommandId ); break; @@ -327,9 +344,18 @@ export const SetInlineFormatCommand: ICommand = { if (startOffset === endOffset) { // Cache the menu style for next input. + const cacheStyle = docMenuStyleService.getStyleCache(); + const key = COMMAND_ID_TO_FORMAT_KEY_MAP[preCommandId]; + docMenuStyleService.setStyleCache( { - [COMMAND_ID_TO_FORMAT_KEY_MAP[preCommandId]]: formatValue, + [key]: cacheStyle?.[key] !== undefined + ? getReverseFormatValue( + cacheStyle, + key, + preCommandId + ) + : formatValue, } ); continue; @@ -383,71 +409,105 @@ function isTextDecoration(value: unknown | ITextDecoration): value is ITextDecor return value !== null && typeof value === 'object'; } -/** - * When clicking on a Bold menu item, you should un-bold if there is bold in the selections, - * or bold if there is no bold text. This method is used to get the reverse style value calculated - * from textRuns in the selection - */ -// eslint-disable-next-line complexity -function getReverseFormatValueInSelection( - textRuns: ITextRun[], - preCommandId: string, - docRanges: ITextRangeWithStyle[] -): BooleanNumber | ITextDecoration | BaselineOffset { - let ti = 0; - let si = 0; - const key: keyof IStyleBase = COMMAND_ID_TO_FORMAT_KEY_MAP[preCommandId]; - - while (ti !== textRuns.length && si !== docRanges.length) { - const { startOffset, endOffset } = docRanges[si]; +function getReverseFormatValue(ts: Nullable, key: keyof IStyleBase, preCommandId: string) { + if (/bl|it/.test(key)) { + return ts?.[key] === BooleanNumber.TRUE ? BooleanNumber.FALSE : BooleanNumber.TRUE; + } - // TODO: @jocs handle sid in textRun - const { st, ed, ts } = textRuns[ti]; + if (/ul|st/.test(key)) { + return isTextDecoration(ts?.[key]) && (ts?.[key] as ITextDecoration).s === BooleanNumber.TRUE + ? { + s: BooleanNumber.FALSE, + } + : { + s: BooleanNumber.TRUE, + }; + } - if (endOffset! <= st) { - si++; - } else if (ed <= startOffset!) { - ti++; + if (/va/.test(key)) { + if (preCommandId === SetInlineFormatSubscriptCommand.id) { + return ts?.[key] === BaselineOffset.SUBSCRIPT + ? BaselineOffset.NORMAL + : BaselineOffset.SUBSCRIPT; } else { - if (/bl|it/.test(key)) { - return ts?.[key] === BooleanNumber.TRUE ? BooleanNumber.FALSE : BooleanNumber.TRUE; - } + return ts?.[key] === BaselineOffset.SUPERSCRIPT + ? BaselineOffset.NORMAL + : BaselineOffset.SUPERSCRIPT; + } + } +} - if (/ul|st/.test(key)) { - return isTextDecoration(ts?.[key]) && (ts?.[key] as ITextDecoration).s === BooleanNumber.TRUE - ? { - s: BooleanNumber.FALSE, - } - : { - s: BooleanNumber.TRUE, - }; +// eslint-disable-next-line complexity +export function getStyleInTextRange( + body: IDocumentBody, + textRange: ITextRangeWithStyle, + defaultStyle: ITextStyle +): ITextStyle { + const { startOffset, endOffset, collapsed } = textRange; + + if (collapsed) { + const textRuns = body.textRuns ?? []; + let textRun: Nullable = null; + + for (let i = textRuns.length - 1; i >= 0; i--) { + const curTextRun = textRuns[i]; + if (curTextRun.st < startOffset && startOffset <= curTextRun.ed) { + textRun = curTextRun; + break; } + } + + return textRun?.ts ? { ...defaultStyle, ...textRun.ts } : defaultStyle; + } + + const { textRuns = [] } = getBodySlice(body, startOffset, endOffset); + + const style = Tools.deepClone(defaultStyle); - if (/va/.test(key)) { - if (preCommandId === SetInlineFormatSubscriptCommand.id) { - return ts?.[key] === BaselineOffset.SUBSCRIPT - ? BaselineOffset.NORMAL - : BaselineOffset.SUBSCRIPT; - } else { - return ts?.[key] === BaselineOffset.SUPERSCRIPT - ? BaselineOffset.NORMAL - : BaselineOffset.SUPERSCRIPT; - } + // Get the min font size in range. + style.fs = Math.max(style.fs!, ...textRuns.map((t) => t?.ts?.fs ?? style.fs!)); + style.ff = textRuns.find((t) => t.ts?.ff != null)?.ts?.ff ?? style.ff; + style.it = textRuns.length && textRuns.every((t) => t.ts?.it === BooleanNumber.TRUE) ? BooleanNumber.TRUE : BooleanNumber.FALSE; + style.bl = textRuns.length && textRuns.every((t) => t.ts?.bl === BooleanNumber.TRUE) ? BooleanNumber.TRUE : BooleanNumber.FALSE; + style.ul = textRuns.length && textRuns.every((t) => t.ts?.ul?.s === BooleanNumber.TRUE) ? textRuns[0].ts?.ul : style.ul; + style.st = textRuns.length && textRuns.every((t) => t.ts?.st?.s === BooleanNumber.TRUE) ? textRuns[0].ts?.st : style.st; + style.bg = textRuns.find((t) => t.ts?.bg != null)?.ts?.bg ?? style.bg; + style.cl = textRuns.find((t) => t.ts?.cl != null)?.ts?.cl ?? style.cl; + + const vas = textRuns.filter((t) => t?.ts?.va != null); + + if (vas.length > 0 && vas.length === textRuns.length) { + const va = vas[0].ts?.va; + let isSame = true; + + for (let i = 1; i < vas.length; i++) { + if (vas[i].ts?.va !== va) { + isSame = false; + break; } + } - ti++; + if (isSame) { + style.va = va; } } - if (/bl|it/.test(key)) { - return BooleanNumber.TRUE; - } else if (/ul|st/.test(key)) { - return { - s: BooleanNumber.TRUE, - }; - } else { - return preCommandId === SetInlineFormatSubscriptCommand.id - ? BaselineOffset.SUBSCRIPT - : BaselineOffset.SUPERSCRIPT; - } + return style; +} + +/** + * When clicking on a Bold menu item, you should un-bold if there is bold in the selections, + * or bold if there is no bold text. This method is used to get the reverse style value calculated + * from textRuns in the selection + */ + +function getReverseFormatValueInSelection( + textStyle: ITextStyle, + preCommandId: string +): BooleanNumber | ITextDecoration | BaselineOffset { + const key: keyof IStyleBase = COMMAND_ID_TO_FORMAT_KEY_MAP[preCommandId]; + + const reverseValue = getReverseFormatValue(textStyle, key, preCommandId)!; + + return reverseValue; } diff --git a/packages/docs-ui/src/controllers/menu/menu.ts b/packages/docs-ui/src/controllers/menu/menu.ts index 059e88090ab..6cc1f817905 100644 --- a/packages/docs-ui/src/controllers/menu/menu.ts +++ b/packages/docs-ui/src/controllers/menu/menu.ts @@ -47,7 +47,7 @@ import { import { combineLatest, map, Observable } from 'rxjs'; import { OpenHeaderFooterPanelCommand } from '../../commands/commands/doc-header-footer.command'; -import { ResetInlineFormatTextBackgroundColorCommand, SetInlineFormatBoldCommand, SetInlineFormatCommand, SetInlineFormatFontFamilyCommand, SetInlineFormatFontSizeCommand, SetInlineFormatItalicCommand, SetInlineFormatStrikethroughCommand, SetInlineFormatSubscriptCommand, SetInlineFormatSuperscriptCommand, SetInlineFormatTextBackgroundColorCommand, SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand } from '../../commands/commands/inline-format.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 { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignOperationCommand, AlignRightCommand } from '../../commands/commands/paragraph-align.command'; import { SwitchDocModeCommand } from '../../commands/commands/switch-doc-mode.command'; @@ -939,11 +939,10 @@ function getFontStyleAtCursor(accessor: IAccessor) { }; } - const { startOffset, segmentId } = activeTextRange; - - const textRuns = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.textRuns; + const { segmentId } = activeTextRange; + const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); - if (textRuns == null) { + if (body == null) { return { ts: { ...defaultTextStyle, @@ -952,22 +951,11 @@ function getFontStyleAtCursor(accessor: IAccessor) { }; } - let textRun; - - for (let i = textRuns.length - 1; i >= 0; i--) { - const curTextRun = textRuns[i]; - - if (curTextRun.st < startOffset && startOffset <= curTextRun.ed) { - textRun = curTextRun; - break; - } - } + const curTextStyle = getStyleInTextRange(body, activeTextRange, defaultTextStyle); return { - ...textRun, ts: { - ...defaultTextStyle, - ...textRun?.ts, + ...curTextStyle, ...cacheStyle, }, };