Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(editor): extract note block #9310

Merged
merged 1 commit into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions blocksuite/affine/block-embed/src/common/render-linked-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import {
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { EMBED_CARD_HEIGHT } from '@blocksuite/affine-shared/consts';
import { NotificationProvider } from '@blocksuite/affine-shared/services';
import { matchFlavours, SpecProvider } from '@blocksuite/affine-shared/utils';
import { BlockStdScope } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
import {
type BlockModel,
type BlockSnapshot,
BlockViewType,
type Doc,
type DraftModel,
type Query,
Slice,
} from '@blocksuite/store';
import { render, type TemplateResult } from 'lit';

Expand Down Expand Up @@ -295,3 +299,122 @@ export function getDocContentWithMaxLength(doc: Doc, maxlength = 500) {

return texts.join('\n');
}

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 promptDocTitle(std: BlockStdScope, autofill?: string) {
const notification = 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 notifyDocCreated(std: BlockStdScope, doc: Doc) {
const notification = 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 = std.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 async function convertSelectedBlocksToLinkedDoc(
std: BlockStdScope,
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];
if (!firstBlock) {
return;
}
// 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: BlockStdScope,
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;
}
2 changes: 1 addition & 1 deletion blocksuite/affine/block-embed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export {
LinkPreviewer,
type LinkPreviewResponseData,
} from './common/link-previewer.js';
export { getDocContentWithMaxLength } from './common/render-linked-doc';
export * from './common/render-linked-doc';
export { toEdgelessEmbedBlock } from './common/to-edgeless-embed-block';
export * from './common/utils';
export * from './embed-figma-block';
Expand Down
43 changes: 43 additions & 0 deletions blocksuite/affine/block-note/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@blocksuite/affine-block-note",
"description": "Note block for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-embed": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.75",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.1",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
]
}
52 changes: 52 additions & 0 deletions blocksuite/affine/block-note/src/effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { BlockComponent } from '@blocksuite/block-std';
import type { BlockModel } from '@blocksuite/store';

import type { updateBlockType } from './commands/block-type';
import type { dedentBlock } from './commands/dedent-block';
import type { dedentBlockToRoot } from './commands/dedent-block-to-root';
import type { dedentBlocks } from './commands/dedent-blocks';
import type { dedentBlocksToRoot } from './commands/dedent-blocks-to-root';
import type { focusBlockEnd } from './commands/focus-block-end';
import type { focusBlockStart } from './commands/focus-block-start';
import type { indentBlock } from './commands/indent-block';
import type { indentBlocks } from './commands/indent-blocks';
import type { selectBlock } from './commands/select-block';
import type { selectBlocksBetween } from './commands/select-blocks-between';
import { NoteBlockComponent } from './note-block';
import {
EdgelessNoteBlockComponent,
EdgelessNoteMask,
} from './note-edgeless-block';
import type { NoteBlockService } from './note-service';

export function effects() {
customElements.define('affine-note', NoteBlockComponent);
customElements.define('edgeless-note-mask', EdgelessNoteMask);
customElements.define('affine-edgeless-note', EdgelessNoteBlockComponent);
}

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;
dedentBlockToRoot: typeof dedentBlockToRoot;
}
interface CommandContext {
focusBlock?: BlockComponent | null;
anchorBlock?: BlockComponent | null;
updatedBlocks?: BlockModel[];
}
interface BlockServices {
'affine:note': NoteBlockService;
}
}
}
6 changes: 6 additions & 0 deletions blocksuite/affine/block-note/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './adapters';
export * from './commands';
export * from './note-block';
export * from './note-edgeless-block';
export * from './note-service';
export * from './note-spec';
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { MoreIndicatorIcon } from '@blocksuite/affine-components/icons';
import type { NoteBlockModel } from '@blocksuite/affine-model';
import {
Expand All @@ -14,7 +13,11 @@ import {
matchFlavours,
stopPropagation,
} from '@blocksuite/affine-shared/utils';
import type { BlockComponent, EditorHost } from '@blocksuite/block-std';
import type {
BlockComponent,
BlockService,
EditorHost,
} from '@blocksuite/block-std';
import { ShadowlessElement, toGfxBlockComponent } from '@blocksuite/block-std';
import {
almostEqual,
Expand All @@ -23,13 +26,12 @@ import {
Point,
WithDisposable,
} from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import type { BlockModel, Slot } 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) {
Expand Down Expand Up @@ -151,7 +153,9 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
}

get rootService() {
return this.std.getService('affine:page') as EdgelessRootService;
return this.std.getService('affine:page') as BlockService & {
slots: Record<string, Slot>;
};
}

private _collapsedContent() {
Expand Down Expand Up @@ -312,7 +316,7 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
override connectedCallback(): void {
super.connectedCallback();

const selection = this.rootService.selection;
const selection = this.gfx.selection;

this._editing = selection.has(this.model.id) && selection.editing;
this._disposables.add(
Expand All @@ -328,7 +332,7 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(

override firstUpdated() {
const { _disposables } = this;
const selection = this.rootService.selection;
const selection = this.gfx.selection;

_disposables.add(
this.rootService.slots.elementResizeStart.on(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { textConversionConfigs } from '@blocksuite/affine-components/rich-text';
import { NoteBlockSchema } from '@blocksuite/affine-model';
import { matchFlavours } from '@blocksuite/affine-shared/utils';
import {
type BaseSelection,
type BlockComponent,
type BlockSelection,
BlockService,
type BlockStdScope,
type UIEventHandler,
type UIEventStateContext,
} from '@blocksuite/block-std';
import type { BlockModel } from '@blocksuite/store';

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';
import { moveBlockConfigs } from './move-block';
import { quickActionConfig } from './quick-action';

export class NoteBlockService extends BlockService {
static override readonly flavour = NoteBlockSchema.model.flavour;
Expand Down Expand Up @@ -51,10 +52,10 @@ export class NoteBlockService extends BlockService {
return {
...acc,
[config.hotkey!]: ctx => {
if (!config.showWhen(this.std.host)) return;
if (!config.showWhen(this.std)) return;

ctx.get('defaultState').event.preventDefault();
config.action(this.std.host);
config.action(this.std);
},
};
},
Expand Down Expand Up @@ -83,8 +84,7 @@ export class NoteBlockService extends BlockService {
})
.inline((ctx, next) => {
const newModels = ctx.updatedBlocks;
const host = ctx.std.host;
if (!host || !newModels) {
if (!newModels) {
return;
}

Expand All @@ -93,7 +93,7 @@ export class NoteBlockService extends BlockService {
}

const [codeModel] = newModels;
onModelElementUpdated(host, codeModel, codeElement => {
onModelElementUpdated(ctx.std, codeModel, codeElement => {
this._std.selection.setGroup('note', [
this._std.selection.create('text', {
from: {
Expand Down Expand Up @@ -584,3 +584,19 @@ export class NoteBlockService extends BlockService {
});
}
}

async function onModelElementUpdated(
std: BlockStdScope,
model: BlockModel,
callback: (block: BlockComponent) => void
) {
const page = model.doc;
if (!page.root) return;

const rootComponent = std.view.getBlock(page.root.id);
if (!rootComponent) return;
await rootComponent.updateComplete;

const element = std.view.getBlock(model.id);
if (element) callback(element);
}
Loading
Loading