refactor(editor): narrow text format parameter (#12946)

#### PR Dependency Tree


* **PR #12946** 👈
  * **PR #12947**
    * **PR #12948**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Updated terminology and types from "text style" to "text attributes"
throughout the text formatting features for improved clarity and
consistency.
* Separated style-specific attributes (like bold, italic, color, and
background) from other text metadata.
* Renamed relevant commands and updated menu and toolbar configurations
to use the new attribute structure.

* **New Features**
* Added support for color and background properties in text style
attributes.

* **Bug Fixes**
* Improved consistency and reliability in text formatting and
highlighting behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-02 14:55:14 +08:00
committed by GitHub
parent 423c5bd711
commit bcd6a70b59
9 changed files with 82 additions and 68 deletions

View File

@@ -9,6 +9,7 @@ import {
promptDocTitle, promptDocTitle,
} from '@blocksuite/affine-block-embed'; } from '@blocksuite/affine-block-embed';
import { updateBlockType } from '@blocksuite/affine-block-note'; import { updateBlockType } from '@blocksuite/affine-block-note';
import type { HighlightType } from '@blocksuite/affine-components/highlight-dropdown-menu';
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar'; import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import { import {
@@ -38,7 +39,6 @@ import type {
ToolbarModuleConfig, ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { ActionPlacement } from '@blocksuite/affine-shared/services'; import { ActionPlacement } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { tableViewMeta } from '@blocksuite/data-view/view-presets'; import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import { import {
CopyIcon, CopyIcon,
@@ -140,7 +140,7 @@ const highlightActionGroup = {
id: 'c.highlight', id: 'c.highlight',
when: ({ chain }) => isFormatSupported(chain).run()[0], when: ({ chain }) => isFormatSupported(chain).run()[0],
content({ chain }) { content({ chain }) {
const updateHighlight = (styles: AffineTextAttributes) => { const updateHighlight = (styles: HighlightType) => {
const payload = { styles }; const payload = { styles };
chain chain
.try(chain => [ .try(chain => [

View File

@@ -1,4 +1,4 @@
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
import { PropTypes, requiredProperties } from '@blocksuite/std'; import { PropTypes, requiredProperties } from '@blocksuite/std';
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
@@ -20,7 +20,10 @@ const colors = [
'grey', 'grey',
] as const; ] as const;
type HighlightType = 'color' | 'background'; export type HighlightType = Pick<
AffineTextStyleAttributes,
'color' | 'background'
>;
// TODO(@fundon): these recent settings should be added to the dropdown menu // TODO(@fundon): these recent settings should be added to the dropdown menu
// tests/blocksutie/e2e/format-bar.spec.ts#253 // tests/blocksutie/e2e/format-bar.spec.ts#253
@@ -33,13 +36,13 @@ type HighlightType = 'color' | 'background';
}) })
export class HighlightDropdownMenu extends LitElement { export class HighlightDropdownMenu extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
accessor updateHighlight!: (styles: AffineTextAttributes) => void; accessor updateHighlight!: (styles: HighlightType) => void;
private readonly _update = (value: string | null, type: HighlightType) => { private readonly _update = (style: HighlightType) => {
// latestHighlightColor = value; // latestHighlightColor = value;
// latestHighlightType = type; // latestHighlightType = type;
this.updateHighlight({ [`${type}`]: value }); this.updateHighlight(style);
}; };
override render() { override render() {
@@ -71,7 +74,7 @@ export class HighlightDropdownMenu extends LitElement {
return html` return html`
<editor-menu-action <editor-menu-action
data-testid="foreground-${color}" data-testid="foreground-${color}"
@click=${() => this._update(value, 'color')} @click=${() => this._update({ color: value })}
> >
<affine-text-duotone-icon <affine-text-duotone-icon
style=${styleMap({ style=${styleMap({
@@ -92,7 +95,7 @@ export class HighlightDropdownMenu extends LitElement {
return html` return html`
<editor-menu-action <editor-menu-action
data-testid="background-${color}" data-testid="background-${color}"
@click=${() => this._update(value, 'background')} @click=${() => this._update({ background: value })}
> >
<affine-text-duotone-icon <affine-text-duotone-icon
style=${styleMap({ style=${styleMap({

View File

@@ -11,7 +11,7 @@ import { type EditorHost, TextSelection } from '@blocksuite/std';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import { import {
isTextStyleActive, isTextAttributeActive,
toggleBold, toggleBold,
toggleCode, toggleCode,
toggleItalic, toggleItalic,
@@ -38,7 +38,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => { activeWhen: host => {
const [result] = host.std.command const [result] = host.std.command
.chain() .chain()
.pipe(isTextStyleActive, { key: 'bold' }) .pipe(isTextAttributeActive, { key: 'bold' })
.run(); .run();
return result; return result;
}, },
@@ -54,7 +54,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => { activeWhen: host => {
const [result] = host.std.command const [result] = host.std.command
.chain() .chain()
.pipe(isTextStyleActive, { key: 'italic' }) .pipe(isTextAttributeActive, { key: 'italic' })
.run(); .run();
return result; return result;
}, },
@@ -70,7 +70,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => { activeWhen: host => {
const [result] = host.std.command const [result] = host.std.command
.chain() .chain()
.pipe(isTextStyleActive, { key: 'underline' }) .pipe(isTextAttributeActive, { key: 'underline' })
.run(); .run();
return result; return result;
}, },
@@ -86,7 +86,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => { activeWhen: host => {
const [result] = host.std.command const [result] = host.std.command
.chain() .chain()
.pipe(isTextStyleActive, { key: 'strike' }) .pipe(isTextAttributeActive, { key: 'strike' })
.run(); .run();
return result; return result;
}, },
@@ -102,7 +102,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => { activeWhen: host => {
const [result] = host.std.command const [result] = host.std.command
.chain() .chain()
.pipe(isTextStyleActive, { key: 'code' }) .pipe(isTextAttributeActive, { key: 'code' })
.run(); .run();
return result; return result;
}, },
@@ -118,7 +118,7 @@ export const textFormatConfigs: TextFormatConfig[] = [
activeWhen: host => { activeWhen: host => {
const [result] = host.std.command const [result] = host.std.command
.chain() .chain()
.pipe(isTextStyleActive, { key: 'link' }) .pipe(isTextAttributeActive, { key: 'link' })
.run(); .run();
return result; return result;
}, },

View File

@@ -1,6 +1,9 @@
import { clearMarksOnDiscontinuousInput } from '@blocksuite/affine-rich-text'; import { clearMarksOnDiscontinuousInput } from '@blocksuite/affine-rich-text';
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands'; import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type {
AffineTextAttributes,
AffineTextStyleAttributes,
} from '@blocksuite/affine-shared/types';
import type { Command, TextSelection } from '@blocksuite/std'; import type { Command, TextSelection } from '@blocksuite/std';
import { import {
INLINE_ROOT_ATTR, INLINE_ROOT_ATTR,
@@ -13,7 +16,7 @@ import { FORMAT_TEXT_SUPPORT_FLAVOURS } from './consts.js';
export const formatTextCommand: Command<{ export const formatTextCommand: Command<{
currentTextSelection?: TextSelection; currentTextSelection?: TextSelection;
textSelection?: TextSelection; textSelection?: TextSelection;
styles: AffineTextAttributes; styles: AffineTextStyleAttributes;
mode?: 'replace' | 'merge'; mode?: 'replace' | 'merge';
}> = (ctx, next) => { }> = (ctx, next) => {
const { styles, mode = 'merge' } = ctx; const { styles, mode = 'merge' } = ctx;

View File

@@ -10,8 +10,8 @@ export { formatBlockCommand } from './format-block.js';
export { formatNativeCommand } from './format-native.js'; export { formatNativeCommand } from './format-native.js';
export { formatTextCommand } from './format-text.js'; export { formatTextCommand } from './format-text.js';
export { export {
getTextStyle, getTextAttributes,
isTextStyleActive, isTextAttributeActive,
toggleBold, toggleBold,
toggleCode, toggleCode,
toggleItalic, toggleItalic,

View File

@@ -2,25 +2,31 @@ import {
getBlockSelectionsCommand, getBlockSelectionsCommand,
getTextSelectionCommand, getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type {
AffineTextAttributes,
AffineTextStyleAttributes,
} from '@blocksuite/affine-shared/types';
import type { Command } from '@blocksuite/std'; import type { Command } from '@blocksuite/std';
import { formatBlockCommand } from './format-block.js'; import { formatBlockCommand } from './format-block.js';
import { formatNativeCommand } from './format-native.js'; import { formatNativeCommand } from './format-native.js';
import { formatTextCommand } from './format-text.js'; import { formatTextCommand } from './format-text.js';
import { getCombinedTextStyle } from './utils.js'; import { getCombinedTextAttributes } from './utils.js';
export const toggleTextStyleCommand: Command<{ export const toggleTextStyleCommand: Command<{
key: Extract< key: Extract<
keyof AffineTextAttributes, keyof AffineTextStyleAttributes,
'bold' | 'italic' | 'underline' | 'strike' | 'code' 'bold' | 'italic' | 'underline' | 'strike' | 'code'
>; >;
}> = (ctx, next) => { }> = (ctx, next) => {
const { std, key } = ctx; const { std, key } = ctx;
const [active] = std.command.chain().pipe(isTextStyleActive, { key }).run(); const [active] = std.command
.chain()
.pipe(isTextAttributeActive, { key })
.run();
const payload: { const payload: {
styles: AffineTextAttributes; styles: AffineTextStyleAttributes;
mode?: 'replace' | 'merge'; mode?: 'replace' | 'merge';
} = { } = {
styles: { styles: {
@@ -46,7 +52,7 @@ export const toggleTextStyleCommand: Command<{
const toggleTextStyleCommandWrapper = ( const toggleTextStyleCommandWrapper = (
key: Extract< key: Extract<
keyof AffineTextAttributes, keyof AffineTextStyleAttributes,
'bold' | 'italic' | 'underline' | 'strike' | 'code' 'bold' | 'italic' | 'underline' | 'strike' | 'code'
> >
): Command => { ): Command => {
@@ -66,30 +72,29 @@ export const toggleUnderline = toggleTextStyleCommandWrapper('underline');
export const toggleStrike = toggleTextStyleCommandWrapper('strike'); export const toggleStrike = toggleTextStyleCommandWrapper('strike');
export const toggleCode = toggleTextStyleCommandWrapper('code'); export const toggleCode = toggleTextStyleCommandWrapper('code');
export const getTextStyle: Command<{}, { textStyle: AffineTextAttributes }> = ( export const getTextAttributes: Command<
ctx, {},
next { textAttributes: AffineTextAttributes }
) => { > = (ctx, next) => {
const [result, innerCtx] = getCombinedTextStyle( const [result, innerCtx] = getCombinedTextAttributes(
ctx.std.command.chain() ctx.std.command.chain()
).run(); ).run();
if (!result) { if (!result) {
return false; return false;
} }
return next({ textStyle: innerCtx.textStyle }); return next({ textAttributes: innerCtx.textAttributes });
}; };
export const isTextStyleActive: Command<{ key: keyof AffineTextAttributes }> = ( export const isTextAttributeActive: Command<{
ctx, key: keyof AffineTextAttributes;
next }> = (ctx, next) => {
) => {
const key = ctx.key; const key = ctx.key;
const [result] = getCombinedTextStyle(ctx.std.command.chain()) const [result] = getCombinedTextAttributes(ctx.std.command.chain())
.pipe((ctx, next) => { .pipe((ctx, next) => {
const { textStyle } = ctx; const { textAttributes } = ctx;
if (textStyle && key in textStyle) { if (textAttributes && key in textAttributes) {
return next(); return next();
} }

View File

@@ -77,8 +77,8 @@ function handleCurrentSelection(
handler: ( handler: (
type: 'text' | 'block' | 'native', type: 'text' | 'block' | 'native',
inlineEditors: InlineEditor<AffineTextAttributes>[] inlineEditors: InlineEditor<AffineTextAttributes>[]
) => { textStyle: AffineTextAttributes } | boolean | void ) => { textAttributes: AffineTextAttributes } | boolean | void
): Chain<InitCommandCtx & { textStyle: AffineTextAttributes }> { ): Chain<InitCommandCtx & { textAttributes: AffineTextAttributes }> {
return chain.try(chain => [ return chain.try(chain => [
// text selection, corresponding to `formatText` command // text selection, corresponding to `formatText` command
chain chain
@@ -174,25 +174,25 @@ function handleCurrentSelection(
]); ]);
} }
export function getCombinedTextStyle(chain: Chain<InitCommandCtx>) { export function getCombinedTextAttributes(chain: Chain<InitCommandCtx>) {
return handleCurrentSelection(chain, (type, inlineEditors) => { return handleCurrentSelection(chain, (type, inlineEditors) => {
if (type === 'text') { if (type === 'text') {
return { return {
textStyle: getCombinedFormatFromInlineEditors( textAttributes: getCombinedFormatFromInlineEditors(
inlineEditors.map(e => [e, e.getInlineRange()]) inlineEditors.map(e => [e, e.getInlineRange()])
), ),
}; };
} }
if (type === 'block') { if (type === 'block') {
return { return {
textStyle: getCombinedFormatFromInlineEditors( textAttributes: getCombinedFormatFromInlineEditors(
inlineEditors.map(e => [e, { index: 0, length: e.yTextLength }]) inlineEditors.map(e => [e, { index: 0, length: e.yTextLength }])
), ),
}; };
} }
if (type === 'native') { if (type === 'native') {
return { return {
textStyle: getCombinedFormatFromInlineEditors( textAttributes: getCombinedFormatFromInlineEditors(
inlineEditors.map(e => [e, e.getInlineRange()]) inlineEditors.map(e => [e, e.getInlineRange()])
), ),
}; };

View File

@@ -35,27 +35,30 @@ export type IndentContext = {
type: 'indent' | 'dedent'; type: 'indent' | 'dedent';
}; };
export interface AffineTextAttributes { export type AffineTextStyleAttributes = {
bold?: true | null; bold?: true | null;
italic?: true | null; italic?: true | null;
underline?: true | null; underline?: true | null;
strike?: true | null; strike?: true | null;
code?: true | null; code?: true | null;
color?: string | null;
background?: string | null;
};
export type AffineTextAttributes = AffineTextStyleAttributes & {
link?: string | null; link?: string | null;
reference?: reference?:
| ({ | ({
type: 'Subpage' | 'LinkedPage'; type: 'Subpage' | 'LinkedPage';
} & ReferenceInfo) } & ReferenceInfo)
| null; | null;
background?: string | null;
color?: string | null;
latex?: string | null; latex?: string | null;
footnote?: FootNote | null; footnote?: FootNote | null;
mention?: { mention?: {
member: string; member: string;
notification?: string; notification?: string;
} | null; } | null;
} };
export type AffineInlineEditor = InlineEditor<AffineTextAttributes>; export type AffineInlineEditor = InlineEditor<AffineTextAttributes>;

View File

@@ -26,7 +26,7 @@ import {
formatBlockCommand, formatBlockCommand,
formatNativeCommand, formatNativeCommand,
formatTextCommand, formatTextCommand,
getTextStyle, getTextAttributes,
toggleBold, toggleBold,
toggleCode, toggleCode,
toggleItalic, toggleItalic,
@@ -45,7 +45,7 @@ import {
getTextSelectionCommand, getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
import { import {
createDefaultDoc, createDefaultDoc,
openSingleFileWith, openSingleFileWith,
@@ -817,8 +817,8 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
name: 'Bold', name: 'Bold',
icon: BoldIcon(), icon: BoldIcon(),
background: ({ std }) => { background: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
return textStyle?.bold ? '#00000012' : ''; return textAttributes?.bold ? '#00000012' : '';
}, },
action: ({ std }) => { action: ({ std }) => {
std.command.exec(toggleBold); std.command.exec(toggleBold);
@@ -828,8 +828,8 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
name: 'Italic', name: 'Italic',
icon: ItalicIcon(), icon: ItalicIcon(),
background: ({ std }) => { background: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
return textStyle?.italic ? '#00000012' : ''; return textAttributes?.italic ? '#00000012' : '';
}, },
action: ({ std }) => { action: ({ std }) => {
std.command.exec(toggleItalic); std.command.exec(toggleItalic);
@@ -839,8 +839,8 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
name: 'UnderLine', name: 'UnderLine',
icon: UnderLineIcon(), icon: UnderLineIcon(),
background: ({ std }) => { background: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
return textStyle?.underline ? '#00000012' : ''; return textAttributes?.underline ? '#00000012' : '';
}, },
action: ({ std }) => { action: ({ std }) => {
std.command.exec(toggleUnderline); std.command.exec(toggleUnderline);
@@ -850,8 +850,8 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
name: 'StrikeThrough', name: 'StrikeThrough',
icon: StrikeThroughIcon(), icon: StrikeThroughIcon(),
background: ({ std }) => { background: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
return textStyle?.strike ? '#00000012' : ''; return textAttributes?.strike ? '#00000012' : '';
}, },
action: ({ std }) => { action: ({ std }) => {
std.command.exec(toggleStrike); std.command.exec(toggleStrike);
@@ -861,8 +861,8 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
name: 'Code', name: 'Code',
icon: CodeIcon(), icon: CodeIcon(),
background: ({ std }) => { background: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
return textStyle?.code ? '#00000012' : ''; return textAttributes?.code ? '#00000012' : '';
}, },
action: ({ std }) => { action: ({ std }) => {
std.command.exec(toggleCode); std.command.exec(toggleCode);
@@ -872,8 +872,8 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
name: 'Link', name: 'Link',
icon: LinkIcon(), icon: LinkIcon(),
background: ({ std }) => { background: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
return textStyle?.link ? '#00000012' : ''; return textAttributes?.link ? '#00000012' : '';
}, },
action: ({ std }) => { action: ({ std }) => {
std.command.exec(toggleLink); std.command.exec(toggleLink);
@@ -883,9 +883,9 @@ const textStyleToolItems: KeyboardToolbarItem[] = [
const highlightToolPanel: KeyboardToolPanelConfig = { const highlightToolPanel: KeyboardToolPanelConfig = {
icon: ({ std }) => { icon: ({ std }) => {
const [_, { textStyle }] = std.command.exec(getTextStyle); const [_, { textAttributes }] = std.command.exec(getTextAttributes);
if (textStyle?.color) { if (textAttributes?.color) {
return HighLightDuotoneIcon(textStyle.color); return HighLightDuotoneIcon(textAttributes.color);
} else { } else {
return HighLightDuotoneIcon(cssVarV2('icon/primary')); return HighLightDuotoneIcon(cssVarV2('icon/primary'));
} }
@@ -916,7 +916,7 @@ const highlightToolPanel: KeyboardToolPanelConfig = {
const payload = { const payload = {
styles: { styles: {
color: cssVarV2(`text/highlight/fg/${color}`), color: cssVarV2(`text/highlight/fg/${color}`),
} satisfies AffineTextAttributes, } satisfies AffineTextStyleAttributes,
}; };
std.command std.command
.chain() .chain()
@@ -961,7 +961,7 @@ const highlightToolPanel: KeyboardToolPanelConfig = {
const payload = { const payload = {
styles: { styles: {
background: cssVarV2(`text/highlight/bg/${color}`), background: cssVarV2(`text/highlight/bg/${color}`),
} satisfies AffineTextAttributes, } satisfies AffineTextStyleAttributes,
}; };
std.command std.command
.chain() .chain()