refactor(editor): extract note block (#9310)

This commit is contained in:
Saul-Mirone
2024-12-26 01:30:43 +00:00
parent 40b90ef51b
commit 2ffd0e561c
50 changed files with 467 additions and 394 deletions

View File

@@ -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;
},
},
];

View File

@@ -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);
},
},
];

View File

@@ -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,
},
];

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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
);

View File

@@ -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,
];

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 });
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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,
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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,
});
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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()),
},

View File

@@ -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';

View File

@@ -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',

View File

@@ -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';

View File

@@ -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,