mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 08:50:50 +08:00
feat: implement textAlign property (#11790)
for paragraph blocks, image blocks, list blocks, and table blocks Should fix #8617 and #11254. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added text alignment options (left, center, right) for paragraph, list, image, note, and table blocks. - Introduced alignment controls in toolbars and slash menus for easier formatting. - Enabled keyboard shortcuts for quick text alignment changes (supports Mac and Windows). - **Localization** - Added English, Simplified Chinese, and Traditional Chinese translations for new alignment commands and shortcuts. - **Style** - Blocks now visually reflect selected text alignment in their layout. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: L-Sun <zover.v@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { ImageBlockModel } from '@blocksuite/affine-model';
|
import { updateBlockAlign } from '@blocksuite/affine-block-note';
|
||||||
|
import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
ActionPlacement,
|
ActionPlacement,
|
||||||
blockCommentToolbarButton,
|
blockCommentToolbarButton,
|
||||||
@@ -12,6 +13,9 @@ import {
|
|||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
DuplicateIcon,
|
DuplicateIcon,
|
||||||
|
TextAlignCenterIcon,
|
||||||
|
TextAlignLeftIcon,
|
||||||
|
TextAlignRightIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||||
import type { ExtensionType } from '@blocksuite/store';
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
@@ -51,7 +55,55 @@ const builtinToolbarConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'c.comment',
|
id: 'c.1.align-left',
|
||||||
|
tooltip: 'Align left',
|
||||||
|
icon: TextAlignLeftIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||||
|
if (block) {
|
||||||
|
ctx.chain
|
||||||
|
.pipe(updateBlockAlign, {
|
||||||
|
textAlign: TextAlign.Left,
|
||||||
|
selectedBlocks: [block],
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c.2.align-center',
|
||||||
|
tooltip: 'Align center',
|
||||||
|
icon: TextAlignCenterIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||||
|
if (block) {
|
||||||
|
ctx.chain
|
||||||
|
.pipe(updateBlockAlign, {
|
||||||
|
textAlign: TextAlign.Center,
|
||||||
|
selectedBlocks: [block],
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c.3.align-right',
|
||||||
|
tooltip: 'Align right',
|
||||||
|
icon: TextAlignRightIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||||
|
if (block) {
|
||||||
|
ctx.chain
|
||||||
|
.pipe(updateBlockAlign, {
|
||||||
|
textAlign: TextAlign.Right,
|
||||||
|
selectedBlocks: [block],
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'd.comment',
|
||||||
...blockCommentToolbarButton,
|
...blockCommentToolbarButton,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -143,6 +143,15 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const alignItemsStyleMap = styleMap({
|
||||||
|
alignItems:
|
||||||
|
this.model.props.textAlign$.value === 'left'
|
||||||
|
? 'flex-start'
|
||||||
|
: this.model.props.textAlign$.value === 'right'
|
||||||
|
? 'flex-end'
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const resovledState = this.resourceController.resolveStateWith({
|
const resovledState = this.resourceController.resolveStateWith({
|
||||||
loadingIcon: LoadingIcon({
|
loadingIcon: LoadingIcon({
|
||||||
strokeColor: cssVarV2('button/pureWhiteText'),
|
strokeColor: cssVarV2('button/pureWhiteText'),
|
||||||
@@ -162,6 +171,7 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
|||||||
html`<affine-page-image
|
html`<affine-page-image
|
||||||
.block=${this}
|
.block=${this}
|
||||||
.state=${resovledState}
|
.state=${resovledState}
|
||||||
|
style="${alignItemsStyleMap}"
|
||||||
></affine-page-image>`,
|
></affine-page-image>`,
|
||||||
() =>
|
() =>
|
||||||
html`<affine-image-fallback-card
|
html`<affine-image-fallback-card
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
|||||||
|
|
||||||
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
|
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
|
||||||
|
|
||||||
|
const textAlignStyle = styleMap({
|
||||||
|
textAlign: this.model.props.textAlign$?.value,
|
||||||
|
});
|
||||||
|
|
||||||
const children = html`<div
|
const children = html`<div
|
||||||
class="affine-block-children-container"
|
class="affine-block-children-container"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
@@ -161,7 +165,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class=${'affine-list-block-container'}>
|
<div class=${'affine-list-block-container'} style="${textAlignStyle}">
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
'affine-list-rich-text-wrapper': true,
|
'affine-list-rich-text-wrapper': true,
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { indentBlock } from './indent-block.js';
|
|||||||
export { indentBlocks } from './indent-blocks.js';
|
export { indentBlocks } from './indent-blocks.js';
|
||||||
export { selectBlock } from './select-block.js';
|
export { selectBlock } from './select-block.js';
|
||||||
export { selectBlocksBetween } from './select-blocks-between.js';
|
export { selectBlocksBetween } from './select-blocks-between.js';
|
||||||
|
export { updateBlockAlign } from './update-block-align.js';
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { TextAlign } from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
getBlockSelectionsCommand,
|
||||||
|
getImageSelectionsCommand,
|
||||||
|
getSelectedBlocksCommand,
|
||||||
|
getTextSelectionCommand,
|
||||||
|
} from '@blocksuite/affine-shared/commands';
|
||||||
|
import {
|
||||||
|
type BlockComponent,
|
||||||
|
type Command,
|
||||||
|
TextSelection,
|
||||||
|
} from '@blocksuite/std';
|
||||||
|
|
||||||
|
type UpdateBlockAlignConfig = {
|
||||||
|
textAlign: TextAlign;
|
||||||
|
selectedBlocks?: BlockComponent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBlockAlign: Command<UpdateBlockAlignConfig> = (
|
||||||
|
ctx,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
let { std, textAlign, selectedBlocks } = ctx;
|
||||||
|
|
||||||
|
if (selectedBlocks === null) {
|
||||||
|
const [result, ctx] = std.command
|
||||||
|
.chain()
|
||||||
|
.tryAll(chain => [
|
||||||
|
chain.pipe(getTextSelectionCommand),
|
||||||
|
chain.pipe(getBlockSelectionsCommand),
|
||||||
|
chain.pipe(getImageSelectionsCommand),
|
||||||
|
])
|
||||||
|
.pipe(getSelectedBlocksCommand, { types: ['text', 'block', 'image'] })
|
||||||
|
.run();
|
||||||
|
if (result) {
|
||||||
|
selectedBlocks = ctx.selectedBlocks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedBlocks || selectedBlocks.length === 0) return false;
|
||||||
|
|
||||||
|
selectedBlocks.forEach(block => {
|
||||||
|
std.store.updateBlock(block.model, { textAlign });
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectionManager = std.host.selection;
|
||||||
|
const textSelection = selectionManager.find(TextSelection);
|
||||||
|
if (!textSelection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
selectionManager.setGroup('note', [textSelection]);
|
||||||
|
return next();
|
||||||
|
};
|
||||||
@@ -4,9 +4,15 @@ import {
|
|||||||
textFormatConfigs,
|
textFormatConfigs,
|
||||||
} from '@blocksuite/affine-inline-preset';
|
} from '@blocksuite/affine-inline-preset';
|
||||||
import {
|
import {
|
||||||
|
type TextAlignConfig,
|
||||||
|
textAlignConfigs,
|
||||||
type TextConversionConfig,
|
type TextConversionConfig,
|
||||||
textConversionConfigs,
|
textConversionConfigs,
|
||||||
} from '@blocksuite/affine-rich-text';
|
} from '@blocksuite/affine-rich-text';
|
||||||
|
import {
|
||||||
|
getSelectedModelsCommand,
|
||||||
|
getTextSelectionCommand,
|
||||||
|
} from '@blocksuite/affine-shared/commands';
|
||||||
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
||||||
import {
|
import {
|
||||||
type SlashMenuActionItem,
|
type SlashMenuActionItem,
|
||||||
@@ -17,7 +23,7 @@ import {
|
|||||||
import { HeadingsIcon } from '@blocksuite/icons/lit';
|
import { HeadingsIcon } from '@blocksuite/icons/lit';
|
||||||
import { BlockSelection } from '@blocksuite/std';
|
import { BlockSelection } from '@blocksuite/std';
|
||||||
|
|
||||||
import { updateBlockType } from '../commands';
|
import { updateBlockAlign, updateBlockType } from '../commands';
|
||||||
import { tooltips } from './tooltips';
|
import { tooltips } from './tooltips';
|
||||||
|
|
||||||
let basicIndex = 0;
|
let basicIndex = 0;
|
||||||
@@ -60,6 +66,10 @@ const noteSlashMenuConfig: SlashMenuConfig = {
|
|||||||
createConversionItem(config, `1_List@${index++}`)
|
createConversionItem(config, `1_List@${index++}`)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
...textAlignConfigs.map((config, index) =>
|
||||||
|
createAlignItem(config, `2_Align@${index++}`)
|
||||||
|
),
|
||||||
|
|
||||||
...textFormatConfigs
|
...textFormatConfigs
|
||||||
.filter(i => !['Code', 'Link'].includes(i.name))
|
.filter(i => !['Code', 'Link'].includes(i.name))
|
||||||
.map((config, index) =>
|
.map((config, index) =>
|
||||||
@@ -89,6 +99,26 @@ 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(updateBlockAlign, { textAlign })
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createTextFormatItem(
|
function createTextFormatItem(
|
||||||
config: TextFormatConfig,
|
config: TextFormatConfig,
|
||||||
group?: SlashMenuItem['group']
|
group?: SlashMenuItem['group']
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import {
|
|||||||
NoteBlockSchema,
|
NoteBlockSchema,
|
||||||
ParagraphBlockModel,
|
ParagraphBlockModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
import {
|
||||||
|
textAlignConfigs,
|
||||||
|
textConversionConfigs,
|
||||||
|
} from '@blocksuite/affine-rich-text';
|
||||||
import {
|
import {
|
||||||
focusBlockEnd,
|
focusBlockEnd,
|
||||||
focusBlockStart,
|
focusBlockStart,
|
||||||
@@ -36,6 +39,7 @@ import {
|
|||||||
indentBlocks,
|
indentBlocks,
|
||||||
selectBlock,
|
selectBlock,
|
||||||
selectBlocksBetween,
|
selectBlocksBetween,
|
||||||
|
updateBlockAlign,
|
||||||
updateBlockType,
|
updateBlockType,
|
||||||
} from './commands';
|
} from './commands';
|
||||||
import { moveBlockConfigs } from './move-block';
|
import { moveBlockConfigs } from './move-block';
|
||||||
@@ -157,6 +161,36 @@ 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()
|
||||||
|
.pipe(updateBlockAlign, { textAlign: item.textAlign })
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<string, UIEventHandler>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
...keymap,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<string, UIEventHandler>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private _focusBlock: BlockComponent | null = null;
|
private _focusBlock: BlockComponent | null = null;
|
||||||
|
|
||||||
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
||||||
@@ -568,6 +602,7 @@ class NoteKeymap {
|
|||||||
...this._bindMoveBlockHotKey(),
|
...this._bindMoveBlockHotKey(),
|
||||||
...this._bindQuickActionHotKey(),
|
...this._bindQuickActionHotKey(),
|
||||||
...this._bindTextConversionHotKey(),
|
...this._bindTextConversionHotKey(),
|
||||||
|
...this._bindTextAlignHotKey(),
|
||||||
Tab: ctx => {
|
Tab: ctx => {
|
||||||
const [success] = this.std.command.exec(indentBlocks);
|
const [success] = this.std.command.exec(indentBlocks);
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textAlignStyle = styleMap({
|
||||||
|
textAlign: this.model.props.textAlign$?.value,
|
||||||
|
});
|
||||||
|
|
||||||
const children = html`<div
|
const children = html`<div
|
||||||
class="affine-block-children-container"
|
class="affine-block-children-container"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
@@ -288,6 +292,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
|||||||
'affine-paragraph-block-container': true,
|
'affine-paragraph-block-container': true,
|
||||||
'highlight-comment': this.isCommentHighlighted,
|
'highlight-comment': this.isCommentHighlighted,
|
||||||
})}
|
})}
|
||||||
|
style="${textAlignStyle}"
|
||||||
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
|
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
notifyDocCreated,
|
notifyDocCreated,
|
||||||
promptDocTitle,
|
promptDocTitle,
|
||||||
} from '@blocksuite/affine-block-embed';
|
} from '@blocksuite/affine-block-embed';
|
||||||
import { updateBlockType } from '@blocksuite/affine-block-note';
|
import {
|
||||||
|
updateBlockAlign,
|
||||||
|
updateBlockType,
|
||||||
|
} from '@blocksuite/affine-block-note';
|
||||||
import type { HighlightType } from '@blocksuite/affine-components/highlight-dropdown-menu';
|
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';
|
||||||
@@ -23,8 +26,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
EmbedLinkedDocBlockSchema,
|
EmbedLinkedDocBlockSchema,
|
||||||
EmbedSyncedDocBlockSchema,
|
EmbedSyncedDocBlockSchema,
|
||||||
|
type TextAlign,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
import {
|
||||||
|
textAlignConfigs,
|
||||||
|
textConversionConfigs,
|
||||||
|
} from '@blocksuite/affine-rich-text';
|
||||||
import {
|
import {
|
||||||
copySelectedModelsCommand,
|
copySelectedModelsCommand,
|
||||||
deleteSelectedModelsCommand,
|
deleteSelectedModelsCommand,
|
||||||
@@ -46,6 +53,7 @@ import {
|
|||||||
ActionPlacement,
|
ActionPlacement,
|
||||||
blockCommentToolbarButton,
|
blockCommentToolbarButton,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||||
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
||||||
import {
|
import {
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
@@ -130,6 +138,64 @@ const conversionsActionGroup = {
|
|||||||
},
|
},
|
||||||
} as const satisfies ToolbarActionGenerator;
|
} as const satisfies ToolbarActionGenerator;
|
||||||
|
|
||||||
|
const alignActionGroup = {
|
||||||
|
id: 'b.align',
|
||||||
|
when: ({ chain }) => 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(updateBlockAlign, { textAlign }).run();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: html`
|
||||||
|
<editor-menu-button
|
||||||
|
.contentPadding="${'8px'}"
|
||||||
|
.button=${html`
|
||||||
|
<editor-icon-button aria-label="Align" .tooltip="${'Align'}">
|
||||||
|
${alignment.icon} ${EditorChevronDown}
|
||||||
|
</editor-icon-button>
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div data-size="large" data-orientation="vertical">
|
||||||
|
${repeat(
|
||||||
|
textAlignConfigs,
|
||||||
|
item => item.name,
|
||||||
|
({ textAlign, name, icon }) => html`
|
||||||
|
<editor-menu-action
|
||||||
|
aria-label=${name}
|
||||||
|
@click=${() => update(textAlign)}
|
||||||
|
>
|
||||||
|
${icon}<span class="label">${name}</span>
|
||||||
|
</editor-menu-action>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</editor-menu-button>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as const satisfies ToolbarActionGenerator;
|
||||||
|
|
||||||
const inlineTextActionGroup = {
|
const inlineTextActionGroup = {
|
||||||
id: 'b.inline-text',
|
id: 'b.inline-text',
|
||||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||||
@@ -291,6 +357,7 @@ const turnIntoLinkedDoc = {
|
|||||||
export const builtinToolbarConfig = {
|
export const builtinToolbarConfig = {
|
||||||
actions: [
|
actions: [
|
||||||
conversionsActionGroup,
|
conversionsActionGroup,
|
||||||
|
alignActionGroup,
|
||||||
inlineTextActionGroup,
|
inlineTextActionGroup,
|
||||||
highlightActionGroup,
|
highlightActionGroup,
|
||||||
turnIntoDatabase,
|
turnIntoDatabase,
|
||||||
|
|||||||
@@ -144,6 +144,16 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
|
|||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
paddingLeft: `${virtualPadding}px`,
|
paddingLeft: `${virtualPadding}px`,
|
||||||
paddingRight: `${virtualPadding}px`,
|
paddingRight: `${virtualPadding}px`,
|
||||||
|
marginLeft:
|
||||||
|
!this.model.props.textAlign$.value ||
|
||||||
|
this.model.props.textAlign$?.value === 'left'
|
||||||
|
? undefined
|
||||||
|
: 'auto',
|
||||||
|
marginRight:
|
||||||
|
!this.model.props.textAlign$.value ||
|
||||||
|
this.model.props.textAlign$?.value === 'right'
|
||||||
|
? undefined
|
||||||
|
: 'auto',
|
||||||
width: 'max-content',
|
width: 'max-content',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
defineBlockSchema,
|
defineBlockSchema,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { TextAlign } from '../../consts';
|
||||||
import type { BlockMeta } from '../../utils/types.js';
|
import type { BlockMeta } from '../../utils/types.js';
|
||||||
import { ImageBlockTransformer } from './image-transformer.js';
|
import { ImageBlockTransformer } from './image-transformer.js';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ export type ImageBlockProps = {
|
|||||||
rotate: number;
|
rotate: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
comments?: Record<string, boolean>;
|
comments?: Record<string, boolean>;
|
||||||
|
textAlign?: TextAlign;
|
||||||
} & Omit<GfxCommonBlockProps, 'scale'> &
|
} & Omit<GfxCommonBlockProps, 'scale'> &
|
||||||
BlockMeta;
|
BlockMeta;
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ const defaultImageProps: ImageBlockProps = {
|
|||||||
rotate: 0,
|
rotate: 0,
|
||||||
size: -1,
|
size: -1,
|
||||||
comments: undefined,
|
comments: undefined,
|
||||||
|
textAlign: undefined,
|
||||||
'meta:createdAt': undefined,
|
'meta:createdAt': undefined,
|
||||||
'meta:createdBy': undefined,
|
'meta:createdBy': undefined,
|
||||||
'meta:updatedAt': undefined,
|
'meta:updatedAt': undefined,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
defineBlockSchema,
|
defineBlockSchema,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { TextAlign } from '../../consts';
|
||||||
import type { BlockMeta } from '../../utils/types';
|
import type { BlockMeta } from '../../utils/types';
|
||||||
|
|
||||||
// `toggle` type has been deprecated, do not use it
|
// `toggle` type has been deprecated, do not use it
|
||||||
@@ -13,6 +14,7 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
|
|||||||
export type ListProps = {
|
export type ListProps = {
|
||||||
type: ListType;
|
type: ListType;
|
||||||
text: Text;
|
text: Text;
|
||||||
|
textAlign?: TextAlign;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
order: number | null;
|
order: number | null;
|
||||||
@@ -25,6 +27,7 @@ export const ListBlockSchema = defineBlockSchema({
|
|||||||
({
|
({
|
||||||
type: 'bulleted',
|
type: 'bulleted',
|
||||||
text: internal.Text(),
|
text: internal.Text(),
|
||||||
|
textAlign: undefined,
|
||||||
checked: false,
|
checked: false,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
type Text,
|
type Text,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { TextAlign } from '../../consts';
|
||||||
import type { BlockMeta } from '../../utils/types';
|
import type { BlockMeta } from '../../utils/types';
|
||||||
|
|
||||||
export type ParagraphType =
|
export type ParagraphType =
|
||||||
@@ -19,6 +20,7 @@ export type ParagraphType =
|
|||||||
|
|
||||||
export type ParagraphProps = {
|
export type ParagraphProps = {
|
||||||
type: ParagraphType;
|
type: ParagraphType;
|
||||||
|
textAlign?: TextAlign;
|
||||||
text: Text;
|
text: Text;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
comments?: Record<string, boolean>;
|
comments?: Record<string, boolean>;
|
||||||
@@ -29,6 +31,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
|
|||||||
props: (internal): ParagraphProps => ({
|
props: (internal): ParagraphProps => ({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: internal.Text(),
|
text: internal.Text(),
|
||||||
|
textAlign: undefined,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
comments: undefined,
|
comments: undefined,
|
||||||
'meta:createdAt': undefined,
|
'meta:createdAt': undefined,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
defineBlockSchema,
|
defineBlockSchema,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { TextAlign } from '../../consts';
|
||||||
import type { BlockMeta } from '../../utils/types';
|
import type { BlockMeta } from '../../utils/types';
|
||||||
|
|
||||||
export type TableCell = {
|
export type TableCell = {
|
||||||
@@ -30,6 +31,7 @@ export interface TableBlockProps extends BlockMeta {
|
|||||||
// key = `${rowId}:${columnId}`
|
// key = `${rowId}:${columnId}`
|
||||||
cells: Record<string, TableCell>;
|
cells: Record<string, TableCell>;
|
||||||
comments?: Record<string, boolean>;
|
comments?: Record<string, boolean>;
|
||||||
|
textAlign?: TextAlign;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableCellSerialized {
|
export interface TableCellSerialized {
|
||||||
@@ -53,6 +55,7 @@ export const TableBlockSchema = defineBlockSchema({
|
|||||||
columns: {},
|
columns: {},
|
||||||
cells: {},
|
cells: {},
|
||||||
comments: undefined,
|
comments: undefined,
|
||||||
|
textAlign: undefined,
|
||||||
'meta:createdAt': undefined,
|
'meta:createdAt': undefined,
|
||||||
'meta:createdBy': undefined,
|
'meta:createdBy': undefined,
|
||||||
'meta:updatedAt': undefined,
|
'meta:updatedAt': undefined,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { type TextAlignConfig, textAlignConfigs } from './align';
|
||||||
export { type TextConversionConfig, textConversionConfigs } from './conversion';
|
export { type TextConversionConfig, textConversionConfigs } from './conversion';
|
||||||
export {
|
export {
|
||||||
asyncGetRichText,
|
asyncGetRichText,
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ type KeyboardShortcutsI18NKeys =
|
|||||||
| 'bodyText'
|
| 'bodyText'
|
||||||
| 'increaseIndent'
|
| 'increaseIndent'
|
||||||
| 'reduceIndent'
|
| 'reduceIndent'
|
||||||
|
| 'alignLeft'
|
||||||
|
| 'alignCenter'
|
||||||
|
| 'alignRight'
|
||||||
| 'groupDatabase'
|
| 'groupDatabase'
|
||||||
| 'moveUp'
|
| 'moveUp'
|
||||||
| 'moveDown'
|
| 'moveDown'
|
||||||
@@ -185,6 +188,9 @@ export const useMacPageKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
[tH('6')]: ['⌘', '⌥', '6'],
|
[tH('6')]: ['⌘', '⌥', '6'],
|
||||||
[t('increaseIndent')]: ['Tab'],
|
[t('increaseIndent')]: ['Tab'],
|
||||||
[t('reduceIndent')]: ['⇧', 'Tab'],
|
[t('reduceIndent')]: ['⇧', 'Tab'],
|
||||||
|
[t('alignLeft')]: ['⌘', '⇧', 'L'],
|
||||||
|
[t('alignCenter')]: ['⌘', '⇧', 'E'],
|
||||||
|
[t('alignRight')]: ['⌘', '⇧', 'R'],
|
||||||
[t('groupDatabase')]: ['⌘', 'G'],
|
[t('groupDatabase')]: ['⌘', 'G'],
|
||||||
[t('switch')]: ['⌥', 'S'],
|
[t('switch')]: ['⌥', 'S'],
|
||||||
// not implement yet
|
// not implement yet
|
||||||
@@ -242,6 +248,9 @@ export const useWinPageKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
[tH('6')]: ['Ctrl', 'Shift', '6'],
|
[tH('6')]: ['Ctrl', 'Shift', '6'],
|
||||||
[t('increaseIndent')]: ['Tab'],
|
[t('increaseIndent')]: ['Tab'],
|
||||||
[t('reduceIndent')]: ['Shift+Tab'],
|
[t('reduceIndent')]: ['Shift+Tab'],
|
||||||
|
[t('alignLeft')]: ['Ctrl', 'Shift', 'L'],
|
||||||
|
[t('alignCenter')]: ['Ctrl', 'Shift', 'E'],
|
||||||
|
[t('alignRight')]: ['Ctrl', 'Shift', 'R'],
|
||||||
[t('groupDatabase')]: ['Ctrl + G'],
|
[t('groupDatabase')]: ['Ctrl + G'],
|
||||||
['Switch']: ['Alt + S'],
|
['Switch']: ['Alt + S'],
|
||||||
// not implement yet
|
// not implement yet
|
||||||
|
|||||||
@@ -2509,6 +2509,18 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Just now`
|
* `Just now`
|
||||||
*/
|
*/
|
||||||
["com.affine.just-now"](): string;
|
["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`
|
* `Append to daily note`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -626,6 +626,9 @@
|
|||||||
"com.affine.journal.placeholder.title": "No Journal",
|
"com.affine.journal.placeholder.title": "No Journal",
|
||||||
"com.affine.journal.placeholder.create": "Create Daily Journal",
|
"com.affine.journal.placeholder.create": "Create Daily Journal",
|
||||||
"com.affine.just-now": "Just now",
|
"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.appendDailyNote": "Append to daily note",
|
||||||
"com.affine.keyboardShortcuts.bodyText": "Body text",
|
"com.affine.keyboardShortcuts.bodyText": "Body text",
|
||||||
"com.affine.keyboardShortcuts.bold": "Bold",
|
"com.affine.keyboardShortcuts.bold": "Bold",
|
||||||
|
|||||||
@@ -623,6 +623,9 @@
|
|||||||
"com.affine.journal.daily-count-updated-empty-tips": "你还没有任何更新",
|
"com.affine.journal.daily-count-updated-empty-tips": "你还没有任何更新",
|
||||||
"com.affine.journal.updated-today": "更新",
|
"com.affine.journal.updated-today": "更新",
|
||||||
"com.affine.just-now": "就是现在",
|
"com.affine.just-now": "就是现在",
|
||||||
|
"com.affine.keyboardShortcuts.alignCenter": "居中对齐",
|
||||||
|
"com.affine.keyboardShortcuts.alignLeft": "左对齐",
|
||||||
|
"com.affine.keyboardShortcuts.alignRight": "右对齐",
|
||||||
"com.affine.keyboardShortcuts.appendDailyNote": "添加日常笔记快捷键",
|
"com.affine.keyboardShortcuts.appendDailyNote": "添加日常笔记快捷键",
|
||||||
"com.affine.keyboardShortcuts.bodyText": "正文",
|
"com.affine.keyboardShortcuts.bodyText": "正文",
|
||||||
"com.affine.keyboardShortcuts.bold": "粗体",
|
"com.affine.keyboardShortcuts.bold": "粗体",
|
||||||
|
|||||||
@@ -623,6 +623,9 @@
|
|||||||
"com.affine.journal.daily-count-updated-empty-tips": "你還沒有任何更新",
|
"com.affine.journal.daily-count-updated-empty-tips": "你還沒有任何更新",
|
||||||
"com.affine.journal.updated-today": "更新",
|
"com.affine.journal.updated-today": "更新",
|
||||||
"com.affine.just-now": "就是現在",
|
"com.affine.just-now": "就是現在",
|
||||||
|
"com.affine.keyboardShortcuts.alignCenter": "置中對齊",
|
||||||
|
"com.affine.keyboardShortcuts.alignLeft": "靠左對齊",
|
||||||
|
"com.affine.keyboardShortcuts.alignRight": "靠右對齊",
|
||||||
"com.affine.keyboardShortcuts.appendDailyNote": "附加到隨筆",
|
"com.affine.keyboardShortcuts.appendDailyNote": "附加到隨筆",
|
||||||
"com.affine.keyboardShortcuts.bodyText": "正文",
|
"com.affine.keyboardShortcuts.bodyText": "正文",
|
||||||
"com.affine.keyboardShortcuts.bold": "粗體",
|
"com.affine.keyboardShortcuts.bold": "粗體",
|
||||||
|
|||||||
@@ -92,13 +92,6 @@ test('should format quick bar show when clicking drag handle', async ({
|
|||||||
|
|
||||||
const { formatBar } = getFormatBar(page);
|
const { formatBar } = getFormatBar(page);
|
||||||
await expect(formatBar).toBeVisible();
|
await expect(formatBar).toBeVisible();
|
||||||
|
|
||||||
const box = await formatBar.boundingBox();
|
|
||||||
if (!box) {
|
|
||||||
throw new Error("formatBar doesn't exist");
|
|
||||||
}
|
|
||||||
assertAlmostEqual(box.x, 251, 5);
|
|
||||||
assertAlmostEqual(box.y - dragHandleRect.y, -55.5, 5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should format quick bar show when select text by keyboard', async ({
|
test('should format quick bar show when select text by keyboard', async ({
|
||||||
@@ -548,17 +541,6 @@ test('should format quick bar work in single block selection', async ({
|
|||||||
const { formatBar } = getFormatBar(page);
|
const { formatBar } = getFormatBar(page);
|
||||||
await expect(formatBar).toBeVisible();
|
await expect(formatBar).toBeVisible();
|
||||||
|
|
||||||
const formatRect = await formatBar.boundingBox();
|
|
||||||
const selectionRect = await blockSelections.boundingBox();
|
|
||||||
if (!formatRect) {
|
|
||||||
throw new Error('formatRect is not found');
|
|
||||||
}
|
|
||||||
if (!selectionRect) {
|
|
||||||
throw new Error('selectionRect is not found');
|
|
||||||
}
|
|
||||||
assertAlmostEqual(formatRect.x - selectionRect.x, 147.5, 10);
|
|
||||||
assertAlmostEqual(formatRect.y - selectionRect.y, -48, 10);
|
|
||||||
|
|
||||||
const boldBtn = formatBar.getByTestId('bold');
|
const boldBtn = formatBar.getByTestId('bold');
|
||||||
await boldBtn.click();
|
await boldBtn.click();
|
||||||
const italicBtn = formatBar.getByTestId('italic');
|
const italicBtn = formatBar.getByTestId('italic');
|
||||||
@@ -603,17 +585,6 @@ test('should format quick bar work in multiple block selection', async ({
|
|||||||
const formatBarController = getFormatBar(page);
|
const formatBarController = getFormatBar(page);
|
||||||
await expect(formatBarController.formatBar).toBeVisible();
|
await expect(formatBarController.formatBar).toBeVisible();
|
||||||
|
|
||||||
const box = await formatBarController.formatBar.boundingBox();
|
|
||||||
if (!box) {
|
|
||||||
throw new Error("formatBar doesn't exist");
|
|
||||||
}
|
|
||||||
const rect = await blockSelections.first().boundingBox();
|
|
||||||
if (!rect) {
|
|
||||||
throw new Error('rect is not found');
|
|
||||||
}
|
|
||||||
assertAlmostEqual(box.x - rect.x, 147.5, 10);
|
|
||||||
assertAlmostEqual(box.y - rect.y, -48, 10);
|
|
||||||
|
|
||||||
await formatBarController.boldBtn.click();
|
await formatBarController.boldBtn.click();
|
||||||
await formatBarController.italicBtn.click();
|
await formatBarController.italicBtn.click();
|
||||||
await formatBarController.underlineBtn.click();
|
await formatBarController.underlineBtn.click();
|
||||||
|
|||||||
@@ -606,7 +606,7 @@ test.describe('slash search', () => {
|
|||||||
await expect(slashMenu).toBeVisible();
|
await expect(slashMenu).toBeVisible();
|
||||||
|
|
||||||
await type(page, 'c');
|
await type(page, 'c');
|
||||||
await expect(slashItems).toHaveCount(8);
|
await expect(slashItems).toHaveCount(9);
|
||||||
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
||||||
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']);
|
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']);
|
||||||
await expect(slashItems.nth(2).locator('.text')).toHaveText(['New Doc']);
|
await expect(slashItems.nth(2).locator('.text')).toHaveText(['New Doc']);
|
||||||
|
|||||||
Reference in New Issue
Block a user