mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
refactor(editor): extract note block (#9310)
This commit is contained in:
@@ -1,125 +0,0 @@
|
||||
import type { BlockSelection, BlockStdScope } from '@blocksuite/block-std';
|
||||
|
||||
const getSelection = (std: BlockStdScope) => std.selection;
|
||||
|
||||
function getBlockSelectionBySide(std: BlockStdScope, tail: boolean) {
|
||||
const selection = getSelection(std);
|
||||
const selections = selection.filter('block');
|
||||
const sel = selections.at(tail ? -1 : 0) as BlockSelection | undefined;
|
||||
return sel ?? null;
|
||||
}
|
||||
|
||||
function getTextSelection(std: BlockStdScope) {
|
||||
const selection = getSelection(std);
|
||||
return selection.find('text');
|
||||
}
|
||||
|
||||
const pathToBlock = (std: BlockStdScope, blockId: string) =>
|
||||
std.view.getBlock(blockId);
|
||||
|
||||
interface MoveBlockConfig {
|
||||
name: string;
|
||||
hotkey: string[];
|
||||
action: (std: BlockStdScope) => void;
|
||||
}
|
||||
|
||||
export const moveBlockConfigs: MoveBlockConfig[] = [
|
||||
{
|
||||
name: 'Move Up',
|
||||
hotkey: ['Mod-Alt-ArrowUp', 'Mod-Shift-ArrowUp'],
|
||||
action: std => {
|
||||
const doc = std.doc;
|
||||
const textSelection = getTextSelection(std);
|
||||
if (textSelection) {
|
||||
const currentModel = pathToBlock(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const previousSiblingModel = doc.getPrev(currentModel);
|
||||
if (!previousSiblingModel) return;
|
||||
|
||||
const parentModel = std.doc.getParent(previousSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
std.doc.moveBlocks(
|
||||
[currentModel],
|
||||
parentModel,
|
||||
previousSiblingModel,
|
||||
true
|
||||
);
|
||||
std.host.updateComplete
|
||||
.then(() => {
|
||||
std.range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
return true;
|
||||
}
|
||||
const blockSelection = getBlockSelectionBySide(std, true);
|
||||
if (blockSelection) {
|
||||
const currentModel = pathToBlock(std, blockSelection.blockId)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const previousSiblingModel = doc.getPrev(currentModel);
|
||||
if (!previousSiblingModel) return;
|
||||
|
||||
const parentModel = doc.getParent(previousSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
doc.moveBlocks(
|
||||
[currentModel],
|
||||
parentModel,
|
||||
previousSiblingModel,
|
||||
false
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Move Down',
|
||||
hotkey: ['Mod-Alt-ArrowDown', 'Mod-Shift-ArrowDown'],
|
||||
action: std => {
|
||||
const doc = std.doc;
|
||||
const textSelection = getTextSelection(std);
|
||||
if (textSelection) {
|
||||
const currentModel = pathToBlock(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const nextSiblingModel = doc.getNext(currentModel);
|
||||
if (!nextSiblingModel) return;
|
||||
|
||||
const parentModel = doc.getParent(nextSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
doc.moveBlocks([currentModel], parentModel, nextSiblingModel, false);
|
||||
std.host.updateComplete
|
||||
.then(() => {
|
||||
std.range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
return true;
|
||||
}
|
||||
const blockSelection = getBlockSelectionBySide(std, true);
|
||||
if (blockSelection) {
|
||||
const currentModel = pathToBlock(std, blockSelection.blockId)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const nextSiblingModel = doc.getNext(currentModel);
|
||||
if (!nextSiblingModel) return;
|
||||
|
||||
const parentModel = doc.getParent(nextSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
doc.moveBlocks([currentModel], parentModel, nextSiblingModel, false);
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,154 +0,0 @@
|
||||
import {
|
||||
CopyIcon,
|
||||
DatabaseTableViewIcon20,
|
||||
LinkedDocIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { convertToDatabase } from '../../../database-block/data-source.js';
|
||||
import { DATABASE_CONVERT_WHITE_LIST } from '../../../database-block/utils/block-utils.js';
|
||||
import {
|
||||
convertSelectedBlocksToLinkedDoc,
|
||||
getTitleFromSelectedModels,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '../../utils/render-linked-doc.js';
|
||||
|
||||
export interface QuickActionConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
disabledToolTip?: string;
|
||||
icon: TemplateResult<1>;
|
||||
hotkey?: string;
|
||||
showWhen: (host: EditorHost) => boolean;
|
||||
enabledWhen: (host: EditorHost) => boolean;
|
||||
action: (host: EditorHost) => void;
|
||||
}
|
||||
|
||||
export const quickActionConfig: QuickActionConfig[] = [
|
||||
{
|
||||
id: 'copy',
|
||||
name: 'Copy',
|
||||
disabledToolTip: undefined,
|
||||
icon: CopyIcon,
|
||||
hotkey: undefined,
|
||||
showWhen: () => true,
|
||||
enabledWhen: () => true,
|
||||
action: host => {
|
||||
host.std.command
|
||||
.chain()
|
||||
.getSelectedModels()
|
||||
.with({
|
||||
onCopy: () => {
|
||||
toast(host, 'Copied to clipboard');
|
||||
},
|
||||
})
|
||||
.draftSelectedModels()
|
||||
.copySelectedModels()
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'convert-to-database',
|
||||
name: 'Group as Table',
|
||||
disabledToolTip:
|
||||
'Contains Block types that cannot be converted to Database',
|
||||
icon: DatabaseTableViewIcon20,
|
||||
showWhen: host => {
|
||||
const [_, ctx] = host.std.command
|
||||
.chain()
|
||||
.getSelectedModels({
|
||||
types: ['block', 'text'],
|
||||
})
|
||||
.run();
|
||||
const { selectedModels } = ctx;
|
||||
if (!selectedModels || selectedModels.length === 0) return false;
|
||||
|
||||
const firstBlock = selectedModels[0];
|
||||
assertExists(firstBlock);
|
||||
if (matchFlavours(firstBlock, ['affine:database'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
enabledWhen: host => {
|
||||
const [_, ctx] = host.std.command
|
||||
.chain()
|
||||
.getSelectedModels({
|
||||
types: ['block', 'text'],
|
||||
})
|
||||
.run();
|
||||
const { selectedModels } = ctx;
|
||||
if (!selectedModels || selectedModels.length === 0) return false;
|
||||
|
||||
return selectedModels.every(block =>
|
||||
DATABASE_CONVERT_WHITE_LIST.includes(block.flavour)
|
||||
);
|
||||
},
|
||||
action: host => {
|
||||
convertToDatabase(host, tableViewMeta.type);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'convert-to-linked-doc',
|
||||
name: 'Create Linked Doc',
|
||||
icon: LinkedDocIcon,
|
||||
hotkey: `Mod-Shift-l`,
|
||||
showWhen: host => {
|
||||
const [_, ctx] = host.std.command
|
||||
.chain()
|
||||
.getSelectedModels({
|
||||
types: ['block'],
|
||||
})
|
||||
.run();
|
||||
const { selectedModels } = ctx;
|
||||
return !!selectedModels && selectedModels.length > 0;
|
||||
},
|
||||
enabledWhen: host => {
|
||||
const [_, ctx] = host.std.command
|
||||
.chain()
|
||||
.getSelectedModels({
|
||||
types: ['block'],
|
||||
})
|
||||
.run();
|
||||
const { selectedModels } = ctx;
|
||||
return !!selectedModels && selectedModels.length > 0;
|
||||
},
|
||||
action: host => {
|
||||
const [_, ctx] = host.std.command
|
||||
.chain()
|
||||
.getSelectedModels({
|
||||
types: ['block'],
|
||||
mode: 'highest',
|
||||
})
|
||||
.draftSelectedModels()
|
||||
.run();
|
||||
const { selectedModels, draftedModels } = ctx;
|
||||
assertExists(selectedModels);
|
||||
if (!selectedModels.length || !draftedModels) return;
|
||||
|
||||
host.selection.clear();
|
||||
|
||||
const doc = host.doc;
|
||||
const autofill = getTitleFromSelectedModels(selectedModels);
|
||||
promptDocTitle(host, autofill)
|
||||
.then(title => {
|
||||
if (title === null) return;
|
||||
convertSelectedBlocksToLinkedDoc(
|
||||
host.std,
|
||||
doc,
|
||||
draftedModels,
|
||||
title
|
||||
).catch(console.error);
|
||||
notifyDocCreated(host, doc);
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,136 +0,0 @@
|
||||
import {
|
||||
BulletedListIcon,
|
||||
CheckBoxIcon,
|
||||
CodeBlockIcon,
|
||||
DividerIcon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
Heading4Icon,
|
||||
Heading5Icon,
|
||||
Heading6Icon,
|
||||
NumberedListIcon,
|
||||
QuoteIcon,
|
||||
TextIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
/**
|
||||
* Text primitive entries used in slash menu and format bar,
|
||||
* which are also used for registering hotkeys for converting block flavours.
|
||||
*/
|
||||
export interface TextConversionConfig {
|
||||
flavour: BlockSuite.Flavour;
|
||||
type?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
hotkey: string[] | null;
|
||||
icon: TemplateResult<1>;
|
||||
}
|
||||
|
||||
export const textConversionConfigs: TextConversionConfig[] = [
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'text',
|
||||
name: 'Text',
|
||||
description: 'Start typing with plain text.',
|
||||
hotkey: [`Mod-Alt-0`, `Mod-Shift-0`],
|
||||
icon: TextIcon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h1',
|
||||
name: 'Heading 1',
|
||||
description: 'Headings in the largest font.',
|
||||
hotkey: [`Mod-Alt-1`, `Mod-Shift-1`],
|
||||
icon: Heading1Icon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h2',
|
||||
name: 'Heading 2',
|
||||
description: 'Headings in the 2nd font size.',
|
||||
hotkey: [`Mod-Alt-2`, `Mod-Shift-2`],
|
||||
icon: Heading2Icon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h3',
|
||||
name: 'Heading 3',
|
||||
description: 'Headings in the 3rd font size.',
|
||||
hotkey: [`Mod-Alt-3`, `Mod-Shift-3`],
|
||||
icon: Heading3Icon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h4',
|
||||
name: 'Heading 4',
|
||||
description: 'Headings in the 4th font size.',
|
||||
hotkey: [`Mod-Alt-4`, `Mod-Shift-4`],
|
||||
icon: Heading4Icon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h5',
|
||||
name: 'Heading 5',
|
||||
description: 'Headings in the 5th font size.',
|
||||
hotkey: [`Mod-Alt-5`, `Mod-Shift-5`],
|
||||
icon: Heading5Icon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h6',
|
||||
name: 'Heading 6',
|
||||
description: 'Headings in the 6th font size.',
|
||||
hotkey: [`Mod-Alt-6`, `Mod-Shift-6`],
|
||||
icon: Heading6Icon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:list',
|
||||
type: 'bulleted',
|
||||
name: 'Bulleted List',
|
||||
description: 'Create a bulleted list.',
|
||||
hotkey: [`Mod-Alt-8`, `Mod-Shift-8`],
|
||||
icon: BulletedListIcon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:list',
|
||||
type: 'numbered',
|
||||
name: 'Numbered List',
|
||||
description: 'Create a numbered list.',
|
||||
hotkey: [`Mod-Alt-9`, `Mod-Shift-9`],
|
||||
icon: NumberedListIcon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:list',
|
||||
type: 'todo',
|
||||
name: 'To-do List',
|
||||
description: 'Add tasks to a to-do list.',
|
||||
hotkey: null,
|
||||
icon: CheckBoxIcon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:code',
|
||||
type: undefined,
|
||||
name: 'Code Block',
|
||||
description: 'Code snippet with formatting.',
|
||||
hotkey: [`Mod-Alt-c`],
|
||||
icon: CodeBlockIcon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'quote',
|
||||
name: 'Quote',
|
||||
description: 'Add a blockquote for emphasis.',
|
||||
hotkey: null,
|
||||
icon: QuoteIcon,
|
||||
},
|
||||
{
|
||||
flavour: 'affine:divider',
|
||||
type: 'divider',
|
||||
name: 'Divider',
|
||||
description: 'Visually separate content.',
|
||||
hotkey: [`Mod-Alt-d`, `Mod-Shift-d`],
|
||||
icon: DividerIcon,
|
||||
},
|
||||
];
|
||||
@@ -1,19 +1,9 @@
|
||||
import type { FrameBlockModel, NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocModeProvider,
|
||||
NotificationProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getBlockProps, matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getBlockProps } from '@blocksuite/affine-shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type BlockModel,
|
||||
type BlockSnapshot,
|
||||
type Doc,
|
||||
type DraftModel,
|
||||
Slice,
|
||||
} from '@blocksuite/store';
|
||||
import { type BlockModel, type Doc } from '@blocksuite/store';
|
||||
|
||||
import { GfxBlockModel } from '../../root-block/edgeless/block-model.js';
|
||||
import {
|
||||
@@ -27,68 +17,6 @@ import {
|
||||
} from '../../root-block/edgeless/utils/query.js';
|
||||
import { getSurfaceBlock } from '../../surface-ref-block/utils.js';
|
||||
|
||||
export function promptDocTitle(host: EditorHost, autofill?: string) {
|
||||
const notification = host.std.getOptional(NotificationProvider);
|
||||
if (!notification) return Promise.resolve(undefined);
|
||||
|
||||
return notification.prompt({
|
||||
title: 'Create linked doc',
|
||||
message: 'Enter a title for the new doc.',
|
||||
placeholder: 'Untitled',
|
||||
autofill,
|
||||
confirmText: 'Confirm',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
}
|
||||
|
||||
export function getTitleFromSelectedModels(selectedModels: DraftModel[]) {
|
||||
const firstBlock = selectedModels[0];
|
||||
if (
|
||||
matchFlavours(firstBlock, ['affine:paragraph']) &&
|
||||
firstBlock.type.startsWith('h')
|
||||
) {
|
||||
return firstBlock.text.toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function notifyDocCreated(host: EditorHost, doc: Doc) {
|
||||
const notification = host.std.getOptional(NotificationProvider);
|
||||
if (!notification) return;
|
||||
|
||||
const abortController = new AbortController();
|
||||
const clear = () => {
|
||||
doc.history.off('stack-item-added', addHandler);
|
||||
doc.history.off('stack-item-popped', popHandler);
|
||||
disposable.dispose();
|
||||
};
|
||||
const closeNotify = () => {
|
||||
abortController.abort();
|
||||
clear();
|
||||
};
|
||||
|
||||
// edit or undo or switch doc, close notify toast
|
||||
const addHandler = doc.history.on('stack-item-added', closeNotify);
|
||||
const popHandler = doc.history.on('stack-item-popped', closeNotify);
|
||||
const disposable = host.slots.unmounted.on(closeNotify);
|
||||
|
||||
notification.notify({
|
||||
title: 'Linked doc created',
|
||||
message: 'You can click undo to recovery block content',
|
||||
accent: 'info',
|
||||
duration: 10 * 1000,
|
||||
action: {
|
||||
label: 'Undo',
|
||||
onClick: () => {
|
||||
doc.undo();
|
||||
clear();
|
||||
},
|
||||
},
|
||||
abort: abortController.signal,
|
||||
onClose: clear,
|
||||
});
|
||||
}
|
||||
|
||||
export function addBlocksToDoc(
|
||||
targetDoc: Doc,
|
||||
model: BlockModel,
|
||||
@@ -110,61 +38,6 @@ export function addBlocksToDoc(
|
||||
}
|
||||
}
|
||||
|
||||
export async function convertSelectedBlocksToLinkedDoc(
|
||||
std: BlockSuite.Std,
|
||||
doc: Doc,
|
||||
selectedModels: DraftModel[] | Promise<DraftModel[]>,
|
||||
docTitle?: string
|
||||
) {
|
||||
const models = await selectedModels;
|
||||
const slice = std.clipboard.sliceToSnapshot(Slice.fromModels(doc, models));
|
||||
if (!slice) {
|
||||
return;
|
||||
}
|
||||
const firstBlock = models[0];
|
||||
assertExists(firstBlock);
|
||||
// if title undefined, use the first heading block content as doc title
|
||||
const title = docTitle || getTitleFromSelectedModels(models);
|
||||
const linkedDoc = createLinkedDocFromSlice(std, doc, slice.content, title);
|
||||
// insert linked doc card
|
||||
doc.addSiblingBlocks(
|
||||
doc.getBlock(firstBlock.id)!.model,
|
||||
[
|
||||
{
|
||||
flavour: 'affine:embed-linked-doc',
|
||||
pageId: linkedDoc.id,
|
||||
},
|
||||
],
|
||||
'before'
|
||||
);
|
||||
// delete selected elements
|
||||
models.forEach(model => doc.deleteBlock(model));
|
||||
return linkedDoc;
|
||||
}
|
||||
|
||||
export function createLinkedDocFromSlice(
|
||||
std: BlockSuite.Std,
|
||||
doc: Doc,
|
||||
snapshots: BlockSnapshot[],
|
||||
docTitle?: string
|
||||
) {
|
||||
// const modelsWithChildren = (list:BlockModel[]):BlockModel[]=>list.flatMap(model=>[model,...modelsWithChildren(model.children)])
|
||||
const linkedDoc = doc.collection.createDoc({});
|
||||
linkedDoc.load(() => {
|
||||
const rootId = linkedDoc.addBlock('affine:page', {
|
||||
title: new doc.Text(docTitle),
|
||||
});
|
||||
linkedDoc.addBlock('affine:surface', {}, rootId);
|
||||
const noteId = linkedDoc.addBlock('affine:note', {}, rootId);
|
||||
snapshots.forEach(snapshot => {
|
||||
std.clipboard
|
||||
.pasteBlockSnapshot(snapshot, linkedDoc, noteId)
|
||||
.catch(console.error);
|
||||
});
|
||||
});
|
||||
return linkedDoc;
|
||||
}
|
||||
|
||||
export function createLinkedDocFromNote(
|
||||
doc: Doc,
|
||||
note: NoteBlockModel,
|
||||
|
||||
@@ -3,6 +3,10 @@ import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
|
||||
import { EmbedExtensions } from '@blocksuite/affine-block-embed';
|
||||
import { ImageBlockSpec } from '@blocksuite/affine-block-image';
|
||||
import { ListBlockSpec } from '@blocksuite/affine-block-list';
|
||||
import {
|
||||
EdgelessNoteBlockSpec,
|
||||
NoteBlockSpec,
|
||||
} from '@blocksuite/affine-block-note';
|
||||
import { ParagraphBlockSpec } from '@blocksuite/affine-block-paragraph';
|
||||
import { RichTextExtensions } from '@blocksuite/affine-components/rich-text';
|
||||
import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
@@ -13,10 +17,6 @@ import { CodeBlockSpec } from '../code-block/code-block-spec.js';
|
||||
import { DataViewBlockSpec } from '../data-view-block/data-view-spec.js';
|
||||
import { DatabaseBlockSpec } from '../database-block/database-spec.js';
|
||||
import { DividerBlockSpec } from '../divider-block/divider-spec.js';
|
||||
import {
|
||||
EdgelessNoteBlockSpec,
|
||||
NoteBlockSpec,
|
||||
} from '../note-block/note-spec.js';
|
||||
|
||||
export const CommonFirstPartyBlockSpecs: ExtensionType[] = [
|
||||
RichTextExtensions,
|
||||
|
||||
@@ -11,16 +11,16 @@ import {
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { ImageBlockSpec } from '@blocksuite/affine-block-image';
|
||||
import { ListBlockSpec } from '@blocksuite/affine-block-list';
|
||||
import {
|
||||
EdgelessNoteBlockSpec,
|
||||
NoteBlockSpec,
|
||||
} from '@blocksuite/affine-block-note';
|
||||
import { ParagraphBlockSpec } from '@blocksuite/affine-block-paragraph';
|
||||
|
||||
import { CodeBlockSpec } from '../../code-block/code-block-spec.js';
|
||||
import { DataViewBlockSpec } from '../../data-view-block/data-view-spec.js';
|
||||
import { DatabaseBlockSpec } from '../../database-block/database-spec.js';
|
||||
import { DividerBlockSpec } from '../../divider-block/divider-spec.js';
|
||||
import {
|
||||
EdgelessNoteBlockSpec,
|
||||
NoteBlockSpec,
|
||||
} from '../../note-block/note-spec.js';
|
||||
|
||||
export {
|
||||
AttachmentBlockSpec,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NoteBlockComponent } from '@blocksuite/affine-block-note';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
menu,
|
||||
@@ -40,7 +41,6 @@ import { html } from 'lit/static-html.js';
|
||||
|
||||
import { BlockRenderer } from '../database-block/detail-panel/block-renderer.js';
|
||||
import { NoteRenderer } from '../database-block/detail-panel/note-renderer.js';
|
||||
import type { NoteBlockComponent } from '../note-block/index.js';
|
||||
import {
|
||||
EdgelessRootBlockComponent,
|
||||
type RootService,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NoteBlockComponent } from '@blocksuite/affine-block-note';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
menu,
|
||||
@@ -44,7 +45,6 @@ import { autoUpdate } from '@floating-ui/dom';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
|
||||
import type { NoteBlockComponent } from '../note-block/index.js';
|
||||
import { EdgelessRootBlockComponent } from '../root-block/index.js';
|
||||
import { getDropResult } from '../root-block/widgets/drag-handle/utils.js';
|
||||
import { popSideDetail } from './components/layout.js';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { effects as blockBookmarkEffects } from '@blocksuite/affine-block-bookma
|
||||
import { effects as blockEmbedEffects } from '@blocksuite/affine-block-embed/effects';
|
||||
import { effects as blockImageEffects } from '@blocksuite/affine-block-image/effects';
|
||||
import { effects as blockListEffects } from '@blocksuite/affine-block-list/effects';
|
||||
import { effects as blockNoteEffects } from '@blocksuite/affine-block-note/effects';
|
||||
import { effects as blockParagraphEffects } from '@blocksuite/affine-block-paragraph/effects';
|
||||
import { effects as blockSurfaceEffects } from '@blocksuite/affine-block-surface/effects';
|
||||
import { effects as componentAiItemEffects } from '@blocksuite/affine-components/ai-item';
|
||||
@@ -66,23 +67,6 @@ import { EdgelessTextBlockComponent } from './edgeless-text-block/index.js';
|
||||
import { FrameBlockComponent } from './frame-block/index.js';
|
||||
import { effects as blockLatexEffects } from './latex-block/effects.js';
|
||||
import { LatexBlockComponent } from './latex-block/index.js';
|
||||
import type { updateBlockType } from './note-block/commands/block-type.js';
|
||||
import type { dedentBlock } from './note-block/commands/dedent-block.js';
|
||||
import type { dedentBlockToRoot } from './note-block/commands/dedent-block-to-root.js';
|
||||
import type { dedentBlocks } from './note-block/commands/dedent-blocks.js';
|
||||
import type { dedentBlocksToRoot } from './note-block/commands/dedent-blocks-to-root.js';
|
||||
import type { focusBlockEnd } from './note-block/commands/focus-block-end.js';
|
||||
import type { focusBlockStart } from './note-block/commands/focus-block-start.js';
|
||||
import type { indentBlock } from './note-block/commands/indent-block.js';
|
||||
import type { indentBlocks } from './note-block/commands/indent-blocks.js';
|
||||
import type { selectBlock } from './note-block/commands/select-block.js';
|
||||
import type { selectBlocksBetween } from './note-block/commands/select-blocks-between.js';
|
||||
import {
|
||||
EdgelessNoteBlockComponent,
|
||||
EdgelessNoteMask,
|
||||
NoteBlockComponent,
|
||||
type NoteBlockService,
|
||||
} from './note-block/index.js';
|
||||
import { EdgelessAutoCompletePanel } from './root-block/edgeless/components/auto-complete/auto-complete-panel.js';
|
||||
import { EdgelessAutoComplete } from './root-block/edgeless/components/auto-complete/edgeless-auto-complete.js';
|
||||
import { EdgelessToolIconButton } from './root-block/edgeless/components/buttons/tool-icon-button.js';
|
||||
@@ -264,6 +248,7 @@ export function effects() {
|
||||
stdEffects();
|
||||
inlineEffects();
|
||||
|
||||
blockNoteEffects();
|
||||
blockAttachmentEffects();
|
||||
blockBookmarkEffects();
|
||||
blockListEffects();
|
||||
@@ -314,8 +299,6 @@ export function effects() {
|
||||
customElements.define('database-datasource-block-renderer', BlockRenderer);
|
||||
customElements.define('affine-latex', LatexBlockComponent);
|
||||
customElements.define('affine-page-root', PageRootBlockComponent);
|
||||
customElements.define('edgeless-note-mask', EdgelessNoteMask);
|
||||
customElements.define('affine-edgeless-note', EdgelessNoteBlockComponent);
|
||||
customElements.define('affine-preview-root', PreviewRootBlockComponent);
|
||||
customElements.define('affine-code', CodeBlockComponent);
|
||||
customElements.define('mini-mindmap-preview', MiniMindmapPreview);
|
||||
@@ -445,7 +428,6 @@ export function effects() {
|
||||
customElements.define('edgeless-present-button', EdgelessPresentButton);
|
||||
customElements.define('edgeless-color-picker', EdgelessColorPicker);
|
||||
customElements.define('overlay-scrollbar', OverlayScrollbar);
|
||||
customElements.define('affine-note', NoteBlockComponent);
|
||||
customElements.define('affine-template-loading', AffineTemplateLoading);
|
||||
customElements.define(
|
||||
'edgeless-color-picker-button',
|
||||
@@ -549,18 +531,7 @@ export function effects() {
|
||||
declare global {
|
||||
namespace BlockSuite {
|
||||
interface Commands {
|
||||
selectBlock: typeof selectBlock;
|
||||
selectBlocksBetween: typeof selectBlocksBetween;
|
||||
focusBlockStart: typeof focusBlockStart;
|
||||
focusBlockEnd: typeof focusBlockEnd;
|
||||
indentBlocks: typeof indentBlocks;
|
||||
dedentBlock: typeof dedentBlock;
|
||||
dedentBlocksToRoot: typeof dedentBlocksToRoot;
|
||||
dedentBlocks: typeof dedentBlocks;
|
||||
indentBlock: typeof indentBlock;
|
||||
updateBlockType: typeof updateBlockType;
|
||||
insertEdgelessText: typeof insertEdgelessTextCommand;
|
||||
dedentBlockToRoot: typeof dedentBlockToRoot;
|
||||
}
|
||||
interface CommandContext {
|
||||
focusBlock?: BlockComponent | null;
|
||||
@@ -573,7 +544,6 @@ declare global {
|
||||
'affine:page': RootBlockConfig;
|
||||
}
|
||||
interface BlockServices {
|
||||
'affine:note': NoteBlockService;
|
||||
'affine:page': RootService;
|
||||
'affine:database': DatabaseBlockService;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export * from './divider-block/index.js';
|
||||
export * from './edgeless-text-block/index.js';
|
||||
export * from './frame-block/index.js';
|
||||
export * from './latex-block/index.js';
|
||||
export * from './note-block/index.js';
|
||||
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
|
||||
export type {
|
||||
Template,
|
||||
@@ -52,6 +51,7 @@ export * from '@blocksuite/affine-block-bookmark';
|
||||
export * from '@blocksuite/affine-block-embed';
|
||||
export * from '@blocksuite/affine-block-image';
|
||||
export * from '@blocksuite/affine-block-list';
|
||||
export * from '@blocksuite/affine-block-note';
|
||||
export * from '@blocksuite/affine-block-paragraph';
|
||||
export * from '@blocksuite/affine-block-surface';
|
||||
export {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
/**
|
||||
* Create a html adapter matcher for note block.
|
||||
*
|
||||
* @param displayModeToSkip - The note with specific display mode to skip.
|
||||
* For example, the note with display mode `EdgelessOnly` should not be converted to html when current editor mode is `Doc(Page)`.
|
||||
* @returns The html adapter matcher.
|
||||
*/
|
||||
const createNoteBlockHtmlAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockHtmlAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
if (node.props.displayMode === displayModeToSkip) {
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docNoteBlockHtmlAdapterMatcher = createNoteBlockHtmlAdapterMatcher(
|
||||
NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
|
||||
export const edgelessNoteBlockHtmlAdapterMatcher =
|
||||
createNoteBlockHtmlAdapterMatcher(NoteDisplayMode.DocOnly);
|
||||
|
||||
export const DocNoteBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
docNoteBlockHtmlAdapterMatcher
|
||||
);
|
||||
|
||||
export const EdgelessNoteBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
edgelessNoteBlockHtmlAdapterMatcher
|
||||
);
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import {
|
||||
DocNoteBlockHtmlAdapterExtension,
|
||||
EdgelessNoteBlockHtmlAdapterExtension,
|
||||
} from './html.js';
|
||||
import {
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
EdgelessNoteBlockMarkdownAdapterExtension,
|
||||
} from './markdown.js';
|
||||
import {
|
||||
DocNoteBlockPlainTextAdapterExtension,
|
||||
EdgelessNoteBlockPlainTextAdapterExtension,
|
||||
} from './plain-text.js';
|
||||
|
||||
export const DocNoteBlockAdapterExtensions: ExtensionType[] = [
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
DocNoteBlockHtmlAdapterExtension,
|
||||
DocNoteBlockPlainTextAdapterExtension,
|
||||
];
|
||||
|
||||
export const EdgelessNoteBlockAdapterExtensions: ExtensionType[] = [
|
||||
EdgelessNoteBlockMarkdownAdapterExtension,
|
||||
EdgelessNoteBlockHtmlAdapterExtension,
|
||||
EdgelessNoteBlockPlainTextAdapterExtension,
|
||||
];
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
/**
|
||||
* Create a markdown adapter matcher for note block.
|
||||
*
|
||||
* @param displayModeToSkip - The note with specific display mode to skip.
|
||||
* For example, the note with display mode `EdgelessOnly` should not be converted to markdown when current editor mode is `Doc`.
|
||||
* @returns The markdown adapter matcher.
|
||||
*/
|
||||
const createNoteBlockMarkdownAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockMarkdownAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
if (node.props.displayMode === displayModeToSkip) {
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docNoteBlockMarkdownAdapterMatcher =
|
||||
createNoteBlockMarkdownAdapterMatcher(NoteDisplayMode.EdgelessOnly);
|
||||
|
||||
export const edgelessNoteBlockMarkdownAdapterMatcher =
|
||||
createNoteBlockMarkdownAdapterMatcher(NoteDisplayMode.DocOnly);
|
||||
|
||||
export const DocNoteBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(docNoteBlockMarkdownAdapterMatcher);
|
||||
|
||||
export const EdgelessNoteBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(edgelessNoteBlockMarkdownAdapterMatcher);
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
/**
|
||||
* Create a plain text adapter matcher for note block.
|
||||
*
|
||||
* @param displayModeToSkip - The note with specific display mode to skip.
|
||||
* For example, the note with display mode `EdgelessOnly` should not be converted to plain text when current editor mode is `Doc(Page)`.
|
||||
* @returns The plain text adapter matcher.
|
||||
*/
|
||||
const createNoteBlockPlainTextAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockPlainTextAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
if (node.props.displayMode === displayModeToSkip) {
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docNoteBlockPlainTextAdapterMatcher =
|
||||
createNoteBlockPlainTextAdapterMatcher(NoteDisplayMode.EdgelessOnly);
|
||||
|
||||
export const edgelessNoteBlockPlainTextAdapterMatcher =
|
||||
createNoteBlockPlainTextAdapterMatcher(NoteDisplayMode.DocOnly);
|
||||
|
||||
export const DocNoteBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(docNoteBlockPlainTextAdapterMatcher);
|
||||
|
||||
export const EdgelessNoteBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(edgelessNoteBlockPlainTextAdapterMatcher);
|
||||
@@ -1,220 +0,0 @@
|
||||
import {
|
||||
asyncSetInlineRange,
|
||||
focusTextModel,
|
||||
onModelTextUpdated,
|
||||
} from '@blocksuite/affine-components/rich-text';
|
||||
import {
|
||||
matchFlavours,
|
||||
mergeToCodeModel,
|
||||
transformModel,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
type UpdateBlockConfig = {
|
||||
flavour: BlockSuite.Flavour;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const updateBlockType: Command<
|
||||
'selectedBlocks',
|
||||
'updatedBlocks',
|
||||
UpdateBlockConfig
|
||||
> = (ctx, next) => {
|
||||
const { std, flavour, props } = ctx;
|
||||
const host = std.host;
|
||||
const doc = std.doc;
|
||||
|
||||
const getSelectedBlocks = () => {
|
||||
let { selectedBlocks } = ctx;
|
||||
|
||||
if (selectedBlocks == null) {
|
||||
const [result, ctx] = std.command
|
||||
.chain()
|
||||
.tryAll(chain => [chain.getTextSelection(), chain.getBlockSelections()])
|
||||
.getSelectedBlocks({ types: ['text', 'block'] })
|
||||
.run();
|
||||
if (result) {
|
||||
selectedBlocks = ctx.selectedBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedBlocks;
|
||||
};
|
||||
|
||||
const selectedBlocks = getSelectedBlocks();
|
||||
if (!selectedBlocks || selectedBlocks.length === 0) return false;
|
||||
|
||||
const blockModels = selectedBlocks.map(ele => ele.model);
|
||||
|
||||
const hasSameDoc = selectedBlocks.every(block => block.doc === doc);
|
||||
if (!hasSameDoc) {
|
||||
// doc check
|
||||
console.error(
|
||||
'Not all models have the same doc instance, the result for update text type may not be correct',
|
||||
selectedBlocks
|
||||
);
|
||||
}
|
||||
|
||||
const mergeToCode: Command<never, 'updatedBlocks'> = (_, next) => {
|
||||
if (flavour !== 'affine:code') return;
|
||||
const id = mergeToCodeModel(blockModels);
|
||||
if (!id) return;
|
||||
const model = doc.getBlockById(id);
|
||||
if (!model) return;
|
||||
asyncSetInlineRange(host, model, {
|
||||
index: model.text?.length ?? 0,
|
||||
length: 0,
|
||||
}).catch(console.error);
|
||||
return next({ updatedBlocks: [model] });
|
||||
};
|
||||
const appendDivider: Command<never, 'updatedBlocks'> = (_, next) => {
|
||||
if (flavour !== 'affine:divider') {
|
||||
return false;
|
||||
}
|
||||
const model = blockModels.at(-1);
|
||||
if (!model) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
const index = parent.children.indexOf(model);
|
||||
const nextSibling = doc.getNext(model);
|
||||
let nextSiblingId = nextSibling?.id as string;
|
||||
const id = doc.addBlock('affine:divider', {}, parent, index + 1);
|
||||
if (!nextSibling) {
|
||||
nextSiblingId = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(host.std, nextSiblingId);
|
||||
const newModel = doc.getBlockById(id);
|
||||
if (!newModel) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
return next({ updatedBlocks: [newModel] });
|
||||
};
|
||||
|
||||
const focusText: Command<'updatedBlocks'> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstNewModel = updatedBlocks[0];
|
||||
const lastNewModel = updatedBlocks[updatedBlocks.length - 1];
|
||||
|
||||
const allTextUpdated = updatedBlocks.map(model =>
|
||||
onModelTextUpdated(host, model)
|
||||
);
|
||||
const selectionManager = host.selection;
|
||||
const textSelection = selectionManager.find('text');
|
||||
if (!textSelection) {
|
||||
return false;
|
||||
}
|
||||
const newTextSelection = selectionManager.create('text', {
|
||||
from: {
|
||||
blockId: firstNewModel.id,
|
||||
index: textSelection.from.index,
|
||||
length: textSelection.from.length,
|
||||
},
|
||||
to: textSelection.to
|
||||
? {
|
||||
blockId: lastNewModel.id,
|
||||
index: textSelection.to.index,
|
||||
length: textSelection.to.length,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
Promise.all(allTextUpdated)
|
||||
.then(() => {
|
||||
selectionManager.setGroup('note', [newTextSelection]);
|
||||
})
|
||||
.catch(console.error);
|
||||
return next();
|
||||
};
|
||||
|
||||
const focusBlock: Command<'updatedBlocks'> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectionManager = host.selection;
|
||||
|
||||
const blockSelections = selectionManager.filter('block');
|
||||
if (blockSelections.length === 0) {
|
||||
return false;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const selections = updatedBlocks.map(model => {
|
||||
return selectionManager.create('block', {
|
||||
blockId: model.id,
|
||||
});
|
||||
});
|
||||
|
||||
selectionManager.setGroup('note', selections);
|
||||
});
|
||||
return next();
|
||||
};
|
||||
|
||||
const [result, resultCtx] = std.command
|
||||
.chain()
|
||||
.inline((_, next) => {
|
||||
doc.captureSync();
|
||||
return next();
|
||||
})
|
||||
// update block type
|
||||
.try<'updatedBlocks'>(chain => [
|
||||
chain.inline<'updatedBlocks'>(mergeToCode),
|
||||
chain.inline<'updatedBlocks'>(appendDivider),
|
||||
chain.inline<'updatedBlocks'>((_, next) => {
|
||||
const newModels: BlockModel[] = [];
|
||||
blockModels.forEach(model => {
|
||||
if (
|
||||
!matchFlavours(model, [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (model.flavour === flavour) {
|
||||
doc.updateBlock(model, props ?? {});
|
||||
newModels.push(model);
|
||||
return;
|
||||
}
|
||||
const newId = transformModel(model, flavour, props);
|
||||
if (!newId) {
|
||||
return;
|
||||
}
|
||||
const newModel = doc.getBlockById(newId);
|
||||
if (newModel) {
|
||||
newModels.push(newModel);
|
||||
}
|
||||
});
|
||||
return next({ updatedBlocks: newModels });
|
||||
}),
|
||||
])
|
||||
// focus
|
||||
.try(chain => [
|
||||
chain.inline((_, next) => {
|
||||
if (['affine:code', 'affine:divider'].includes(flavour)) {
|
||||
return next();
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
chain.inline(focusText),
|
||||
chain.inline(focusBlock),
|
||||
chain.inline((_, next) => next()),
|
||||
])
|
||||
.run();
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next({ updatedBlocks: resultCtx.updatedBlocks });
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const dedentBlockToRoot: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.doc.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
let parent = doc.getParent(model);
|
||||
let changed = false;
|
||||
while (parent && !matchFlavours(parent, ['affine:note'])) {
|
||||
if (!changed) {
|
||||
if (stopCapture) doc.captureSync();
|
||||
changed = true;
|
||||
}
|
||||
std.command.exec('dedentBlock', { blockId: model.id, stopCapture: true });
|
||||
parent = doc.getParent(model);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const dedentBlock: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.doc.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
const grandParent = parent && doc.getParent(parent);
|
||||
if (doc.readonly || !parent || parent.role !== 'content' || !grandParent) {
|
||||
// Top most, can not unindent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
doc.moveBlocks([model, ...collapsedSiblings], grandParent, parent, false);
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const nextSiblings = doc.getNexts(model);
|
||||
doc.moveBlocks(nextSiblings, model);
|
||||
doc.moveBlocks([model], grandParent, parent, false);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const dedentBlocksToRoot: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const text = std.selection.find('text');
|
||||
if (text) {
|
||||
// If the text selection is not at the beginning of the block, use default behavior
|
||||
if (text.from.index !== 0) return;
|
||||
|
||||
blockIds = [text.from.blockId, text.to?.blockId].filter(
|
||||
(x): x is string => !!x
|
||||
);
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || doc.readonly) return;
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
for (let i = blockIds.length - 1; i >= 0; i--) {
|
||||
const model = blockIds[i];
|
||||
const parent = doc.getParent(model);
|
||||
if (parent && !matchFlavours(parent, ['affine:note'])) {
|
||||
std.command.exec('dedentBlockToRoot', {
|
||||
blockId: model,
|
||||
stopCapture: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const dedentBlocks: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc, selection, range, host } = std;
|
||||
const { schema } = doc;
|
||||
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const nativeRange = range.value;
|
||||
if (nativeRange) {
|
||||
const topBlocks = range.getSelectedBlockComponentsByRange(nativeRange, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
if (topBlocks.length > 0) {
|
||||
blockIds = topBlocks.map(block => block.blockId);
|
||||
}
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || doc.readonly) return;
|
||||
|
||||
// Find the first model that can be unindented
|
||||
let firstDedentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const model = doc.getBlock(blockIds[i])?.model;
|
||||
if (!model) continue;
|
||||
const parent = doc.getParent(blockIds[i]);
|
||||
if (!parent) continue;
|
||||
const grandParent = doc.getParent(parent);
|
||||
if (!grandParent) continue;
|
||||
|
||||
if (schema.isValid(model.flavour, grandParent.flavour)) {
|
||||
firstDedentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDedentIndex === -1) return;
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstDedentIndex).forEach(id => {
|
||||
const model = doc.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
collapsedIds.push(...collapsedSiblings.map(sibling => sibling.id));
|
||||
}
|
||||
});
|
||||
// Models waiting to be dedented
|
||||
const dedentIds = blockIds
|
||||
.slice(firstDedentIndex)
|
||||
.filter(id => !collapsedIds.includes(id));
|
||||
dedentIds.reverse().forEach(id => {
|
||||
std.command.exec('dedentBlock', { blockId: id, stopCapture: false });
|
||||
});
|
||||
|
||||
const textSelection = selection.find('text');
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const focusBlockEnd: Command<'focusBlock'> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock || !focusBlock.model.text) return;
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create('text', {
|
||||
from: {
|
||||
blockId: focusBlock.blockId,
|
||||
index: focusBlock.model.text.length,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const focusBlockStart: Command<'focusBlock'> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock || !focusBlock.model.text) return;
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create('text', {
|
||||
from: { blockId: focusBlock.blockId, index: 0, length: 0 },
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const indentBlock: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
const { schema } = doc;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.doc.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const previousSibling = doc.getPrev(model);
|
||||
if (
|
||||
doc.readonly ||
|
||||
!previousSibling ||
|
||||
!schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
// can not indent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
doc.moveBlocks([model, ...collapsedSiblings], previousSibling);
|
||||
} else {
|
||||
doc.moveBlocks([model], previousSibling);
|
||||
}
|
||||
|
||||
// update collapsed state of affine list
|
||||
if (
|
||||
matchFlavours(previousSibling, ['affine:list']) &&
|
||||
previousSibling.collapsed
|
||||
) {
|
||||
doc.updateBlock(previousSibling, {
|
||||
collapsed: false,
|
||||
} as Partial<ListBlockModel>);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const indentBlocks: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc, selection, range, host } = std;
|
||||
const { schema } = doc;
|
||||
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const nativeRange = range.value;
|
||||
if (nativeRange) {
|
||||
const topBlocks = range.getSelectedBlockComponentsByRange(nativeRange, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
if (topBlocks.length > 0) {
|
||||
blockIds = topBlocks.map(block => block.blockId);
|
||||
}
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || doc.readonly) return;
|
||||
|
||||
// Find the first model that can be indented
|
||||
let firstIndentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const previousSibling = doc.getPrev(blockIds[i]);
|
||||
const model = doc.getBlock(blockIds[i])?.model;
|
||||
if (
|
||||
model &&
|
||||
previousSibling &&
|
||||
schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
firstIndentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No model can be indented
|
||||
if (firstIndentIndex === -1) return;
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstIndentIndex).forEach(id => {
|
||||
const model = doc.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
collapsedIds.push(...collapsedSiblings.map(sibling => sibling.id));
|
||||
}
|
||||
});
|
||||
// Models waiting to be indented
|
||||
const indentIds = blockIds
|
||||
.slice(firstIndentIndex)
|
||||
.filter(id => !collapsedIds.includes(id));
|
||||
const firstModel = doc.getBlock(indentIds[0])?.model;
|
||||
if (!firstModel) return;
|
||||
|
||||
{
|
||||
// > # 123
|
||||
// > # 456
|
||||
// > # 789
|
||||
//
|
||||
// we need to update 123 collapsed state to false when indent 456 and 789
|
||||
|
||||
const nearestHeading = getNearestHeadingBefore(firstModel);
|
||||
if (
|
||||
nearestHeading &&
|
||||
matchFlavours(nearestHeading, ['affine:paragraph']) &&
|
||||
nearestHeading.collapsed
|
||||
) {
|
||||
doc.updateBlock(nearestHeading, {
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
indentIds.forEach(id => {
|
||||
std.command.exec('indentBlock', { blockId: id, stopCapture: false });
|
||||
});
|
||||
|
||||
{
|
||||
// 123
|
||||
// > # 456
|
||||
// 789
|
||||
// 012
|
||||
//
|
||||
// we need to update 456 collapsed state to false when indent 789 and 012
|
||||
const nearestHeading = getNearestHeadingBefore(firstModel);
|
||||
if (
|
||||
nearestHeading &&
|
||||
matchFlavours(nearestHeading, ['affine:paragraph']) &&
|
||||
nearestHeading.collapsed
|
||||
) {
|
||||
doc.updateBlock(nearestHeading, {
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const textSelection = selection.find('text');
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
getBlockIndexCommand,
|
||||
getBlockSelectionsCommand,
|
||||
getNextBlockCommand,
|
||||
getPrevBlockCommand,
|
||||
getSelectedBlocksCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type { BlockCommands } from '@blocksuite/block-std';
|
||||
|
||||
import { updateBlockType } from './block-type.js';
|
||||
import { dedentBlock } from './dedent-block.js';
|
||||
import { dedentBlockToRoot } from './dedent-block-to-root.js';
|
||||
import { dedentBlocks } from './dedent-blocks.js';
|
||||
import { dedentBlocksToRoot } from './dedent-blocks-to-root.js';
|
||||
import { focusBlockEnd } from './focus-block-end.js';
|
||||
import { focusBlockStart } from './focus-block-start.js';
|
||||
import { indentBlock } from './indent-block.js';
|
||||
import { indentBlocks } from './indent-blocks.js';
|
||||
import { selectBlock } from './select-block.js';
|
||||
import { selectBlocksBetween } from './select-blocks-between.js';
|
||||
|
||||
export const commands: BlockCommands = {
|
||||
// block
|
||||
getBlockIndex: getBlockIndexCommand,
|
||||
getPrevBlock: getPrevBlockCommand,
|
||||
getNextBlock: getNextBlockCommand,
|
||||
getSelectedBlocks: getSelectedBlocksCommand,
|
||||
getBlockSelections: getBlockSelectionsCommand,
|
||||
selectBlock,
|
||||
selectBlocksBetween,
|
||||
focusBlockStart,
|
||||
focusBlockEnd,
|
||||
updateBlockType,
|
||||
indentBlock,
|
||||
dedentBlock,
|
||||
indentBlocks,
|
||||
dedentBlocks,
|
||||
dedentBlockToRoot,
|
||||
dedentBlocksToRoot,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const selectBlock: Command<'focusBlock'> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create('block', { blockId: focusBlock.blockId }),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const selectBlocksBetween: Command<
|
||||
'focusBlock' | 'anchorBlock',
|
||||
never,
|
||||
{ tail: boolean }
|
||||
> = (ctx, next) => {
|
||||
const { focusBlock, anchorBlock, tail } = ctx;
|
||||
if (!focusBlock || !anchorBlock) {
|
||||
return;
|
||||
}
|
||||
const selection = ctx.std.selection;
|
||||
|
||||
// In same block
|
||||
if (anchorBlock.blockId === focusBlock.blockId) {
|
||||
const blockId = focusBlock.blockId;
|
||||
selection.setGroup('note', [selection.create('block', { blockId })]);
|
||||
return next();
|
||||
}
|
||||
|
||||
// In different blocks
|
||||
const selections = [...selection.value];
|
||||
if (selections.every(sel => sel.blockId !== focusBlock.blockId)) {
|
||||
if (tail) {
|
||||
selections.push(
|
||||
selection.create('block', { blockId: focusBlock.blockId })
|
||||
);
|
||||
} else {
|
||||
selections.unshift(
|
||||
selection.create('block', { blockId: focusBlock.blockId })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let start = false;
|
||||
const sel = selections.filter(sel => {
|
||||
if (
|
||||
sel.blockId === anchorBlock.blockId ||
|
||||
sel.blockId === focusBlock.blockId
|
||||
) {
|
||||
start = !start;
|
||||
return true;
|
||||
}
|
||||
return start;
|
||||
});
|
||||
|
||||
selection.setGroup('note', sel);
|
||||
return next();
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './commands/index.js';
|
||||
export * from './note-block.js';
|
||||
export * from './note-edgeless-block.js';
|
||||
export * from './note-service.js';
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { BlockComponent } from '@blocksuite/block-std';
|
||||
import { css, html } from 'lit';
|
||||
|
||||
import type { NoteBlockService } from './note-service.js';
|
||||
|
||||
export class NoteBlockComponent extends BlockComponent<
|
||||
NoteBlockModel,
|
||||
NoteBlockService
|
||||
> {
|
||||
static override styles = css`
|
||||
.affine-note-block-container {
|
||||
display: flow-root;
|
||||
}
|
||||
.affine-note-block-container.selected {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="affine-note-block-container">
|
||||
<div class="affine-block-children-container">
|
||||
${this.renderChildren(this.model)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-note': NoteBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { MoreIndicatorIcon } from '@blocksuite/affine-components/icons';
|
||||
import type { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
NoteDisplayMode,
|
||||
StrokeStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
getClosestBlockComponentByPoint,
|
||||
handleNativeRangeAtPoint,
|
||||
matchFlavours,
|
||||
stopPropagation,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent, EditorHost } from '@blocksuite/block-std';
|
||||
import { ShadowlessElement, toGfxBlockComponent } from '@blocksuite/block-std';
|
||||
import {
|
||||
almostEqual,
|
||||
Bound,
|
||||
clamp,
|
||||
Point,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootService } from '../root-block/index.js';
|
||||
import { NoteBlockComponent } from './note-block.js';
|
||||
|
||||
export class EdgelessNoteMask extends WithDisposable(ShadowlessElement) {
|
||||
protected override firstUpdated() {
|
||||
const maskDOM = this.renderRoot!.querySelector('.affine-note-mask');
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (!this.model.edgeless.collapse) {
|
||||
const bound = Bound.deserialize(this.model.xywh);
|
||||
const scale = this.model.edgeless.scale ?? 1;
|
||||
const height = entry.contentRect.height * scale;
|
||||
|
||||
if (!height || almostEqual(bound.h, height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bound.h = height;
|
||||
this.model.stash('xywh');
|
||||
this.model.xywh = bound.serialize();
|
||||
this.model.pop('xywh');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(maskDOM!);
|
||||
|
||||
this._disposables.add(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const extra = this.editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
return html`
|
||||
<div
|
||||
class="affine-note-mask"
|
||||
style=${styleMap({
|
||||
position: 'absolute',
|
||||
top: `${-extra}px`,
|
||||
left: `${-extra}px`,
|
||||
bottom: `${-extra}px`,
|
||||
right: `${-extra}px`,
|
||||
zIndex: '1',
|
||||
pointerEvents: this.display ? 'auto' : 'none',
|
||||
borderRadius: `${
|
||||
this.model.edgeless.style.borderRadius * this.zoom
|
||||
}px`,
|
||||
})}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor display!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editing!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor model!: NoteBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor zoom!: number;
|
||||
}
|
||||
|
||||
const ACTIVE_NOTE_EXTRA_PADDING = 20;
|
||||
|
||||
export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
NoteBlockComponent
|
||||
) {
|
||||
static override styles = css`
|
||||
.edgeless-note-collapse-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.edgeless-note-collapse-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.edgeless-note-collapse-button.flip {
|
||||
transform: translateX(-50%) rotate(180deg);
|
||||
}
|
||||
.edgeless-note-collapse-button.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edgeless-note-container:has(.affine-embed-synced-doc-container.editing)
|
||||
> .note-background {
|
||||
left: ${-ACTIVE_NOTE_EXTRA_PADDING}px !important;
|
||||
top: ${-ACTIVE_NOTE_EXTRA_PADDING}px !important;
|
||||
width: calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px) !important;
|
||||
height: calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px) !important;
|
||||
}
|
||||
|
||||
.edgeless-note-container:has(.affine-embed-synced-doc-container.editing)
|
||||
> edgeless-note-mask {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
private get _isShowCollapsedContent() {
|
||||
return this.model.edgeless.collapse && (this._isResizing || this._isHover);
|
||||
}
|
||||
|
||||
get _zoom() {
|
||||
return this.gfx.viewport.zoom;
|
||||
}
|
||||
|
||||
get rootService() {
|
||||
return this.std.getService('affine:page') as EdgelessRootService;
|
||||
}
|
||||
|
||||
private _collapsedContent() {
|
||||
if (!this._isShowCollapsedContent) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { xywh, edgeless } = this.model;
|
||||
const { borderSize } = edgeless.style;
|
||||
|
||||
const extraPadding = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
const extraBorder = this._editing ? borderSize : 0;
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const scale = edgeless.scale ?? 1;
|
||||
const width = bound.w / scale + extraPadding * 2 + extraBorder;
|
||||
const height = bound.h / scale;
|
||||
|
||||
const rect = this._notePageContent?.getBoundingClientRect();
|
||||
if (!rect) return nothing;
|
||||
|
||||
const zoom = this.gfx.viewport.zoom;
|
||||
this._noteFullHeight =
|
||||
rect.height / scale / zoom + 2 * EDGELESS_BLOCK_CHILD_PADDING;
|
||||
|
||||
if (height >= this._noteFullHeight) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
width: `${width}px`,
|
||||
height: `${this._noteFullHeight - height}px`,
|
||||
position: 'absolute',
|
||||
left: `${-(extraPadding + extraBorder / 2)}px`,
|
||||
top: `${height + extraPadding + extraBorder / 2}px`,
|
||||
background: 'var(--affine-white)',
|
||||
opacity: 0.5,
|
||||
pointerEvents: 'none',
|
||||
borderLeft: '2px var(--affine-blue) solid',
|
||||
borderBottom: '2px var(--affine-blue) solid',
|
||||
borderRight: '2px var(--affine-blue) solid',
|
||||
borderRadius: '0 0 8px 8px',
|
||||
})}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClickAtBackground(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (!this._editing) return;
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
const offsetY = 16 * this._zoom;
|
||||
const offsetX = 2 * this._zoom;
|
||||
const x = clamp(e.x, rect.left + offsetX, rect.right - offsetX);
|
||||
const y = clamp(e.y, rect.top + offsetY, rect.bottom - offsetY);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
|
||||
if (this.doc.readonly) return;
|
||||
|
||||
this._tryAddParagraph(x, y);
|
||||
}
|
||||
|
||||
private _hovered() {
|
||||
if (
|
||||
this.selection.value.some(
|
||||
sel => sel.type === 'surface' && sel.blockId === this.model.id
|
||||
)
|
||||
) {
|
||||
this._isHover = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _leaved() {
|
||||
if (this._isHover) {
|
||||
this._isHover = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _setCollapse(event: MouseEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const { collapse, collapsedHeight } = this.model.edgeless;
|
||||
|
||||
if (collapse) {
|
||||
this.model.doc.updateBlock(this.model, () => {
|
||||
this.model.edgeless.collapse = false;
|
||||
});
|
||||
} else if (collapsedHeight) {
|
||||
const { xywh, edgeless } = this.model;
|
||||
const bound = Bound.deserialize(xywh);
|
||||
bound.h = collapsedHeight * (edgeless.scale ?? 1);
|
||||
this.model.doc.updateBlock(this.model, () => {
|
||||
this.model.edgeless.collapse = true;
|
||||
this.model.xywh = bound.serialize();
|
||||
});
|
||||
}
|
||||
|
||||
this.selection.clear();
|
||||
}
|
||||
|
||||
private _tryAddParagraph(x: number, y: number) {
|
||||
const nearest = getClosestBlockComponentByPoint(
|
||||
new Point(x, y)
|
||||
) as BlockComponent | null;
|
||||
if (!nearest) return;
|
||||
|
||||
const nearestBBox = nearest.getBoundingClientRect();
|
||||
const yRel = y - nearestBBox.top;
|
||||
|
||||
const insertPos: 'before' | 'after' =
|
||||
yRel < nearestBBox.height / 2 ? 'before' : 'after';
|
||||
|
||||
const nearestModel = nearest.model as BlockModel;
|
||||
const nearestModelIdx = this.model.children.indexOf(nearestModel);
|
||||
|
||||
const children = this.model.children;
|
||||
const siblingModel =
|
||||
children[
|
||||
clamp(
|
||||
nearestModelIdx + (insertPos === 'before' ? -1 : 1),
|
||||
0,
|
||||
children.length
|
||||
)
|
||||
];
|
||||
|
||||
if (
|
||||
(!nearestModel.text ||
|
||||
!matchFlavours(nearestModel, ['affine:paragraph', 'affine:list'])) &&
|
||||
(!siblingModel ||
|
||||
!siblingModel.text ||
|
||||
!matchFlavours(siblingModel, ['affine:paragraph', 'affine:list']))
|
||||
) {
|
||||
const [pId] = this.doc.addSiblingBlocks(
|
||||
nearestModel,
|
||||
[{ flavour: 'affine:paragraph' }],
|
||||
insertPos
|
||||
);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
this.std.selection.setGroup('note', [
|
||||
this.std.selection.create('text', {
|
||||
from: {
|
||||
blockId: pId,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
const selection = this.rootService.selection;
|
||||
|
||||
this._editing = selection.has(this.model.id) && selection.editing;
|
||||
this._disposables.add(
|
||||
selection.slots.updated.on(() => {
|
||||
if (selection.has(this.model.id) && selection.editing) {
|
||||
this._editing = true;
|
||||
} else {
|
||||
this._editing = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { _disposables } = this;
|
||||
const selection = this.rootService.selection;
|
||||
|
||||
_disposables.add(
|
||||
this.rootService.slots.elementResizeStart.on(() => {
|
||||
if (selection.selectedElements.includes(this.model)) {
|
||||
this._isResizing = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
this.rootService.slots.elementResizeEnd.on(() => {
|
||||
this._isResizing = false;
|
||||
})
|
||||
);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const rect = this._notePageContent?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const zoom = this.gfx.viewport.zoom;
|
||||
const scale = this.model.edgeless.scale ?? 1;
|
||||
this._noteFullHeight =
|
||||
rect.height / scale / zoom + 2 * EDGELESS_BLOCK_CHILD_PADDING;
|
||||
});
|
||||
if (this._notePageContent) {
|
||||
observer.observe(this, { childList: true, subtree: true });
|
||||
_disposables.add(() => observer.disconnect());
|
||||
}
|
||||
}
|
||||
|
||||
override getRenderingRect() {
|
||||
const { xywh, edgeless } = this.model;
|
||||
const { collapse, scale = 1 } = edgeless;
|
||||
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const width = bound.w / scale;
|
||||
const height = bound.h / scale;
|
||||
|
||||
return {
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
w: width,
|
||||
h: collapse ? height : 'inherit',
|
||||
zIndex: this.toZIndex(),
|
||||
};
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { model } = this;
|
||||
const { displayMode } = model;
|
||||
if (!!displayMode && displayMode === NoteDisplayMode.DocOnly)
|
||||
return nothing;
|
||||
|
||||
const { xywh, edgeless } = model;
|
||||
const { borderRadius, borderSize, borderStyle, shadowType } =
|
||||
edgeless.style;
|
||||
const { collapse, collapsedHeight, scale = 1 } = edgeless;
|
||||
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const width = bound.w / scale;
|
||||
const height = bound.h / scale;
|
||||
|
||||
const style = {
|
||||
height: '100%',
|
||||
padding: `${EDGELESS_BLOCK_CHILD_PADDING}px`,
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: borderRadius + 'px',
|
||||
pointerEvents: 'all',
|
||||
transformOrigin: '0 0',
|
||||
transform: `scale(${scale})`,
|
||||
fontWeight: '400',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
};
|
||||
|
||||
const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
const backgroundColor = this.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(model.background, DEFAULT_NOTE_BACKGROUND_COLOR);
|
||||
|
||||
const backgroundStyle = {
|
||||
position: 'absolute',
|
||||
left: `${-extra}px`,
|
||||
top: `${-extra}px`,
|
||||
width: `${width + extra * 2}px`,
|
||||
height: `calc(100% + ${extra * 2}px)`,
|
||||
borderRadius: borderRadius + 'px',
|
||||
transition: this._editing
|
||||
? 'left 0.3s, top 0.3s, width 0.3s, height 0.3s'
|
||||
: 'none',
|
||||
backgroundColor,
|
||||
border: `${borderSize}px ${
|
||||
borderStyle === StrokeStyle.Dash ? 'dashed' : borderStyle
|
||||
} var(--affine-black-10)`,
|
||||
boxShadow: this._editing
|
||||
? 'var(--affine-active-shadow)'
|
||||
: !shadowType
|
||||
? 'none'
|
||||
: `var(${shadowType})`,
|
||||
};
|
||||
|
||||
const isCollapsable =
|
||||
collapse != null &&
|
||||
collapsedHeight != null &&
|
||||
collapsedHeight !== this._noteFullHeight;
|
||||
|
||||
const isCollapseArrowUp = collapse
|
||||
? this._noteFullHeight < height
|
||||
: !!collapsedHeight && collapsedHeight < height;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="edgeless-note-container"
|
||||
style=${styleMap(style)}
|
||||
data-model-height="${bound.h}"
|
||||
@mouseleave=${this._leaved}
|
||||
@mousemove=${this._hovered}
|
||||
data-scale="${scale}"
|
||||
>
|
||||
<div
|
||||
class="note-background"
|
||||
style=${styleMap(backgroundStyle)}
|
||||
@pointerdown=${stopPropagation}
|
||||
@click=${this._handleClickAtBackground}
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="edgeless-note-page-content"
|
||||
style=${styleMap({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'overflow-y': this._isShowCollapsedContent ? 'initial' : 'clip',
|
||||
})}
|
||||
>
|
||||
${this.renderPageContent()}
|
||||
</div>
|
||||
|
||||
${isCollapsable
|
||||
? html`<div
|
||||
class="${classMap({
|
||||
'edgeless-note-collapse-button': true,
|
||||
flip: isCollapseArrowUp,
|
||||
hide: this._isSelected,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
bottom: this._editing ? `${-extra}px` : '0',
|
||||
})}
|
||||
@mousedown=${stopPropagation}
|
||||
@mouseup=${stopPropagation}
|
||||
@click=${this._setCollapse}
|
||||
>
|
||||
${MoreIndicatorIcon}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._collapsedContent()}
|
||||
|
||||
<edgeless-note-mask
|
||||
.model=${this.model}
|
||||
.display=${!this._editing}
|
||||
.host=${this.host}
|
||||
.zoom=${this.gfx.viewport.zoom ?? 1}
|
||||
.editing=${this._editing}
|
||||
></edgeless-note-mask>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _editing = false;
|
||||
|
||||
@state()
|
||||
private accessor _isHover = false;
|
||||
|
||||
@state()
|
||||
private accessor _isResizing = false;
|
||||
|
||||
@state()
|
||||
private accessor _isSelected = false;
|
||||
|
||||
@state()
|
||||
private accessor _noteFullHeight = 0;
|
||||
|
||||
@query('.edgeless-note-page-content .affine-note-block-container')
|
||||
private accessor _notePageContent: HTMLElement | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-note': EdgelessNoteBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,586 +0,0 @@
|
||||
import { NoteBlockSchema } from '@blocksuite/affine-model';
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BaseSelection,
|
||||
type BlockComponent,
|
||||
type BlockSelection,
|
||||
BlockService,
|
||||
type UIEventHandler,
|
||||
type UIEventStateContext,
|
||||
} from '@blocksuite/block-std';
|
||||
|
||||
import { moveBlockConfigs } from '../_common/configs/move-block.js';
|
||||
import { quickActionConfig } from '../_common/configs/quick-action/config.js';
|
||||
import { textConversionConfigs } from '../_common/configs/text-conversion.js';
|
||||
import { onModelElementUpdated } from '../root-block/utils/callback.js';
|
||||
|
||||
export class NoteBlockService extends BlockService {
|
||||
static override readonly flavour = NoteBlockSchema.model.flavour;
|
||||
|
||||
private _anchorSel: BlockSelection | null = null;
|
||||
|
||||
private readonly _bindMoveBlockHotKey = () => {
|
||||
return moveBlockConfigs.reduce(
|
||||
(acc, config) => {
|
||||
const keys = config.hotkey.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
return config.action(this.std);
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
return {
|
||||
...acc,
|
||||
...keys,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindQuickActionHotKey = () => {
|
||||
return quickActionConfig
|
||||
.filter(config => config.hotkey)
|
||||
.reduce(
|
||||
(acc, config) => {
|
||||
return {
|
||||
...acc,
|
||||
[config.hotkey!]: ctx => {
|
||||
if (!config.showWhen(this.std.host)) return;
|
||||
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
config.action(this.std.host);
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindTextConversionHotKey = () => {
|
||||
return textConversionConfigs
|
||||
.filter(item => item.hotkey)
|
||||
.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()
|
||||
.updateBlockType({
|
||||
flavour: item.flavour,
|
||||
props: {
|
||||
type: item.type,
|
||||
},
|
||||
})
|
||||
.inline((ctx, next) => {
|
||||
const newModels = ctx.updatedBlocks;
|
||||
const host = ctx.std.host;
|
||||
if (!host || !newModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.flavour !== 'affine:code') {
|
||||
return;
|
||||
}
|
||||
|
||||
const [codeModel] = newModels;
|
||||
onModelElementUpdated(host, codeModel, codeElement => {
|
||||
this._std.selection.setGroup('note', [
|
||||
this._std.selection.create('text', {
|
||||
from: {
|
||||
blockId: codeElement.blockId,
|
||||
index: 0,
|
||||
length: codeModel.text?.length ?? 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
}).catch(console.error);
|
||||
|
||||
next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...keymap,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private _focusBlock: BlockComponent | null = null;
|
||||
|
||||
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
||||
const doc = this._std.doc;
|
||||
let parent = doc.getBlock(blockId)?.model ?? null;
|
||||
while (parent) {
|
||||
if (matchFlavours(parent, [NoteBlockSchema.model.flavour])) {
|
||||
return parent;
|
||||
}
|
||||
parent = doc.getParent(parent);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private readonly _onArrowDown = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.inline((_, next) => {
|
||||
this._reset();
|
||||
return next();
|
||||
})
|
||||
.try(cmd => [
|
||||
// text selection - select the next block
|
||||
// 1. is paragraph, list, code block - follow the default behavior
|
||||
// 2. is not - select the next block (use block selection instead of text selection)
|
||||
cmd
|
||||
.getTextSelection()
|
||||
.inline<'currentSelectionPath'>((ctx, next) => {
|
||||
const currentTextSelection = ctx.currentTextSelection;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: currentTextSelection.blockId });
|
||||
})
|
||||
.getNextBlock()
|
||||
.inline((ctx, next) => {
|
||||
const { nextBlock } = ctx;
|
||||
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchFlavours(nextBlock.model, [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
])
|
||||
) {
|
||||
this._std.command
|
||||
.chain()
|
||||
.with({
|
||||
focusBlock: nextBlock,
|
||||
})
|
||||
.selectBlock()
|
||||
.run();
|
||||
}
|
||||
|
||||
return next({});
|
||||
}),
|
||||
|
||||
// block selection - select the next block
|
||||
// 1. is paragraph, list, code block - focus it
|
||||
// 2. is not - select it using block selection
|
||||
cmd
|
||||
.getBlockSelections()
|
||||
.inline<'currentSelectionPath'>((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
const blockSelection = currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: blockSelection.blockId });
|
||||
})
|
||||
.getNextBlock()
|
||||
.inline<'focusBlock'>((ctx, next) => {
|
||||
const { nextBlock } = ctx;
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if (
|
||||
matchFlavours(nextBlock.model, [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
])
|
||||
) {
|
||||
this._std.command
|
||||
.chain()
|
||||
.focusBlockStart({ focusBlock: nextBlock })
|
||||
.run();
|
||||
return next();
|
||||
}
|
||||
|
||||
this._std.command
|
||||
.chain()
|
||||
.with({ focusBlock: nextBlock })
|
||||
.selectBlock()
|
||||
.run();
|
||||
return next();
|
||||
}),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onArrowUp = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.inline((_, next) => {
|
||||
this._reset();
|
||||
return next();
|
||||
})
|
||||
.try(cmd => [
|
||||
// text selection - select the previous block
|
||||
// 1. is paragraph, list, code block - follow the default behavior
|
||||
// 2. is not - select the previous block (use block selection instead of text selection)
|
||||
cmd
|
||||
.getTextSelection()
|
||||
.inline<'currentSelectionPath'>((ctx, next) => {
|
||||
const currentTextSelection = ctx.currentTextSelection;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: currentTextSelection.blockId });
|
||||
})
|
||||
.getPrevBlock()
|
||||
.inline((ctx, next) => {
|
||||
const { prevBlock } = ctx;
|
||||
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchFlavours(prevBlock.model, [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
])
|
||||
) {
|
||||
this._std.command
|
||||
.chain()
|
||||
.with({
|
||||
focusBlock: prevBlock,
|
||||
})
|
||||
.selectBlock()
|
||||
.run();
|
||||
}
|
||||
|
||||
return next({});
|
||||
}),
|
||||
// block selection - select the previous block
|
||||
// 1. is paragraph, list, code block - focus it
|
||||
// 2. is not - select it using block selection
|
||||
cmd
|
||||
.getBlockSelections()
|
||||
.inline<'currentSelectionPath'>((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
const blockSelection = currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: blockSelection.blockId });
|
||||
})
|
||||
.getPrevBlock()
|
||||
.inline<'focusBlock'>((ctx, next) => {
|
||||
const { prevBlock } = ctx;
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
matchFlavours(prevBlock.model, [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
])
|
||||
) {
|
||||
event.preventDefault();
|
||||
this._std.command
|
||||
.chain()
|
||||
.focusBlockEnd({ focusBlock: prevBlock })
|
||||
.run();
|
||||
return next();
|
||||
}
|
||||
|
||||
this._std.command
|
||||
.chain()
|
||||
.with({ focusBlock: prevBlock })
|
||||
.selectBlock()
|
||||
.run();
|
||||
return next();
|
||||
}),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onBlockShiftDown = (cmd: BlockSuite.CommandChain) => {
|
||||
return cmd
|
||||
.getBlockSelections()
|
||||
.inline<'currentSelectionPath' | 'anchorBlock'>((ctx, next) => {
|
||||
const blockSelections = ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._anchorSel) {
|
||||
this._anchorSel = blockSelections.at(-1) ?? null;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorBlock = ctx.std.view.getBlock(this._anchorSel.blockId);
|
||||
if (!anchorBlock) {
|
||||
return;
|
||||
}
|
||||
return next({
|
||||
anchorBlock,
|
||||
currentSelectionPath:
|
||||
this._focusBlock?.blockId ?? anchorBlock?.blockId,
|
||||
});
|
||||
})
|
||||
.getNextBlock({})
|
||||
.inline<'focusBlock'>((ctx, next) => {
|
||||
const nextBlock = ctx.nextBlock;
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
this._focusBlock = nextBlock;
|
||||
return next({
|
||||
focusBlock: this._focusBlock,
|
||||
});
|
||||
})
|
||||
.selectBlocksBetween({ tail: true });
|
||||
};
|
||||
|
||||
private readonly _onBlockShiftUp = (cmd: BlockSuite.CommandChain) => {
|
||||
return cmd
|
||||
.getBlockSelections()
|
||||
.inline<'currentSelectionPath' | 'anchorBlock'>((ctx, next) => {
|
||||
const blockSelections = ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
return;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
this._anchorSel = blockSelections.at(0) ?? null;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
return;
|
||||
}
|
||||
const anchorBlock = ctx.std.view.getBlock(this._anchorSel.blockId);
|
||||
if (!anchorBlock) {
|
||||
return;
|
||||
}
|
||||
return next({
|
||||
anchorBlock,
|
||||
currentSelectionPath:
|
||||
this._focusBlock?.blockId ?? anchorBlock?.blockId,
|
||||
});
|
||||
})
|
||||
.getPrevBlock({})
|
||||
.inline((ctx, next) => {
|
||||
const prevBlock = ctx.prevBlock;
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
this._focusBlock = prevBlock;
|
||||
return next({
|
||||
focusBlock: this._focusBlock,
|
||||
});
|
||||
})
|
||||
.selectBlocksBetween({ tail: false });
|
||||
};
|
||||
|
||||
private readonly _onEnter = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.getBlockSelections()
|
||||
.inline((ctx, next) => {
|
||||
const blockSelection = ctx.currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { view, doc, selection } = ctx.std;
|
||||
|
||||
const element = view.getBlock(blockSelection.blockId);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { model } = element;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parent.children.indexOf(model) ?? undefined;
|
||||
|
||||
const blockId = doc.addBlock('affine:paragraph', {}, parent, index + 1);
|
||||
|
||||
const sel = selection.create('text', {
|
||||
from: {
|
||||
blockId,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
selection.setGroup('note', [sel]);
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onEsc = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.getBlockSelections()
|
||||
.inline((ctx, next) => {
|
||||
const blockSelection = ctx.currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.std.selection.update(selList => {
|
||||
return selList.filter(sel => !sel.is('block'));
|
||||
});
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onSelectAll: UIEventHandler = ctx => {
|
||||
const selection = this._std.selection;
|
||||
const block = selection.find('block');
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
const note = this._getClosestNoteByBlockId(block.blockId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const children = note.children;
|
||||
const blocks: BlockSelection[] = children.map(child => {
|
||||
return selection.create('block', {
|
||||
blockId: child.id,
|
||||
});
|
||||
});
|
||||
selection.update(selList => {
|
||||
return selList
|
||||
.filter<BaseSelection>(sel => !sel.is('block'))
|
||||
.concat(blocks);
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _onShiftArrowDown = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.try(cmd => [
|
||||
// block selection
|
||||
this._onBlockShiftDown(cmd),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onShiftArrowUp = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.try(cmd => [
|
||||
// block selection
|
||||
this._onBlockShiftUp(cmd),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _reset = () => {
|
||||
this._anchorSel = null;
|
||||
this._focusBlock = null;
|
||||
};
|
||||
|
||||
private get _std() {
|
||||
return this.std;
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
this.handleEvent('keyDown', ctx => {
|
||||
const state = ctx.get('keyboardState');
|
||||
if (['Control', 'Meta', 'Shift'].includes(state.raw.key)) {
|
||||
return;
|
||||
}
|
||||
this._reset();
|
||||
});
|
||||
|
||||
this.bindHotKey({
|
||||
...this._bindMoveBlockHotKey(),
|
||||
...this._bindQuickActionHotKey(),
|
||||
...this._bindTextConversionHotKey(),
|
||||
Tab: ctx => {
|
||||
const { success } = this.std.command.exec('indentBlocks');
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Tab': ctx => {
|
||||
const { success } = this.std.command.exec('dedentBlocks');
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Mod-Backspace': ctx => {
|
||||
const { success } = this.std.command.exec('dedentBlocksToRoot');
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowDown: this._onArrowDown,
|
||||
ArrowUp: this._onArrowUp,
|
||||
'Shift-ArrowDown': this._onShiftArrowDown,
|
||||
'Shift-ArrowUp': this._onShiftArrowUp,
|
||||
Escape: this._onEsc,
|
||||
Enter: this._onEnter,
|
||||
'Mod-a': this._onSelectAll,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
BlockViewExtension,
|
||||
CommandExtension,
|
||||
type ExtensionType,
|
||||
FlavourExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import {
|
||||
DocNoteBlockAdapterExtensions,
|
||||
EdgelessNoteBlockAdapterExtensions,
|
||||
} from './adapters/index.js';
|
||||
import { commands } from './commands/index.js';
|
||||
import { NoteBlockService } from './note-service.js';
|
||||
|
||||
export const NoteBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:note'),
|
||||
NoteBlockService,
|
||||
CommandExtension(commands),
|
||||
BlockViewExtension('affine:note', literal`affine-note`),
|
||||
DocNoteBlockAdapterExtensions,
|
||||
].flat();
|
||||
|
||||
export const EdgelessNoteBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:note'),
|
||||
NoteBlockService,
|
||||
CommandExtension(commands),
|
||||
BlockViewExtension('affine:note', literal`affine-edgeless-note`),
|
||||
EdgelessNoteBlockAdapterExtensions,
|
||||
].flat();
|
||||
@@ -1,14 +1,13 @@
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent, BlockSelection } from '@blocksuite/block-std';
|
||||
import { IS_MAC, IS_WINDOWS } from '@blocksuite/global/env';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
import {
|
||||
convertSelectedBlocksToLinkedDoc,
|
||||
getTitleFromSelectedModels,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '../../_common/utils/render-linked-doc.js';
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent, BlockSelection } from '@blocksuite/block-std';
|
||||
import { IS_MAC, IS_WINDOWS } from '@blocksuite/global/env';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
export class PageKeyboardManager {
|
||||
private readonly _handleDelete = () => {
|
||||
@@ -117,7 +116,7 @@ export class PageKeyboardManager {
|
||||
|
||||
const doc = rootComponent.host.doc;
|
||||
const autofill = getTitleFromSelectedModels(selectedModels);
|
||||
promptDocTitle(rootComponent.host, autofill)
|
||||
promptDocTitle(rootComponent.std, autofill)
|
||||
.then(title => {
|
||||
if (title === null) return;
|
||||
convertSelectedBlocksToLinkedDoc(
|
||||
@@ -126,7 +125,7 @@ export class PageKeyboardManager {
|
||||
draftedModels,
|
||||
title
|
||||
).catch(console.error);
|
||||
notifyDocCreated(rootComponent.host, doc);
|
||||
notifyDocCreated(rootComponent.std, doc);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NoteBlockComponent } from '@blocksuite/affine-block-note';
|
||||
import { captureEventTarget } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
BLOCK_ID_ATTR,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
import { Point, throttle } from '@blocksuite/global/utils';
|
||||
import { computed } from '@preact/signals-core';
|
||||
|
||||
import type { NoteBlockComponent } from '../../../../note-block/index.js';
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless/index.js';
|
||||
import {
|
||||
DRAG_HANDLE_CONTAINER_WIDTH,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { AttachmentBlockComponent } from '@blocksuite/affine-block-attachment';
|
||||
import type { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark';
|
||||
import type {
|
||||
EmbedFigmaBlockComponent,
|
||||
EmbedGithubBlockComponent,
|
||||
EmbedLoomBlockComponent,
|
||||
EmbedYoutubeBlockComponent,
|
||||
import {
|
||||
type EmbedFigmaBlockComponent,
|
||||
type EmbedGithubBlockComponent,
|
||||
type EmbedLoomBlockComponent,
|
||||
type EmbedYoutubeBlockComponent,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
|
||||
@@ -30,8 +32,6 @@ import {
|
||||
import {
|
||||
createLinkedDocFromEdgelessElements,
|
||||
createLinkedDocFromNote,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '../../../../_common/utils/render-linked-doc.js';
|
||||
import { duplicate } from '../../../edgeless/utils/clipboard-utils.js';
|
||||
import { getSortedCloneElements } from '../../../edgeless/utils/clone-utils.js';
|
||||
@@ -245,11 +245,11 @@ export const conversionsGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
|
||||
label: 'Turn into linked doc',
|
||||
type: 'turn-into-linked-doc',
|
||||
action: async ctx => {
|
||||
const { doc, service, surface, host, std } = ctx;
|
||||
const { doc, service, surface, std } = ctx;
|
||||
const element = ctx.getNoteBlock();
|
||||
if (!element) return;
|
||||
|
||||
const title = await promptDocTitle(host);
|
||||
const title = await promptDocTitle(std);
|
||||
if (title === null) return;
|
||||
|
||||
const linkedDoc = createLinkedDocFromNote(doc, element, title);
|
||||
@@ -309,7 +309,7 @@ export const conversionsGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
|
||||
host,
|
||||
std,
|
||||
}) => {
|
||||
const title = await promptDocTitle(host);
|
||||
const title = await promptDocTitle(std);
|
||||
if (title === null) return;
|
||||
|
||||
const elements = getSortedCloneElements(selection.selectedElements);
|
||||
@@ -360,7 +360,7 @@ export const conversionsGroup: MenuItemGroup<ElementToolbarMoreMenuContext> = {
|
||||
other: 'new doc',
|
||||
});
|
||||
|
||||
notifyDocCreated(host, doc);
|
||||
notifyDocCreated(std, doc);
|
||||
},
|
||||
when: ctx => !(ctx.getLinkedDocBlock() || ctx.getNoteBlock()),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { whenHover } from '@blocksuite/affine-components/hover';
|
||||
import { ArrowDownIcon } from '@blocksuite/affine-components/icons';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-components/rich-text';
|
||||
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
@@ -8,7 +9,6 @@ import { html } from 'lit';
|
||||
import { ref, type RefOrCallback } from 'lit/directives/ref.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { textConversionConfigs } from '../../../../_common/configs/text-conversion.js';
|
||||
import type { ParagraphActionConfigItem } from '../config.js';
|
||||
import type { AffineFormatBarWidget } from '../format-bar.js';
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
convertSelectedBlocksToLinkedDoc,
|
||||
getTitleFromSelectedModels,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
BoldIcon,
|
||||
BulletedListIcon,
|
||||
@@ -37,12 +43,6 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
|
||||
import {
|
||||
convertSelectedBlocksToLinkedDoc,
|
||||
getTitleFromSelectedModels,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '../../../_common/utils/render-linked-doc.js';
|
||||
import { convertToDatabase } from '../../../database-block/data-source.js';
|
||||
import { DATABASE_CONVERT_WHITE_LIST } from '../../../database-block/utils/block-utils.js';
|
||||
import { FormatBarContext } from './context.js';
|
||||
@@ -201,7 +201,7 @@ export function toolbarDefaultConfig(toolbar: AffineFormatBarWidget) {
|
||||
})
|
||||
.draftSelectedModels()
|
||||
.run();
|
||||
const { draftedModels, selectedModels } = ctx;
|
||||
const { draftedModels, selectedModels, std } = ctx;
|
||||
if (!selectedModels?.length || !draftedModels) return;
|
||||
|
||||
const host = formatBar.host;
|
||||
@@ -209,16 +209,16 @@ export function toolbarDefaultConfig(toolbar: AffineFormatBarWidget) {
|
||||
|
||||
const doc = host.doc;
|
||||
const autofill = getTitleFromSelectedModels(selectedModels);
|
||||
promptDocTitle(host, autofill)
|
||||
promptDocTitle(std, autofill)
|
||||
.then(async title => {
|
||||
if (title === null) return;
|
||||
await convertSelectedBlocksToLinkedDoc(
|
||||
host.std,
|
||||
std,
|
||||
doc,
|
||||
draftedModels,
|
||||
title
|
||||
);
|
||||
notifyDocCreated(host, doc);
|
||||
notifyDocCreated(std, doc);
|
||||
host.std.getOptional(TelemetryProvider)?.track('DocCreated', {
|
||||
control: 'create linked doc',
|
||||
page: 'doc editor',
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
getInlineEditorByModel,
|
||||
insertContent,
|
||||
REFERENCE_NODE,
|
||||
textConversionConfigs,
|
||||
textFormatConfigs,
|
||||
} from '@blocksuite/affine-components/rich-text';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
@@ -49,7 +50,6 @@ import { Slice, Text } from '@blocksuite/store';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { toggleEmbedCardCreateModal } from '../../../_common/components/embed-card/modal/embed-card-create-modal.js';
|
||||
import { textConversionConfigs } from '../../../_common/configs/text-conversion.js';
|
||||
import type { DataViewBlockComponent } from '../../../data-view-block/index.js';
|
||||
import { getSurfaceBlock } from '../../../surface-ref-block/utils.js';
|
||||
import type { RootBlockComponent } from '../../types.js';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { TextFormatConfig } from '@blocksuite/affine-components/rich-text';
|
||||
import type {
|
||||
TextConversionConfig,
|
||||
TextFormatConfig,
|
||||
} from '@blocksuite/affine-components/rich-text';
|
||||
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { TextConversionConfig } from '../../../_common/configs/text-conversion.js';
|
||||
import type {
|
||||
SlashMenuActionItem,
|
||||
SlashMenuContext,
|
||||
|
||||
Reference in New Issue
Block a user