mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
373
blocksuite/affine/blocks/root/src/configs/toolbar.ts
Normal file
373
blocksuite/affine/blocks/root/src/configs/toolbar.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import {
|
||||
convertToDatabase,
|
||||
DATABASE_CONVERT_WHITE_LIST,
|
||||
} from '@blocksuite/affine-block-database';
|
||||
import {
|
||||
convertSelectedBlocksToLinkedDoc,
|
||||
getTitleFromSelectedModels,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { updateBlockType } from '@blocksuite/affine-block-note';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
deleteTextCommand,
|
||||
formatBlockCommand,
|
||||
formatNativeCommand,
|
||||
formatTextCommand,
|
||||
isFormatSupported,
|
||||
textFormatConfigs,
|
||||
} from '@blocksuite/affine-inline-preset';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
copySelectedModelsCommand,
|
||||
deleteSelectedModelsCommand,
|
||||
draftSelectedModelsCommand,
|
||||
duplicateSelectedModelsCommand,
|
||||
getBlockSelectionsCommand,
|
||||
getImageSelectionsCommand,
|
||||
getSelectedBlocksCommand,
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type {
|
||||
ToolbarAction,
|
||||
ToolbarActionGenerator,
|
||||
ToolbarActionGroup,
|
||||
ToolbarModuleConfig,
|
||||
} 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 {
|
||||
ArrowDownSmallIcon,
|
||||
CopyIcon,
|
||||
DatabaseTableViewIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
LinkedPageIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { type BlockComponent, BlockSelection } from '@blocksuite/std';
|
||||
import { toDraftModel } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const conversionsActionGroup = {
|
||||
id: 'a.conversions',
|
||||
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();
|
||||
|
||||
// only support model with text
|
||||
// TODO(@fundon): displays only in a single paragraph, `length === 1`.
|
||||
const allowed = ok && selectedModels.filter(model => model.text).length > 0;
|
||||
if (!allowed) return null;
|
||||
|
||||
const model = selectedModels[0];
|
||||
const conversion =
|
||||
textConversionConfigs.find(
|
||||
({ flavour, type }) =>
|
||||
flavour === model.flavour &&
|
||||
(type ? 'type' in model.props && type === model.props.type : true)
|
||||
) ?? textConversionConfigs[0];
|
||||
const update = (flavour: string, type?: string) => {
|
||||
chain
|
||||
.pipe(updateBlockType, {
|
||||
flavour,
|
||||
...(type && { props: { type } }),
|
||||
})
|
||||
.run();
|
||||
};
|
||||
|
||||
return {
|
||||
content: html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Conversions"
|
||||
.tooltip="${'Turn into'}"
|
||||
>
|
||||
${conversion.icon} ${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${repeat(
|
||||
textConversionConfigs.filter(c => c.flavour !== 'affine:divider'),
|
||||
item => item.name,
|
||||
({ flavour, type, name, icon }) => html`
|
||||
<editor-menu-action
|
||||
aria-label=${name}
|
||||
?data-selected=${conversion.name === name}
|
||||
@click=${() => update(flavour, type)}
|
||||
>
|
||||
${icon}<span class="label">${name}</span>
|
||||
</editor-menu-action>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
} as const satisfies ToolbarActionGenerator;
|
||||
|
||||
const inlineTextActionGroup = {
|
||||
id: 'b.inline-text',
|
||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||
actions: textFormatConfigs.map(
|
||||
({ id, name, action, activeWhen, icon }, score) => {
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
score,
|
||||
tooltip: name,
|
||||
run: ({ host }) => action(host),
|
||||
active: ({ host }) => activeWhen(host),
|
||||
};
|
||||
}
|
||||
),
|
||||
} as const satisfies ToolbarActionGroup;
|
||||
|
||||
const highlightActionGroup = {
|
||||
id: 'c.highlight',
|
||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||
content({ chain }) {
|
||||
const updateHighlight = (styles: AffineTextAttributes) => {
|
||||
const payload = { styles };
|
||||
chain
|
||||
.try(chain => [
|
||||
chain.pipe(getTextSelectionCommand).pipe(formatTextCommand, payload),
|
||||
chain
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe(formatBlockCommand, payload),
|
||||
chain.pipe(formatNativeCommand, payload),
|
||||
])
|
||||
.run();
|
||||
};
|
||||
return html`
|
||||
<affine-highlight-dropdown-menu
|
||||
.updateHighlight=${updateHighlight}
|
||||
></affine-highlight-dropdown-menu>
|
||||
`;
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const turnIntoDatabase = {
|
||||
id: 'd.convert-to-database',
|
||||
tooltip: 'Create Table',
|
||||
icon: DatabaseTableViewIcon(),
|
||||
when({ chain }) {
|
||||
const middleware = (count = 0) => {
|
||||
return (ctx: { selectedBlocks: BlockComponent[] }, next: () => void) => {
|
||||
const { selectedBlocks } = ctx;
|
||||
if (!selectedBlocks || selectedBlocks.length === count) return;
|
||||
|
||||
const allowed = selectedBlocks.every(block =>
|
||||
DATABASE_CONVERT_WHITE_LIST.includes(block.flavour)
|
||||
);
|
||||
if (!allowed) return;
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
let [ok] = chain
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
types: ['text'],
|
||||
})
|
||||
.pipe(middleware(1))
|
||||
.run();
|
||||
|
||||
if (ok) return true;
|
||||
|
||||
[ok] = chain
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
chain.pipe(getImageSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
types: ['block', 'image'],
|
||||
})
|
||||
.pipe(middleware(0))
|
||||
.run();
|
||||
|
||||
return ok;
|
||||
},
|
||||
run({ host }) {
|
||||
convertToDatabase(host, tableViewMeta.type);
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const turnIntoLinkedDoc = {
|
||||
id: 'e.convert-to-linked-doc',
|
||||
tooltip: 'Create Linked Doc',
|
||||
icon: LinkedPageIcon(),
|
||||
when({ chain }) {
|
||||
const [ok, { selectedModels }] = chain
|
||||
.pipe(getSelectedModelsCommand, {
|
||||
types: ['block', 'text'],
|
||||
mode: 'flat',
|
||||
})
|
||||
.run();
|
||||
return ok && Boolean(selectedModels?.length);
|
||||
},
|
||||
run({ chain, store, selection, std, track }) {
|
||||
const [ok, { draftedModels, selectedModels }] = chain
|
||||
.pipe(getSelectedModelsCommand, {
|
||||
types: ['block', 'text'],
|
||||
mode: 'flat',
|
||||
})
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.run();
|
||||
if (!ok || !draftedModels || !selectedModels?.length) return;
|
||||
|
||||
selection.clear();
|
||||
|
||||
const autofill = getTitleFromSelectedModels(
|
||||
selectedModels.map(toDraftModel)
|
||||
);
|
||||
promptDocTitle(std, autofill)
|
||||
.then(async title => {
|
||||
if (title === null) return;
|
||||
await convertSelectedBlocksToLinkedDoc(
|
||||
std,
|
||||
store,
|
||||
draftedModels,
|
||||
title
|
||||
);
|
||||
notifyDocCreated(std, store);
|
||||
|
||||
track('DocCreated', {
|
||||
segment: 'doc',
|
||||
page: 'doc editor',
|
||||
module: 'toolbar',
|
||||
control: 'create linked doc',
|
||||
type: 'embed-linked-doc',
|
||||
});
|
||||
|
||||
track('LinkedDocCreated', {
|
||||
segment: 'doc',
|
||||
page: 'doc editor',
|
||||
module: 'toolbar',
|
||||
control: 'create linked doc',
|
||||
type: 'embed-linked-doc',
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
export const builtinToolbarConfig = {
|
||||
actions: [
|
||||
conversionsActionGroup,
|
||||
inlineTextActionGroup,
|
||||
highlightActionGroup,
|
||||
turnIntoDatabase,
|
||||
turnIntoLinkedDoc,
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
actions: [
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon(),
|
||||
run({ chain, host }) {
|
||||
const [ok] = chain
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.pipe(copySelectedModelsCommand)
|
||||
.run();
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
toast(host, 'Copied to clipboard');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
icon: DuplicateIcon(),
|
||||
run({ chain, store, selection }) {
|
||||
store.captureSync();
|
||||
|
||||
const [ok, { selectedBlocks = [] }] = chain
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
types: ['text'],
|
||||
mode: 'highest',
|
||||
})
|
||||
.run();
|
||||
|
||||
// If text selection exists, convert to block selection
|
||||
if (ok && selectedBlocks.length) {
|
||||
selection.setGroup(
|
||||
'note',
|
||||
selectedBlocks.map(block =>
|
||||
selection.create(BlockSelection, {
|
||||
blockId: block.model.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
chain
|
||||
.pipe(getSelectedModelsCommand, {
|
||||
types: ['block', 'image'],
|
||||
mode: 'highest',
|
||||
})
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.pipe(duplicateSelectedModelsCommand)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
],
|
||||
when(ctx) {
|
||||
return !ctx.flags.isNative();
|
||||
},
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'c.delete',
|
||||
actions: [
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon(),
|
||||
variant: 'destructive',
|
||||
run({ chain }) {
|
||||
// removes text
|
||||
const [ok] = chain
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(deleteTextCommand)
|
||||
.run();
|
||||
|
||||
if (ok) return;
|
||||
|
||||
// removes blocks
|
||||
chain
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
chain.pipe(getImageSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(deleteSelectedModelsCommand)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
],
|
||||
when(ctx) {
|
||||
return !ctx.flags.isNative();
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
Reference in New Issue
Block a user