From bd268044b489d5e0f00ce594f3234f8173cb27f7 Mon Sep 17 00:00:00 2001 From: UNIDY Date: Thu, 17 Apr 2025 21:59:25 +0800 Subject: [PATCH] feat: implement textAlign property for paragraph blocks, image blocks, list blocks, and table blocks --- .../blocks/image/src/configs/toolbar.ts | 44 ++++++++++- .../affine/blocks/image/src/image-block.ts | 15 +++- .../affine/blocks/list/src/list-block.ts | 6 +- .../blocks/note/src/configs/slash-menu.ts | 35 ++++++++ .../affine/blocks/note/src/note-keymap.ts | 49 +++++++++++- .../blocks/paragraph/src/paragraph-block.ts | 5 ++ .../affine/blocks/root/src/configs/toolbar.ts | 79 ++++++++++++++++++- .../affine/blocks/table/src/table-block.ts | 8 ++ .../model/src/blocks/image/image-model.ts | 3 + .../model/src/blocks/list/list-model.ts | 3 + .../src/blocks/paragraph/paragraph-model.ts | 3 + .../model/src/blocks/table/table-model.ts | 3 + blocksuite/affine/rich-text/src/align.ts | 35 ++++++++ blocksuite/affine/rich-text/src/index.ts | 1 + .../components/hooks/affine/use-shortcuts.ts | 9 +++ packages/frontend/i18n/src/i18n.gen.ts | 12 +++ packages/frontend/i18n/src/resources/en.json | 3 + .../frontend/i18n/src/resources/zh-Hans.json | 3 + .../frontend/i18n/src/resources/zh-Hant.json | 3 + 19 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 blocksuite/affine/rich-text/src/align.ts diff --git a/blocksuite/affine/blocks/image/src/configs/toolbar.ts b/blocksuite/affine/blocks/image/src/configs/toolbar.ts index b513cc3fb8..2202f652a7 100644 --- a/blocksuite/affine/blocks/image/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/image/src/configs/toolbar.ts @@ -1,4 +1,4 @@ -import { ImageBlockModel } from '@blocksuite/affine-model'; +import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model'; import { ActionPlacement, type ToolbarModuleConfig, @@ -11,6 +11,9 @@ import { DeleteIcon, DownloadIcon, DuplicateIcon, + TextAlignCenterIcon, + TextAlignLeftIcon, + TextAlignRightIcon, } from '@blocksuite/icons/lit'; import { BlockFlavourIdentifier } from '@blocksuite/std'; import type { ExtensionType } from '@blocksuite/store'; @@ -49,6 +52,45 @@ const builtinToolbarConfig = { }); }, }, + { + id: 'c.1.align-left', + tooltip: 'Align left', + icon: TextAlignLeftIcon(), + run(ctx) { + const block = ctx.getCurrentBlockByType(ImageBlockComponent); + if (block) { + ctx.std.host.doc.updateBlock(block.model, { + textAlign: TextAlign.Left, + }); + } + }, + }, + { + id: 'c.2.align-center', + tooltip: 'Align center', + icon: TextAlignCenterIcon(), + run(ctx) { + const block = ctx.getCurrentBlockByType(ImageBlockComponent); + if (block) { + ctx.std.host.doc.updateBlock(block.model, { + textAlign: TextAlign.Center, + }); + } + }, + }, + { + id: 'c.3.align-right', + tooltip: 'Align right', + icon: TextAlignRightIcon(), + run(ctx) { + const block = ctx.getCurrentBlockByType(ImageBlockComponent); + if (block) { + ctx.std.host.doc.updateBlock(block.model, { + textAlign: TextAlign.Right, + }); + } + }, + }, { placement: ActionPlacement.More, id: 'a.clipboard', diff --git a/blocksuite/affine/blocks/image/src/image-block.ts b/blocksuite/affine/blocks/image/src/image-block.ts index 6148397262..b8b7f54ae7 100644 --- a/blocksuite/affine/blocks/image/src/image-block.ts +++ b/blocksuite/affine/blocks/image/src/image-block.ts @@ -112,6 +112,15 @@ export class ImageBlockComponent extends CaptionedBlockComponent ${when( @@ -122,7 +131,11 @@ export class ImageBlockComponent extends CaptionedBlockComponent`, - () => html`` + () => + html`` )} diff --git a/blocksuite/affine/blocks/list/src/list-block.ts b/blocksuite/affine/blocks/list/src/list-block.ts index d37ac2b0e5..5c9d57ea7c 100644 --- a/blocksuite/affine/blocks/list/src/list-block.ts +++ b/blocksuite/affine/blocks/list/src/list-block.ts @@ -144,6 +144,10 @@ export class ListBlockComponent extends CaptionedBlockComponent const listIcon = getListIcon(model, !collapsed, _onClickIcon); + const textAlignStyle = styleMap({ + textAlign: this.model.props.textAlign$?.value, + }); + const children = html`
`; return html` -
+
+ createAlignItem(config, `2_Align@${index++}`) + ), + ...textFormatConfigs .filter(i => !['Code', 'Link'].includes(i.name)) .map((config, index) => @@ -85,6 +95,31 @@ function createConversionItem( }; } +function createAlignItem( + config: TextAlignConfig, + group?: SlashMenuItem['group'] +): SlashMenuActionItem { + const { textAlign, name, icon } = config; + return { + name, + group, + icon, + action: ({ std }) => { + std.command + .chain() + .pipe(getTextSelectionCommand) + .pipe(getSelectedModelsCommand, { types: ['text'] }) + .pipe((ctx, next) => { + ctx.selectedModels.forEach(model => { + ctx.std.host.doc.updateBlock(model, { textAlign }); + }); + return next(); + }) + .run(); + }, + }; +} + function createTextFormatItem( config: TextFormatConfig, group?: SlashMenuItem['group'] diff --git a/blocksuite/affine/blocks/note/src/note-keymap.ts b/blocksuite/affine/blocks/note/src/note-keymap.ts index 80bf9c477d..cb35507906 100644 --- a/blocksuite/affine/blocks/note/src/note-keymap.ts +++ b/blocksuite/affine/blocks/note/src/note-keymap.ts @@ -5,13 +5,17 @@ import { NoteBlockSchema, ParagraphBlockModel, } from '@blocksuite/affine-model'; -import { textConversionConfigs } from '@blocksuite/affine-rich-text'; +import { + textAlignConfigs, + textConversionConfigs, +} from '@blocksuite/affine-rich-text'; import { focusBlockEnd, focusBlockStart, getBlockSelectionsCommand, getNextBlockCommand, getPrevBlockCommand, + getSelectedModelsCommand, getTextSelectionCommand, } from '@blocksuite/affine-shared/commands'; import { @@ -157,6 +161,48 @@ class NoteKeymap { ); }; + private readonly _bindTextAlignHotKey = () => { + return textAlignConfigs.reduce( + (acc, item) => { + const keymap = item.hotkey!.reduce( + (acc, key) => { + return { + ...acc, + [key]: ctx => { + ctx.get('defaultState').event.preventDefault(); + const [result] = this._std.command + .chain() + .tryAll(chain => [ + chain.pipe(getTextSelectionCommand), + chain.pipe(getBlockSelectionsCommand), + ]) + .pipe(getSelectedModelsCommand, { types: ['text', 'block'] }) + .pipe((ctx, next) => { + ctx.selectedModels.forEach(model => { + ctx.std.host.doc.updateBlock(model, { + textAlign: item.textAlign, + }); + }); + return next(); + }) + .run(); + + return result; + }, + }; + }, + {} as Record + ); + + return { + ...acc, + ...keymap, + }; + }, + {} as Record + ); + }; + private _focusBlock: BlockComponent | null = null; private readonly _getClosestNoteByBlockId = (blockId: string) => { @@ -568,6 +614,7 @@ class NoteKeymap { ...this._bindMoveBlockHotKey(), ...this._bindQuickActionHotKey(), ...this._bindTextConversionHotKey(), + ...this._bindTextAlignHotKey(), Tab: ctx => { const [success] = this.std.command.exec(indentBlocks); diff --git a/blocksuite/affine/blocks/paragraph/src/paragraph-block.ts b/blocksuite/affine/blocks/paragraph/src/paragraph-block.ts index db6e69a166..dfd7214931 100644 --- a/blocksuite/affine/blocks/paragraph/src/paragraph-block.ts +++ b/blocksuite/affine/blocks/paragraph/src/paragraph-block.ts @@ -235,6 +235,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent
isFormatSupported(chain).run()[0], + generate({ chain }) { + const [ok, { selectedModels = [] }] = chain + .tryAll(chain => [ + chain.pipe(getTextSelectionCommand), + chain.pipe(getBlockSelectionsCommand), + ]) + .pipe(getSelectedModelsCommand, { types: ['text', 'block'] }) + .run(); + if (!ok) return null; + + const alignment = + textAlignConfigs.find( + ({ textAlign }) => + textAlign === + getMostCommonValue( + selectedModels.map( + ({ props }) => props as { textAlign?: TextAlign } + ), + 'textAlign' + ) + ) ?? textAlignConfigs[0]; + const update = (textAlign: TextAlign) => { + chain + .pipe( + (ctx, next) => { + ctx.selectedModels.forEach(model => { + ctx.std.host.doc.updateBlock(model, { + textAlign: ctx.textAlign, + }); + }); + return next(); + }, + { selectedModels, textAlign } + ) + .run(); + }; + + return { + content: html` + + ${alignment.icon} ${ArrowDownSmallIcon()} + + `} + > +
+ ${repeat( + textAlignConfigs, + item => item.name, + ({ textAlign, name, icon }) => html` + update(textAlign)} + > + ${icon}${name} + + ` + )} +
+
+ `, + }; + }, +} as const satisfies ToolbarActionGenerator; + const inlineTextActionGroup = { id: 'b.inline-text', when: ({ chain }) => isFormatSupported(chain).run()[0], @@ -269,6 +345,7 @@ const turnIntoLinkedDoc = { export const builtinToolbarConfig = { actions: [ conversionsActionGroup, + alignActionGroup, inlineTextActionGroup, highlightActionGroup, turnIntoDatabase, diff --git a/blocksuite/affine/blocks/table/src/table-block.ts b/blocksuite/affine/blocks/table/src/table-block.ts index ce0f07118e..11fe99220a 100644 --- a/blocksuite/affine/blocks/table/src/table-block.ts +++ b/blocksuite/affine/blocks/table/src/table-block.ts @@ -144,6 +144,14 @@ export class TableBlockComponent extends CaptionedBlockComponent diff --git a/blocksuite/affine/model/src/blocks/image/image-model.ts b/blocksuite/affine/model/src/blocks/image/image-model.ts index a7092b1ad5..e76f0031c8 100644 --- a/blocksuite/affine/model/src/blocks/image/image-model.ts +++ b/blocksuite/affine/model/src/blocks/image/image-model.ts @@ -9,6 +9,7 @@ import { defineBlockSchema, } from '@blocksuite/store'; +import type { TextAlign } from '../../consts'; import type { BlockMeta } from '../../utils/types.js'; import { ImageBlockTransformer } from './image-transformer.js'; @@ -19,6 +20,7 @@ export type ImageBlockProps = { height?: number; rotate: number; size?: number; + textAlign?: TextAlign; } & Omit & BlockMeta; @@ -32,6 +34,7 @@ const defaultImageProps: ImageBlockProps = { lockedBySelf: false, rotate: 0, size: -1, + textAlign: undefined, 'meta:createdAt': undefined, 'meta:createdBy': undefined, 'meta:updatedAt': undefined, diff --git a/blocksuite/affine/model/src/blocks/list/list-model.ts b/blocksuite/affine/model/src/blocks/list/list-model.ts index 70dbbf4657..efcb2f0115 100644 --- a/blocksuite/affine/model/src/blocks/list/list-model.ts +++ b/blocksuite/affine/model/src/blocks/list/list-model.ts @@ -5,6 +5,7 @@ import { defineBlockSchema, } from '@blocksuite/store'; +import type { TextAlign } from '../../consts'; import type { BlockMeta } from '../../utils/types'; // `toggle` type has been deprecated, do not use it @@ -13,6 +14,7 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle'; export type ListProps = { type: ListType; text: Text; + textAlign?: TextAlign; checked: boolean; collapsed: boolean; order: number | null; @@ -24,6 +26,7 @@ export const ListBlockSchema = defineBlockSchema({ ({ type: 'bulleted', text: internal.Text(), + textAlign: undefined, checked: false, collapsed: false, diff --git a/blocksuite/affine/model/src/blocks/paragraph/paragraph-model.ts b/blocksuite/affine/model/src/blocks/paragraph/paragraph-model.ts index f98f55a118..80d03a4bf5 100644 --- a/blocksuite/affine/model/src/blocks/paragraph/paragraph-model.ts +++ b/blocksuite/affine/model/src/blocks/paragraph/paragraph-model.ts @@ -5,6 +5,7 @@ import { type Text, } from '@blocksuite/store'; +import type { TextAlign } from '../../consts'; import type { BlockMeta } from '../../utils/types'; export type ParagraphType = @@ -19,6 +20,7 @@ export type ParagraphType = export type ParagraphProps = { type: ParagraphType; + textAlign?: TextAlign; text: Text; collapsed: boolean; } & BlockMeta; @@ -28,6 +30,7 @@ export const ParagraphBlockSchema = defineBlockSchema({ props: (internal): ParagraphProps => ({ type: 'text', text: internal.Text(), + textAlign: undefined, collapsed: false, 'meta:createdAt': undefined, 'meta:createdBy': undefined, diff --git a/blocksuite/affine/model/src/blocks/table/table-model.ts b/blocksuite/affine/model/src/blocks/table/table-model.ts index 6ee88bfe96..b66a283cbc 100644 --- a/blocksuite/affine/model/src/blocks/table/table-model.ts +++ b/blocksuite/affine/model/src/blocks/table/table-model.ts @@ -5,6 +5,7 @@ import { defineBlockSchema, } from '@blocksuite/store'; +import type { TextAlign } from '../../consts'; import type { BlockMeta } from '../../utils/types'; export type TableCell = { @@ -29,6 +30,7 @@ export interface TableBlockProps extends BlockMeta { columns: Record; // key = `${rowId}:${columnId}` cells: Record; + textAlign?: TextAlign; } export interface TableCellSerialized { @@ -51,6 +53,7 @@ export const TableBlockSchema = defineBlockSchema({ rows: {}, columns: {}, cells: {}, + textAlign: undefined, 'meta:createdAt': undefined, 'meta:createdBy': undefined, 'meta:updatedAt': undefined, diff --git a/blocksuite/affine/rich-text/src/align.ts b/blocksuite/affine/rich-text/src/align.ts new file mode 100644 index 0000000000..bf6647fb81 --- /dev/null +++ b/blocksuite/affine/rich-text/src/align.ts @@ -0,0 +1,35 @@ +import { TextAlign } from '@blocksuite/affine-model'; +import { + TextAlignCenterIcon, + TextAlignLeftIcon, + TextAlignRightIcon, +} from '@blocksuite/icons/lit'; +import type { TemplateResult } from 'lit'; + +export interface TextAlignConfig { + textAlign: TextAlign; + name: string; + hotkey: string[] | null; + icon: TemplateResult<1>; +} + +export const textAlignConfigs: TextAlignConfig[] = [ + { + textAlign: TextAlign.Left, + name: 'Align left', + hotkey: [`Mod-Shift-L`], + icon: TextAlignLeftIcon(), + }, + { + textAlign: TextAlign.Center, + name: 'Align center', + hotkey: [`Mod-Shift-E`], + icon: TextAlignCenterIcon(), + }, + { + textAlign: TextAlign.Right, + name: 'Align right', + hotkey: [`Mod-Shift-R`], + icon: TextAlignRightIcon(), + }, +]; diff --git a/blocksuite/affine/rich-text/src/index.ts b/blocksuite/affine/rich-text/src/index.ts index 7074cd72c5..b382da7b1e 100644 --- a/blocksuite/affine/rich-text/src/index.ts +++ b/blocksuite/affine/rich-text/src/index.ts @@ -1,3 +1,4 @@ +export { type TextAlignConfig, textAlignConfigs } from './align'; export { type TextConversionConfig, textConversionConfigs } from './conversion'; export { asyncGetRichText, diff --git a/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts b/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts index e83b36aa7d..22ec3be027 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts @@ -38,6 +38,9 @@ type KeyboardShortcutsI18NKeys = | 'bodyText' | 'increaseIndent' | 'reduceIndent' + | 'alignLeft' + | 'alignCenter' + | 'alignRight' | 'groupDatabase' | 'moveUp' | 'moveDown' @@ -185,6 +188,9 @@ export const useMacPageKeyboardShortcuts = (): ShortcutMap => { [tH('6')]: ['⌘', '⌥', '6'], [t('increaseIndent')]: ['Tab'], [t('reduceIndent')]: ['⇧', 'Tab'], + [t('alignLeft')]: ['⌘', '⇧', 'L'], + [t('alignCenter')]: ['⌘', '⇧', 'E'], + [t('alignRight')]: ['⌘', '⇧', 'R'], [t('groupDatabase')]: ['⌘', 'G'], [t('switch')]: ['⌥', 'S'], // not implement yet @@ -242,6 +248,9 @@ export const useWinPageKeyboardShortcuts = (): ShortcutMap => { [tH('6')]: ['Ctrl', 'Shift', '6'], [t('increaseIndent')]: ['Tab'], [t('reduceIndent')]: ['Shift+Tab'], + [t('alignLeft')]: ['Ctrl', 'Shift', 'L'], + [t('alignCenter')]: ['Ctrl', 'Shift', 'E'], + [t('alignRight')]: ['Ctrl', 'Shift', 'R'], [t('groupDatabase')]: ['Ctrl + G'], ['Switch']: ['Alt + S'], // not implement yet diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 6c275a1f8f..28a62b095e 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -2401,6 +2401,18 @@ export function useAFFiNEI18N(): { * `Just now` */ ["com.affine.just-now"](): string; + /** + * `Align center` + */ + ["com.affine.keyboardShortcuts.alignCenter"](): string; + /** + * `Align left` + */ + ["com.affine.keyboardShortcuts.alignLeft"](): string; + /** + * `Align right` + */ + ["com.affine.keyboardShortcuts.alignRight"](): string; /** * `Append to daily note` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 19e115cdf7..6c1dbed248 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -600,6 +600,9 @@ "com.affine.journal.daily-count-updated-empty-tips": "You haven't updated anything yet", "com.affine.journal.updated-today": "Updated", "com.affine.just-now": "Just now", + "com.affine.keyboardShortcuts.alignCenter": "Align center", + "com.affine.keyboardShortcuts.alignLeft": "Align left", + "com.affine.keyboardShortcuts.alignRight": "Align right", "com.affine.keyboardShortcuts.appendDailyNote": "Append to daily note", "com.affine.keyboardShortcuts.bodyText": "Body text", "com.affine.keyboardShortcuts.bold": "Bold", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index e1436192ab..5b48d0417b 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -593,6 +593,9 @@ "com.affine.journal.daily-count-updated-empty-tips": "你还没有任何更新", "com.affine.journal.updated-today": "更新", "com.affine.just-now": "就是现在", + "com.affine.keyboardShortcuts.alignCenter": "居中对齐", + "com.affine.keyboardShortcuts.alignLeft": "左对齐", + "com.affine.keyboardShortcuts.alignRight": "右对齐", "com.affine.keyboardShortcuts.appendDailyNote": "添加日常笔记快捷键", "com.affine.keyboardShortcuts.bodyText": "正文", "com.affine.keyboardShortcuts.bold": "粗体", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index cda9563ca4..1f17c6b8a2 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -593,6 +593,9 @@ "com.affine.journal.daily-count-updated-empty-tips": "你還沒有任何更新", "com.affine.journal.updated-today": "更新", "com.affine.just-now": "就是現在", + "com.affine.keyboardShortcuts.alignCenter": "置中對齊", + "com.affine.keyboardShortcuts.alignLeft": "靠左對齊", + "com.affine.keyboardShortcuts.alignRight": "靠右對齊", "com.affine.keyboardShortcuts.appendDailyNote": "附加到隨筆", "com.affine.keyboardShortcuts.bodyText": "正文", "com.affine.keyboardShortcuts.bold": "粗體",