mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
This PR performs a significant architectural refactoring by extracting rich text functionality into a dedicated package. Here are the key changes: 1. **New Package Creation** - Created a new package `@blocksuite/affine-rich-text` to house rich text related functionality - Moved rich text components, utilities, and types from `@blocksuite/affine-components` to this new package 2. **Dependency Updates** - Updated multiple block packages to include the new `@blocksuite/affine-rich-text` as a direct dependency: - block-callout - block-code - block-database - block-edgeless-text - block-embed - block-list - block-note - block-paragraph 3. **Import Path Updates** - Refactored all imports that previously referenced rich text functionality from `@blocksuite/affine-components/rich-text` to now use `@blocksuite/affine-rich-text` - Updated imports for components like: - DefaultInlineManagerExtension - RichText types and interfaces - Text manipulation utilities (focusTextModel, textKeymap, etc.) - Reference node components and providers 4. **Build Configuration Updates** - Added references to the new rich text package in the `tsconfig.json` files of all affected packages - Maintained workspace dependencies using the `workspace:*` version specifier The primary motivation appears to be: 1. Better separation of concerns by isolating rich text functionality 2. Improved maintainability through more modular package structure 3. Clearer dependencies between packages 4. Potential for better tree-shaking and bundle optimization This is primarily an architectural improvement that should make the codebase more maintainable and better organized.
1104 lines
29 KiB
TypeScript
1104 lines
29 KiB
TypeScript
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
|
|
import { insertDatabaseBlockCommand } from '@blocksuite/affine-block-database';
|
|
import { insertImagesCommand } from '@blocksuite/affine-block-image';
|
|
import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex';
|
|
import {
|
|
canDedentListCommand,
|
|
canIndentListCommand,
|
|
dedentListCommand,
|
|
indentListCommand,
|
|
} from '@blocksuite/affine-block-list';
|
|
import { updateBlockType } from '@blocksuite/affine-block-note';
|
|
import {
|
|
canDedentParagraphCommand,
|
|
canIndentParagraphCommand,
|
|
dedentParagraphCommand,
|
|
indentParagraphCommand,
|
|
} from '@blocksuite/affine-block-paragraph';
|
|
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
|
|
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
|
|
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
|
|
import { toast } from '@blocksuite/affine-components/toast';
|
|
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
|
import {
|
|
formatBlockCommand,
|
|
formatNativeCommand,
|
|
formatTextCommand,
|
|
getInlineEditorByModel,
|
|
getTextStyle,
|
|
insertContent,
|
|
insertInlineLatex,
|
|
toggleBold,
|
|
toggleCode,
|
|
toggleItalic,
|
|
toggleLink,
|
|
toggleStrike,
|
|
toggleUnderline,
|
|
} from '@blocksuite/affine-rich-text';
|
|
import {
|
|
copySelectedModelsCommand,
|
|
deleteSelectedModelsCommand,
|
|
draftSelectedModelsCommand,
|
|
duplicateSelectedModelsCommand,
|
|
getBlockSelectionsCommand,
|
|
getSelectedModelsCommand,
|
|
getTextSelectionCommand,
|
|
} from '@blocksuite/affine-shared/commands';
|
|
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
|
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
|
|
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
|
import {
|
|
createDefaultDoc,
|
|
openFileOrFiles,
|
|
type Signal,
|
|
} from '@blocksuite/affine-shared/utils';
|
|
import type { BlockStdScope } from '@blocksuite/block-std';
|
|
import { viewPresets } from '@blocksuite/data-view/view-presets';
|
|
import { assertType } from '@blocksuite/global/utils';
|
|
import {
|
|
AttachmentIcon,
|
|
BoldIcon,
|
|
BulletedListIcon,
|
|
CheckBoxCheckLinearIcon,
|
|
CloseIcon,
|
|
CodeBlockIcon,
|
|
CodeIcon,
|
|
CollapseTabIcon,
|
|
CopyIcon,
|
|
DatabaseKanbanViewIcon,
|
|
DatabaseTableViewIcon,
|
|
DeleteIcon,
|
|
DividerIcon,
|
|
DuplicateIcon,
|
|
FontIcon,
|
|
FrameIcon,
|
|
GithubIcon,
|
|
GroupIcon,
|
|
ImageIcon,
|
|
ItalicIcon,
|
|
LinkedPageIcon,
|
|
LinkIcon,
|
|
LoomLogoIcon,
|
|
NewPageIcon,
|
|
NowIcon,
|
|
NumberedListIcon,
|
|
PlusIcon,
|
|
QuoteIcon,
|
|
RedoIcon,
|
|
RightTabIcon,
|
|
StrikeThroughIcon,
|
|
TeXIcon,
|
|
TextIcon,
|
|
TodayIcon,
|
|
TomorrowIcon,
|
|
UnderLineIcon,
|
|
UndoIcon,
|
|
YesterdayIcon,
|
|
YoutubeDuotoneIcon,
|
|
} from '@blocksuite/icons/lit';
|
|
import { computed } from '@preact/signals-core';
|
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
import type { TemplateResult } from 'lit';
|
|
|
|
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
|
|
import { formatDate, formatTime } from '../../utils/misc.js';
|
|
import type { AffineLinkedDocWidget } from '../linked-doc/index.js';
|
|
import {
|
|
FigmaDuotoneIcon,
|
|
HeadingIcon,
|
|
HighLightDuotoneIcon,
|
|
TextBackgroundDuotoneIcon,
|
|
TextColorIcon,
|
|
} from './icons.js';
|
|
|
|
export type KeyboardToolbarConfig = {
|
|
items: KeyboardToolbarItem[];
|
|
};
|
|
|
|
export type KeyboardToolbarItem =
|
|
| KeyboardToolbarActionItem
|
|
| KeyboardSubToolbarConfig
|
|
| KeyboardToolPanelConfig;
|
|
|
|
export type KeyboardIconType =
|
|
| TemplateResult
|
|
| ((ctx: KeyboardToolbarContext) => TemplateResult);
|
|
|
|
export type KeyboardToolbarActionItem = {
|
|
name: string;
|
|
icon: KeyboardIconType;
|
|
background?: string | ((ctx: KeyboardToolbarContext) => string | undefined);
|
|
/**
|
|
* @default true
|
|
* @description Whether to show the item in the toolbar.
|
|
*/
|
|
showWhen?: (ctx: KeyboardToolbarContext) => boolean;
|
|
/**
|
|
* @default false
|
|
* @description Whether to set the item as disabled status.
|
|
*/
|
|
disableWhen?: (ctx: KeyboardToolbarContext) => boolean;
|
|
/**
|
|
* @description The action to be executed when the item is clicked.
|
|
*/
|
|
action?: (ctx: KeyboardToolbarContext) => void | Promise<void>;
|
|
};
|
|
|
|
export type KeyboardSubToolbarConfig = {
|
|
icon: KeyboardIconType;
|
|
items: KeyboardToolbarItem[];
|
|
/**
|
|
* It will enter this sub-toolbar when the condition is met.
|
|
*/
|
|
autoShow?: (ctx: KeyboardToolbarContext) => Signal<boolean>;
|
|
};
|
|
|
|
export type KeyboardToolbarContext = {
|
|
std: BlockStdScope;
|
|
rootComponent: PageRootBlockComponent;
|
|
/**
|
|
* Close tool bar, and blur the focus if blur is true, default is false
|
|
*/
|
|
closeToolbar: (blur?: boolean) => void;
|
|
/**
|
|
* Close current tool panel and show virtual keyboard
|
|
*/
|
|
closeToolPanel: () => void;
|
|
};
|
|
|
|
export type KeyboardToolPanelConfig = {
|
|
icon: KeyboardIconType;
|
|
activeIcon?: KeyboardIconType;
|
|
activeBackground?: string;
|
|
groups: (KeyboardToolPanelGroup | DynamicKeyboardToolPanelGroup)[];
|
|
};
|
|
|
|
export type KeyboardToolPanelGroup = {
|
|
name: string;
|
|
items: KeyboardToolbarActionItem[];
|
|
};
|
|
|
|
export type DynamicKeyboardToolPanelGroup = (
|
|
ctx: KeyboardToolbarContext
|
|
) => KeyboardToolPanelGroup | null;
|
|
|
|
const textToolActionItems: KeyboardToolbarActionItem[] = [
|
|
{
|
|
name: 'Text',
|
|
icon: TextIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:paragraph'),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:paragraph',
|
|
props: { type: 'text' },
|
|
});
|
|
},
|
|
},
|
|
...([1, 2, 3, 4, 5, 6] as const).map(i => ({
|
|
name: `Heading ${i}`,
|
|
icon: HeadingIcon(i),
|
|
showWhen: ({ std }: KeyboardToolbarContext) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:paragraph'),
|
|
action: ({ std }: KeyboardToolbarContext) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:paragraph',
|
|
props: { type: `h${i}` },
|
|
});
|
|
},
|
|
})),
|
|
{
|
|
name: 'CodeBlock',
|
|
showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:code'),
|
|
icon: CodeBlockIcon(),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:code',
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Quote',
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:paragraph'),
|
|
icon: QuoteIcon(),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:paragraph',
|
|
props: { type: 'quote' },
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Divider',
|
|
icon: DividerIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:divider'),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:divider',
|
|
props: { type: 'divider' },
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Inline equation',
|
|
icon: TeXIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:paragraph'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getTextSelectionCommand)
|
|
.pipe(insertInlineLatex)
|
|
.run();
|
|
},
|
|
},
|
|
];
|
|
|
|
const listToolActionItems: KeyboardToolbarActionItem[] = [
|
|
{
|
|
name: 'BulletedList',
|
|
icon: BulletedListIcon(),
|
|
showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:list'),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:list',
|
|
props: {
|
|
type: 'bulleted',
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'NumberedList',
|
|
icon: NumberedListIcon(),
|
|
showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:list'),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:list',
|
|
props: {
|
|
type: 'numbered',
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'CheckBox',
|
|
icon: CheckBoxCheckLinearIcon(),
|
|
showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:list'),
|
|
action: ({ std }) => {
|
|
std.command.exec(updateBlockType, {
|
|
flavour: 'affine:list',
|
|
props: {
|
|
type: 'todo',
|
|
},
|
|
});
|
|
},
|
|
},
|
|
];
|
|
|
|
const pageToolGroup: KeyboardToolPanelGroup = {
|
|
name: 'Page',
|
|
items: [
|
|
{
|
|
name: 'NewPage',
|
|
icon: NewPageIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:embed-linked-doc'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(({ selectedModels }) => {
|
|
const newDoc = createDefaultDoc(std.store.workspace);
|
|
if (!selectedModels?.length) return;
|
|
insertContent(std.host, selectedModels[0], REFERENCE_NODE, {
|
|
reference: {
|
|
type: 'LinkedPage',
|
|
pageId: newDoc.id,
|
|
},
|
|
});
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'LinkedPage',
|
|
icon: LinkedPageIcon(),
|
|
showWhen: ({ std, rootComponent }) => {
|
|
const linkedDocWidget = std.view.getWidget(
|
|
'affine-linked-doc-widget',
|
|
rootComponent.model.id
|
|
);
|
|
if (!linkedDocWidget) return false;
|
|
|
|
return std.store.schema.flavourSchemaMap.has('affine:embed-linked-doc');
|
|
},
|
|
action: ({ rootComponent, closeToolPanel }) => {
|
|
const { std } = rootComponent;
|
|
|
|
const linkedDocWidget = std.view.getWidget(
|
|
'affine-linked-doc-widget',
|
|
rootComponent.model.id
|
|
);
|
|
if (!linkedDocWidget) return;
|
|
assertType<AffineLinkedDocWidget>(linkedDocWidget);
|
|
|
|
const triggerKey = linkedDocWidget.config.triggerKeys[0];
|
|
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(ctx => {
|
|
const { selectedModels } = ctx;
|
|
if (!selectedModels?.length) return;
|
|
|
|
const currentModel = selectedModels[0];
|
|
insertContent(std.host, currentModel, triggerKey);
|
|
|
|
const inlineEditor = getInlineEditorByModel(std.host, currentModel);
|
|
// Wait for range to be updated
|
|
inlineEditor?.slots.inlineRangeSync.once(() => {
|
|
linkedDocWidget.show({
|
|
mode: 'mobile',
|
|
addTriggerKey: true,
|
|
});
|
|
closeToolPanel();
|
|
});
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const contentMediaToolGroup: KeyboardToolPanelGroup = {
|
|
name: 'Content & Media',
|
|
items: [
|
|
{
|
|
name: 'Image',
|
|
icon: ImageIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:image'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertImagesCommand, { removeEmptyLine: true })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Link',
|
|
icon: LinkIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:bookmark'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const parentModel = std.store.getParent(model);
|
|
if (!parentModel) return;
|
|
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
std.host,
|
|
'Links',
|
|
'The added link will be displayed as a card view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Attachment',
|
|
icon: AttachmentIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:attachment'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const file = await openFileOrFiles();
|
|
if (!file) return;
|
|
|
|
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
|
|
|
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Youtube',
|
|
icon: YoutubeDuotoneIcon({
|
|
style: `color: white`,
|
|
}),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:embed-youtube'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const parentModel = std.store.getParent(model);
|
|
if (!parentModel) return;
|
|
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
std.host,
|
|
'YouTube',
|
|
'The added YouTube video link will be displayed as an embed view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Github',
|
|
icon: GithubIcon({ style: `color: black` }),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:embed-github'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const parentModel = std.store.getParent(model);
|
|
if (!parentModel) return;
|
|
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
std.host,
|
|
'GitHub',
|
|
'The added GitHub issue or pull request link will be displayed as a card view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Figma',
|
|
icon: FigmaDuotoneIcon,
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:embed-figma'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const parentModel = std.store.getParent(model);
|
|
if (!parentModel) {
|
|
return;
|
|
}
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
std.host,
|
|
'Figma',
|
|
'The added Figma link will be displayed as an embed view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Loom',
|
|
icon: LoomLogoIcon({ style: `color: #625DF5` }),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:embed-loom'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const parentModel = std.store.getParent(model);
|
|
if (!parentModel) return;
|
|
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
std.host,
|
|
'Loom',
|
|
'The added Loom video link will be displayed as an embed view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Equation',
|
|
icon: TeXIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:latex'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertLatexBlockCommand, {
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const documentGroupFrameToolGroup: DynamicKeyboardToolPanelGroup = ({
|
|
std,
|
|
}) => {
|
|
const { store } = std;
|
|
|
|
const frameModels = store
|
|
.getBlocksByFlavour('affine:frame')
|
|
.map(block => block.model) as FrameBlockModel[];
|
|
|
|
const frameItems = frameModels.map<KeyboardToolbarActionItem>(frameModel => ({
|
|
name: 'Frame: ' + frameModel.title.toString(),
|
|
icon: FrameIcon(),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertSurfaceRefBlockCommand, {
|
|
reference: frameModel.id,
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.run();
|
|
},
|
|
}));
|
|
|
|
const surfaceModel = getSurfaceBlock(store);
|
|
|
|
const groupElements = surfaceModel
|
|
? surfaceModel.getElementsByType('group')
|
|
: [];
|
|
|
|
const groupItems = groupElements.map<KeyboardToolbarActionItem>(group => ({
|
|
name: 'Group: ' + group.title.toString(),
|
|
icon: GroupIcon(),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertSurfaceRefBlockCommand, {
|
|
reference: group.id,
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.run();
|
|
},
|
|
}));
|
|
|
|
const items = [...frameItems, ...groupItems];
|
|
|
|
if (items.length === 0) return null;
|
|
|
|
return {
|
|
name: 'Document Group&Frame',
|
|
items,
|
|
};
|
|
};
|
|
|
|
const dateToolGroup: KeyboardToolPanelGroup = {
|
|
name: 'Date',
|
|
items: [
|
|
{
|
|
name: 'Today',
|
|
icon: TodayIcon(),
|
|
action: ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
insertContent(std.host, model, formatDate(new Date()));
|
|
},
|
|
},
|
|
{
|
|
name: 'Tomorrow',
|
|
icon: TomorrowIcon(),
|
|
action: ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const tomorrow = new Date();
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
insertContent(std.host, model, formatDate(tomorrow));
|
|
},
|
|
},
|
|
{
|
|
name: 'Yesterday',
|
|
icon: YesterdayIcon(),
|
|
action: ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
insertContent(std.host, model, formatDate(yesterday));
|
|
},
|
|
},
|
|
{
|
|
name: 'Now',
|
|
icon: NowIcon(),
|
|
action: ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
insertContent(std.host, model, formatTime(new Date()));
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const databaseToolGroup: KeyboardToolPanelGroup = {
|
|
name: 'Database',
|
|
items: [
|
|
{
|
|
name: 'Table view',
|
|
icon: DatabaseTableViewIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:database'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertDatabaseBlockCommand, {
|
|
viewType: viewPresets.tableViewMeta.type,
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Kanban view',
|
|
icon: DatabaseKanbanViewIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:database'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertDatabaseBlockCommand, {
|
|
viewType: viewPresets.kanbanViewMeta.type,
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const moreToolPanel: KeyboardToolPanelConfig = {
|
|
icon: PlusIcon(),
|
|
activeIcon: CloseIcon({
|
|
style: `color: ${cssVarV2('icon/activated')}`,
|
|
}),
|
|
activeBackground: cssVarV2('edgeless/selection/selectionMarqueeBackground'),
|
|
groups: [
|
|
{ name: 'Basic', items: textToolActionItems },
|
|
{ name: 'List', items: listToolActionItems },
|
|
pageToolGroup,
|
|
contentMediaToolGroup,
|
|
documentGroupFrameToolGroup,
|
|
dateToolGroup,
|
|
databaseToolGroup,
|
|
],
|
|
};
|
|
|
|
const textToolPanel: KeyboardToolPanelConfig = {
|
|
icon: TextIcon(),
|
|
groups: [
|
|
{
|
|
name: 'Turn into',
|
|
items: textToolActionItems,
|
|
},
|
|
],
|
|
};
|
|
|
|
const textStyleToolItems: KeyboardToolbarItem[] = [
|
|
{
|
|
name: 'Bold',
|
|
icon: BoldIcon(),
|
|
background: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
return textStyle?.bold ? '#00000012' : '';
|
|
},
|
|
action: ({ std }) => {
|
|
std.command.exec(toggleBold);
|
|
},
|
|
},
|
|
{
|
|
name: 'Italic',
|
|
icon: ItalicIcon(),
|
|
background: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
return textStyle?.italic ? '#00000012' : '';
|
|
},
|
|
action: ({ std }) => {
|
|
std.command.exec(toggleItalic);
|
|
},
|
|
},
|
|
{
|
|
name: 'UnderLine',
|
|
icon: UnderLineIcon(),
|
|
background: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
return textStyle?.underline ? '#00000012' : '';
|
|
},
|
|
action: ({ std }) => {
|
|
std.command.exec(toggleUnderline);
|
|
},
|
|
},
|
|
{
|
|
name: 'StrikeThrough',
|
|
icon: StrikeThroughIcon(),
|
|
background: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
return textStyle?.strike ? '#00000012' : '';
|
|
},
|
|
action: ({ std }) => {
|
|
std.command.exec(toggleStrike);
|
|
},
|
|
},
|
|
{
|
|
name: 'Code',
|
|
icon: CodeIcon(),
|
|
background: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
return textStyle?.code ? '#00000012' : '';
|
|
},
|
|
action: ({ std }) => {
|
|
std.command.exec(toggleCode);
|
|
},
|
|
},
|
|
{
|
|
name: 'Link',
|
|
icon: LinkIcon(),
|
|
background: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
return textStyle?.link ? '#00000012' : '';
|
|
},
|
|
action: ({ std }) => {
|
|
std.command.exec(toggleLink);
|
|
},
|
|
},
|
|
];
|
|
|
|
const highlightToolPanel: KeyboardToolPanelConfig = {
|
|
icon: ({ std }) => {
|
|
const [_, { textStyle }] = std.command.exec(getTextStyle);
|
|
if (textStyle?.color) {
|
|
return HighLightDuotoneIcon(textStyle.color);
|
|
} else {
|
|
return HighLightDuotoneIcon(cssVarV2('icon/primary'));
|
|
}
|
|
},
|
|
groups: [
|
|
{
|
|
name: 'Color',
|
|
items: [
|
|
{
|
|
name: 'Default Color',
|
|
icon: TextColorIcon(cssVarV2('text/highlight/fg/orange')),
|
|
},
|
|
...(
|
|
[
|
|
'red',
|
|
'orange',
|
|
'yellow',
|
|
'green',
|
|
'teal',
|
|
'blue',
|
|
'purple',
|
|
'grey',
|
|
] as const
|
|
).map<KeyboardToolbarActionItem>(color => ({
|
|
name: color.charAt(0).toUpperCase() + color.slice(1),
|
|
icon: TextColorIcon(cssVarV2(`text/highlight/fg/${color}`)),
|
|
action: ({ std }) => {
|
|
const payload = {
|
|
styles: {
|
|
color: cssVarV2(`text/highlight/fg/${color}`),
|
|
} satisfies AffineTextAttributes,
|
|
};
|
|
std.command
|
|
.chain()
|
|
.try(chain => [
|
|
chain
|
|
.pipe(getTextSelectionCommand)
|
|
.pipe(formatTextCommand, payload),
|
|
chain
|
|
.pipe(getBlockSelectionsCommand)
|
|
.pipe(formatBlockCommand, payload),
|
|
chain.pipe(formatNativeCommand, payload),
|
|
])
|
|
.run();
|
|
},
|
|
})),
|
|
],
|
|
},
|
|
{
|
|
name: 'Background',
|
|
items: [
|
|
{
|
|
name: 'Default Color',
|
|
icon: TextBackgroundDuotoneIcon(cssVarV2('text/highlight/bg/orange')),
|
|
},
|
|
...(
|
|
[
|
|
'red',
|
|
'orange',
|
|
'yellow',
|
|
'green',
|
|
'teal',
|
|
'blue',
|
|
'purple',
|
|
'grey',
|
|
] as const
|
|
).map<KeyboardToolbarActionItem>(color => ({
|
|
name: color.charAt(0).toUpperCase() + color.slice(1),
|
|
icon: TextBackgroundDuotoneIcon(
|
|
cssVarV2(`text/highlight/bg/${color}`)
|
|
),
|
|
action: ({ std }) => {
|
|
const payload = {
|
|
styles: {
|
|
background: cssVarV2(`text/highlight/bg/${color}`),
|
|
} satisfies AffineTextAttributes,
|
|
};
|
|
std.command
|
|
.chain()
|
|
.try(chain => [
|
|
chain
|
|
.pipe(getTextSelectionCommand)
|
|
.pipe(formatTextCommand, payload),
|
|
chain
|
|
.pipe(getBlockSelectionsCommand)
|
|
.pipe(formatBlockCommand, payload),
|
|
chain.pipe(formatNativeCommand, payload),
|
|
])
|
|
.run();
|
|
},
|
|
})),
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const textSubToolbarConfig: KeyboardSubToolbarConfig = {
|
|
icon: FontIcon(),
|
|
items: [
|
|
textToolPanel,
|
|
...textStyleToolItems,
|
|
{
|
|
name: 'InlineTex',
|
|
icon: TeXIcon(),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getTextSelectionCommand)
|
|
.pipe(insertInlineLatex)
|
|
.run();
|
|
},
|
|
},
|
|
highlightToolPanel,
|
|
],
|
|
autoShow: ({ std }) => {
|
|
return computed(() => {
|
|
const [_, { currentTextSelection: selection }] = std.command.exec(
|
|
getTextSelectionCommand
|
|
);
|
|
return selection ? !selection.isCollapsed() : false;
|
|
});
|
|
},
|
|
};
|
|
|
|
export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
|
|
items: [
|
|
moreToolPanel,
|
|
// TODO(@L-Sun): add ai function in AFFiNE side
|
|
// { icon: AiIcon(iconStyle) },
|
|
textSubToolbarConfig,
|
|
{
|
|
name: 'Image',
|
|
icon: ImageIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:image'),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertImagesCommand, { removeEmptyLine: true })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Attachment',
|
|
icon: AttachmentIcon(),
|
|
showWhen: ({ std }) =>
|
|
std.store.schema.flavourSchemaMap.has('affine:attachment'),
|
|
action: async ({ std }) => {
|
|
const [_, { selectedModels }] = std.command.exec(
|
|
getSelectedModelsCommand
|
|
);
|
|
const model = selectedModels?.[0];
|
|
if (!model) return;
|
|
|
|
const file = await openFileOrFiles();
|
|
if (!file) return;
|
|
|
|
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
|
|
|
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
|
|
if (model.text?.length === 0) {
|
|
std.store.deleteBlock(model);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: 'Undo',
|
|
icon: UndoIcon(),
|
|
disableWhen: ({ std }) => !std.store.canUndo,
|
|
action: ({ std }) => {
|
|
std.store.undo();
|
|
},
|
|
},
|
|
{
|
|
name: 'Redo',
|
|
icon: RedoIcon(),
|
|
disableWhen: ({ std }) => !std.store.canRedo,
|
|
action: ({ std }) => {
|
|
std.store.redo();
|
|
},
|
|
},
|
|
{
|
|
name: 'RightTab',
|
|
icon: RightTabIcon(),
|
|
disableWhen: ({ std }) => {
|
|
const [success] = std.command
|
|
.chain()
|
|
.tryAll(chain => [
|
|
chain.pipe(canIndentParagraphCommand),
|
|
chain.pipe(canIndentListCommand),
|
|
])
|
|
.run();
|
|
return !success;
|
|
},
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.tryAll(chain => [
|
|
chain.pipe(canIndentParagraphCommand).pipe(indentParagraphCommand),
|
|
chain.pipe(canIndentListCommand).pipe(indentListCommand),
|
|
])
|
|
.run();
|
|
},
|
|
},
|
|
...listToolActionItems,
|
|
...textToolActionItems.filter(({ name }) => name === 'Divider'),
|
|
{
|
|
name: 'CollapseTab',
|
|
icon: CollapseTabIcon(),
|
|
disableWhen: ({ std }) => {
|
|
const [success] = std.command
|
|
.chain()
|
|
.tryAll(chain => [
|
|
chain.pipe(canDedentParagraphCommand),
|
|
chain.pipe(canDedentListCommand),
|
|
])
|
|
.run();
|
|
return !success;
|
|
},
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.tryAll(chain => [
|
|
chain.pipe(canDedentParagraphCommand).pipe(dedentParagraphCommand),
|
|
chain.pipe(canDedentListCommand).pipe(dedentListCommand),
|
|
])
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Copy',
|
|
icon: CopyIcon(),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.with({
|
|
onCopy: () => {
|
|
toast(std.host, 'Copied to clipboard');
|
|
},
|
|
})
|
|
.pipe(draftSelectedModelsCommand)
|
|
.pipe(copySelectedModelsCommand)
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Duplicate',
|
|
icon: DuplicateIcon(),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(draftSelectedModelsCommand)
|
|
.pipe(duplicateSelectedModelsCommand)
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Delete',
|
|
icon: DeleteIcon(),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(deleteSelectedModelsCommand)
|
|
.run();
|
|
},
|
|
},
|
|
],
|
|
};
|