feat(editor): add callout block (#10563)
- Add `CalloutBlockModel ` - Implement `CalloutBlockComponent ` - Integrate with slash menu (/)
This commit is contained in:
45
blocksuite/affine/block-callout/package.json
Normal file
45
blocksuite/affine/block-callout/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-block-callout",
|
||||
"description": "Callout 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-components": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/block-std": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"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__"
|
||||
],
|
||||
"version": "0.20.0"
|
||||
}
|
||||
121
blocksuite/affine/block-callout/src/callout-block.ts
Normal file
121
blocksuite/affine/block-callout/src/callout-block.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { DefaultInlineManagerExtension } from '@blocksuite/affine-components/rich-text';
|
||||
import { type CalloutBlockModel } from '@blocksuite/affine-model';
|
||||
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
DocModeProvider,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { css, html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
|
||||
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.affine-callout-block-container {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
|
||||
}
|
||||
|
||||
.affine-callout-emoji-container {
|
||||
margin-right: 12px;
|
||||
margin-top: 10px;
|
||||
user-select: none;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.affine-callout-emoji:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.affine-callout-children {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
private _emojiMenuAbortController: AbortController | null = null;
|
||||
private readonly _toggleEmojiMenu = () => {
|
||||
if (this._emojiMenuAbortController) {
|
||||
this._emojiMenuAbortController.abort();
|
||||
}
|
||||
this._emojiMenuAbortController = new AbortController();
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
|
||||
createLitPortal({
|
||||
template: html`<affine-emoji-menu
|
||||
.theme=${theme}
|
||||
.onEmojiSelect=${(data: any) => {
|
||||
this.model.emoji = data.native;
|
||||
console.log(data);
|
||||
}}
|
||||
></affine-emoji-menu>`,
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
container: this.host,
|
||||
computePosition: {
|
||||
referenceElement: this._emojiButton,
|
||||
placement: 'bottom-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
autoUpdate: { animationFrame: true },
|
||||
},
|
||||
abortController: this._emojiMenuAbortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
};
|
||||
|
||||
get attributeRenderer() {
|
||||
return this.inlineManager.getRenderer();
|
||||
}
|
||||
|
||||
get attributesSchema() {
|
||||
return this.inlineManager.getSchema();
|
||||
}
|
||||
|
||||
get embedChecker() {
|
||||
return this.inlineManager.embedChecker;
|
||||
}
|
||||
|
||||
get inlineManager() {
|
||||
return this.std.get(DefaultInlineManagerExtension.identifier);
|
||||
}
|
||||
|
||||
@query('.affine-callout-emoji')
|
||||
private accessor _emojiButton!: HTMLElement;
|
||||
|
||||
override get topContenteditableElement() {
|
||||
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
||||
return this.closest<BlockComponent>(NOTE_SELECTOR);
|
||||
}
|
||||
return this.rootComponent;
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="affine-callout-block-container">
|
||||
<div
|
||||
@click=${this._toggleEmojiMenu}
|
||||
contenteditable="false"
|
||||
class="affine-callout-emoji-container"
|
||||
>
|
||||
<span class="affine-callout-emoji">${this.model.emoji}</span>
|
||||
</div>
|
||||
<div class="affine-callout-children">
|
||||
${this.renderChildren(this.model)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
8
blocksuite/affine/block-callout/src/callout-spec.ts
Normal file
8
blocksuite/affine/block-callout/src/callout-spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
export const CalloutBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:callout'),
|
||||
BlockViewExtension('affine:callout', literal`affine-callout`),
|
||||
];
|
||||
14
blocksuite/affine/block-callout/src/effects.ts
Normal file
14
blocksuite/affine/block-callout/src/effects.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CalloutBlockComponent } from './callout-block';
|
||||
import { EmojiMenu } from './emoji-menu';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-callout', CalloutBlockComponent);
|
||||
customElements.define('affine-emoji-menu', EmojiMenu);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-callout': CalloutBlockComponent;
|
||||
'affine-emoji-menu': EmojiMenu;
|
||||
}
|
||||
}
|
||||
34
blocksuite/affine/block-callout/src/emoji-menu.ts
Normal file
34
blocksuite/affine/block-callout/src/emoji-menu.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import data from '@emoji-mart/data';
|
||||
import { Picker } from 'emoji-mart';
|
||||
import { html, LitElement, type PropertyValues } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
|
||||
export class EmojiMenu extends WithDisposable(LitElement) {
|
||||
override firstUpdated(props: PropertyValues) {
|
||||
const result = super.firstUpdated(props);
|
||||
|
||||
const picker = new Picker({
|
||||
data,
|
||||
onEmojiSelect: this.onEmojiSelect,
|
||||
autoFocus: true,
|
||||
theme: this.theme,
|
||||
});
|
||||
this.emojiMenu.append(picker as unknown as Node);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmojiSelect: (data: any) => void = () => {};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme: 'light' | 'dark' = 'light';
|
||||
|
||||
@query('.affine-emoji-menu')
|
||||
accessor emojiMenu!: HTMLElement;
|
||||
|
||||
override render() {
|
||||
return html`<div class="affine-emoji-menu"></div>`;
|
||||
}
|
||||
}
|
||||
3
blocksuite/affine/block-callout/src/index.ts
Normal file
3
blocksuite/affine/block-callout/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './callout-block.js';
|
||||
export * from './callout-spec.js';
|
||||
export * from './effects.js';
|
||||
18
blocksuite/affine/block-callout/tsconfig.json
Normal file
18
blocksuite/affine/block-callout/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../components" },
|
||||
{ "path": "../model" },
|
||||
{ "path": "../shared" },
|
||||
{ "path": "../../framework/block-std" },
|
||||
{ "path": "../../framework/global" },
|
||||
{ "path": "../../framework/inline" },
|
||||
{ "path": "../../framework/store" }
|
||||
]
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
textKeymap,
|
||||
} from '@blocksuite/affine-components/rich-text';
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
@@ -40,7 +41,12 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
|
||||
const { store } = std;
|
||||
const model = store.getBlock(text.from.blockId)?.model;
|
||||
if (!model || !matchModels(model, [ParagraphBlockModel])) return;
|
||||
if (
|
||||
!model ||
|
||||
!matchModels(model, [ParagraphBlockModel]) ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
|
||||
const event = ctx.get('defaultState').event;
|
||||
event.preventDefault();
|
||||
@@ -71,7 +77,12 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (!text) return;
|
||||
const model = store.getBlock(text.from.blockId)?.model;
|
||||
if (!model || !matchModels(model, [ParagraphBlockModel])) return;
|
||||
if (
|
||||
!model ||
|
||||
!matchModels(model, [ParagraphBlockModel]) ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
const inlineEditor = getInlineEditorByModel(
|
||||
std.host,
|
||||
text.from.blockId
|
||||
@@ -98,16 +109,21 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (!text) return;
|
||||
const model = store.getBlock(text.from.blockId)?.model;
|
||||
if (!model || !matchModels(model, [ParagraphBlockModel])) return;
|
||||
if (
|
||||
!model ||
|
||||
!matchModels(model, [ParagraphBlockModel]) ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
const inlineEditor = getInlineEditorByModel(
|
||||
std.host,
|
||||
text.from.blockId
|
||||
);
|
||||
const range = inlineEditor?.getInlineRange();
|
||||
if (!range || !inlineEditor) return;
|
||||
const inlineRange = inlineEditor?.getInlineRange();
|
||||
if (!inlineRange || !inlineEditor) return;
|
||||
|
||||
const raw = ctx.get('keyboardState').raw;
|
||||
const isEnd = model.text.length === range.index;
|
||||
const isEnd = model.text.length === inlineRange.index;
|
||||
|
||||
if (model.type === 'quote') {
|
||||
const textStr = model.text.toString();
|
||||
@@ -129,7 +145,7 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
if (isEnd && endWithTwoBlankLines) {
|
||||
raw.preventDefault();
|
||||
store.captureSync();
|
||||
model.text.delete(range.index - 1, 1);
|
||||
model.text.delete(inlineRange.index - 1, 1);
|
||||
std.command.chain().pipe(addParagraphCommand).run();
|
||||
return true;
|
||||
}
|
||||
@@ -149,7 +165,7 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
if (index === -1) return true;
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
|
||||
const rightText = model.text.split(range.index);
|
||||
const rightText = model.text.split(inlineRange.index);
|
||||
const newId = store.addBlock(
|
||||
model.flavour,
|
||||
{ type: model.type, text: rightText },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
CalloutBlockModel,
|
||||
CodeBlockModel,
|
||||
DatabaseBlockModel,
|
||||
DividerBlockModel,
|
||||
@@ -25,7 +26,12 @@ export function forwardDelete(std: BlockStdScope) {
|
||||
if (!text) return;
|
||||
const isCollapsed = text.isCollapsed();
|
||||
const model = store.getBlock(text.from.blockId)?.model;
|
||||
if (!model || !matchModels(model, [ParagraphBlockModel])) return;
|
||||
if (
|
||||
!model ||
|
||||
!matchModels(model, [ParagraphBlockModel]) ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
const isEnd = isCollapsed && text.from.index === model.text.length;
|
||||
if (!isEnd) return;
|
||||
const parent = store.getParent(model);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { insertImagesCommand } from '@blocksuite/affine-block-image';
|
||||
import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex';
|
||||
import { focusBlockEnd } from '@blocksuite/affine-block-note';
|
||||
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
|
||||
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
|
||||
import { insertTableBlockCommand } from '@blocksuite/affine-block-table';
|
||||
@@ -61,6 +62,7 @@ import { assertType } from '@blocksuite/global/utils';
|
||||
import {
|
||||
DualLinkIcon,
|
||||
ExportToPdfIcon,
|
||||
FontIcon,
|
||||
FrameIcon,
|
||||
GroupingIcon,
|
||||
ImageIcon,
|
||||
@@ -171,6 +173,37 @@ export const defaultSlashMenuConfig: SlashMenuConfig = {
|
||||
!insideEdgelessText(model),
|
||||
})),
|
||||
|
||||
{
|
||||
name: 'Callout',
|
||||
description: 'Let your words stand out.',
|
||||
icon: FontIcon(),
|
||||
alias: ['callout'],
|
||||
showWhen: ({ model }) => {
|
||||
return model.doc.get(FeatureFlagService).getFlag('enable_callout');
|
||||
},
|
||||
action: ({ model, rootComponent }) => {
|
||||
const { doc } = model;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
if (index === -1) return;
|
||||
const calloutId = doc.addBlock('affine:callout', {}, parent, index + 1);
|
||||
if (!calloutId) return;
|
||||
const paragraphId = doc.addBlock('affine:paragraph', {}, calloutId);
|
||||
if (!paragraphId) return;
|
||||
rootComponent.updateComplete
|
||||
.then(() => {
|
||||
const paragraph = rootComponent.std.view.getBlock(paragraphId);
|
||||
if (!paragraph) return;
|
||||
rootComponent.std.command.exec(focusBlockEnd, {
|
||||
focusBlock: paragraph,
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Inline equation',
|
||||
description: 'Create a equation block.',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CodeBlockModel, ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
CodeBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
isHorizontalRuleMarkdown,
|
||||
isMarkdownPrefix,
|
||||
@@ -37,7 +41,13 @@ export function markdownInput(
|
||||
const isHeading = isParagraph && model.type.startsWith('h');
|
||||
const isParagraphQuoteBlock = isParagraph && model.type === 'quote';
|
||||
const isCodeBlock = matchModels(model, [CodeBlockModel]);
|
||||
if (isHeading || isParagraphQuoteBlock || isCodeBlock) return;
|
||||
if (
|
||||
isHeading ||
|
||||
isParagraphQuoteBlock ||
|
||||
isCodeBlock ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
|
||||
const lineInfo = inline.getLine(range.index);
|
||||
if (!lineInfo) return;
|
||||
|
||||
39
blocksuite/affine/model/src/blocks/callout/callout-model.ts
Normal file
39
blocksuite/affine/model/src/blocks/callout/callout-model.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
BlockModel,
|
||||
BlockSchemaExtension,
|
||||
defineBlockSchema,
|
||||
type Text,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
export const CalloutBlockSchema = defineBlockSchema({
|
||||
flavour: 'affine:callout',
|
||||
props: internal => ({
|
||||
emoji: '😀',
|
||||
text: internal.Text(),
|
||||
}),
|
||||
metadata: {
|
||||
version: 1,
|
||||
role: 'hub',
|
||||
parent: [
|
||||
'affine:note',
|
||||
'affine:database',
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:edgeless-text',
|
||||
],
|
||||
children: ['affine:paragraph'],
|
||||
},
|
||||
toModel: () => new CalloutBlockModel(),
|
||||
});
|
||||
|
||||
export type CalloutProps = {
|
||||
emoji: string;
|
||||
text: Text;
|
||||
};
|
||||
|
||||
export class CalloutBlockModel extends BlockModel<CalloutProps> {
|
||||
override text!: Text;
|
||||
}
|
||||
|
||||
export const CalloutBlockSchemaExtension =
|
||||
BlockSchemaExtension(CalloutBlockSchema);
|
||||
1
blocksuite/affine/model/src/blocks/callout/index.ts
Normal file
1
blocksuite/affine/model/src/blocks/callout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './callout-model.js';
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './attachment/index.js';
|
||||
export * from './bookmark/index.js';
|
||||
export * from './callout/index.js';
|
||||
export * from './code/index.js';
|
||||
export * from './database/index.js';
|
||||
export * from './divider/index.js';
|
||||
|
||||
@@ -88,6 +88,7 @@ export const NoteBlockSchema = defineBlockSchema({
|
||||
'affine:surface-ref',
|
||||
'affine:embed-*',
|
||||
'affine:latex',
|
||||
'affine:callout',
|
||||
TableModelFlavour,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:edgeless-text',
|
||||
'affine:callout',
|
||||
],
|
||||
},
|
||||
toModel: () => new ParagraphBlockModel(),
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface BlockSuiteFlags {
|
||||
enable_mobile_keyboard_toolbar: boolean;
|
||||
enable_mobile_linked_doc_menu: boolean;
|
||||
enable_block_meta: boolean;
|
||||
enable_callout: boolean;
|
||||
}
|
||||
|
||||
export class FeatureFlagService extends StoreExtension {
|
||||
@@ -38,6 +39,7 @@ export class FeatureFlagService extends StoreExtension {
|
||||
enable_mobile_keyboard_toolbar: false,
|
||||
enable_mobile_linked_doc_menu: false,
|
||||
enable_block_meta: false,
|
||||
enable_callout: false,
|
||||
});
|
||||
|
||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-callout": "workspace:*",
|
||||
"@blocksuite/affine-block-list": "workspace:*",
|
||||
"@blocksuite/affine-block-note": "workspace:*",
|
||||
"@blocksuite/affine-block-paragraph": "workspace:*",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type CalloutBlockComponent } from '@blocksuite/affine-block-callout';
|
||||
import {
|
||||
AFFINE_EDGELESS_NOTE,
|
||||
type EdgelessNoteBlockComponent,
|
||||
@@ -6,7 +7,7 @@ import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||
import {
|
||||
DatabaseBlockModel,
|
||||
ListBlockModel,
|
||||
type ParagraphBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
@@ -209,6 +210,14 @@ export const getClosestBlockByPoint = (
|
||||
return null;
|
||||
}
|
||||
|
||||
if (matchModels(closestBlock.model, [ParagraphBlockModel])) {
|
||||
const callout =
|
||||
closestBlock.closest<CalloutBlockComponent>('affine-callout');
|
||||
if (callout) {
|
||||
return callout;
|
||||
}
|
||||
}
|
||||
|
||||
return closestBlock;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../block-callout" },
|
||||
{ "path": "../block-list" },
|
||||
{ "path": "../block-note" },
|
||||
{ "path": "../block-paragraph" },
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-attachment": "workspace:*",
|
||||
"@blocksuite/affine-block-bookmark": "workspace:*",
|
||||
"@blocksuite/affine-block-callout": "workspace:*",
|
||||
"@blocksuite/affine-block-code": "workspace:*",
|
||||
"@blocksuite/affine-block-data-view": "workspace:*",
|
||||
"@blocksuite/affine-block-database": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { effects as blockAttachmentEffects } from '@blocksuite/affine-block-attachment/effects';
|
||||
import { effects as blockBookmarkEffects } from '@blocksuite/affine-block-bookmark/effects';
|
||||
import { effects as blockCalloutEffects } from '@blocksuite/affine-block-callout/effects';
|
||||
import { effects as blockCodeEffects } from '@blocksuite/affine-block-code/effects';
|
||||
import { effects as blockDataViewEffects } from '@blocksuite/affine-block-data-view/effects';
|
||||
import { effects as blockDatabaseEffects } from '@blocksuite/affine-block-database/effects';
|
||||
@@ -72,6 +73,7 @@ export function effects() {
|
||||
blockCodeEffects();
|
||||
blockTableEffects();
|
||||
blockRootEffects();
|
||||
blockCalloutEffects();
|
||||
|
||||
componentCaptionEffects();
|
||||
componentContextMenuEffects();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
|
||||
import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
|
||||
import { CalloutBlockSpec } from '@blocksuite/affine-block-callout';
|
||||
import { CodeBlockSpec } from '@blocksuite/affine-block-code';
|
||||
import { DataViewBlockSpec } from '@blocksuite/affine-block-data-view';
|
||||
import { DatabaseBlockSpec } from '@blocksuite/affine-block-database';
|
||||
@@ -55,6 +56,7 @@ export const CommonBlockSpecs: ExtensionType[] = [
|
||||
ParagraphBlockSpec,
|
||||
DefaultOpenDocExtension,
|
||||
FontLoaderService,
|
||||
CalloutBlockSpec,
|
||||
].flat();
|
||||
|
||||
export const PageFirstPartyBlockSpecs: ExtensionType[] = [
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TableSelectionExtension } from '@blocksuite/affine-block-table';
|
||||
import {
|
||||
AttachmentBlockSchemaExtension,
|
||||
BookmarkBlockSchemaExtension,
|
||||
CalloutBlockSchemaExtension,
|
||||
CodeBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
DividerBlockSchemaExtension,
|
||||
@@ -78,6 +79,7 @@ export const StoreExtensions: ExtensionType[] = [
|
||||
EdgelessTextBlockSchemaExtension,
|
||||
LatexBlockSchemaExtension,
|
||||
TableBlockSchemaExtension,
|
||||
CalloutBlockSchemaExtension,
|
||||
|
||||
BlockSelectionExtension,
|
||||
TextSelectionExtension,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SurfaceBlockSchema } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
AttachmentBlockSchema,
|
||||
BookmarkBlockSchema,
|
||||
CalloutBlockSchema,
|
||||
CodeBlockSchema,
|
||||
DatabaseBlockSchema,
|
||||
DividerBlockSchema,
|
||||
@@ -54,4 +55,5 @@ export const AffineSchemas: z.infer<typeof BlockSchema>[] = [
|
||||
EdgelessTextBlockSchema,
|
||||
LatexBlockSchema,
|
||||
TableBlockSchema,
|
||||
CalloutBlockSchema,
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"references": [
|
||||
{ "path": "../affine/block-attachment" },
|
||||
{ "path": "../affine/block-bookmark" },
|
||||
{ "path": "../affine/block-callout" },
|
||||
{ "path": "../affine/block-code" },
|
||||
{ "path": "../affine/block-data-view" },
|
||||
{ "path": "../affine/block-database" },
|
||||
|
||||
@@ -17,6 +17,11 @@ const extensions = StoreExtensions;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
||||
vi.mock('emoji-mart', () => {
|
||||
return {
|
||||
Picker: vi.fn(),
|
||||
};
|
||||
});
|
||||
docCollection = new TestWorkspace({ id: 'test' });
|
||||
docCollection.meta.initialize();
|
||||
const initPage = async (page: Store) => {
|
||||
|
||||
@@ -113,6 +113,16 @@ export const AFFINE_FLAGS = {
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_callout: {
|
||||
category: 'blocksuite',
|
||||
bsFlag: 'enable_callout',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-callout.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-callout.description',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: false,
|
||||
},
|
||||
enable_emoji_folder_icon: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
import { JOURNAL_DATE_FORMAT } from '@affine/core/modules/journal';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import dayjs from 'dayjs';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('emoji-mart', () => {
|
||||
return {
|
||||
Picker: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { suggestJournalDate } from '../suggest-journal-date';
|
||||
|
||||
|
||||
@@ -5391,6 +5391,14 @@ export function useAFFiNEI18N(): {
|
||||
* `Once enabled, all blocks will have created time, updated time, created by and updated by.`
|
||||
*/
|
||||
["com.affine.settings.workspace.experimental-features.enable-block-meta.description"](): string;
|
||||
/**
|
||||
* `Callout`
|
||||
*/
|
||||
["com.affine.settings.workspace.experimental-features.enable-callout.name"](): string;
|
||||
/**
|
||||
* `Let your words stand out.`
|
||||
*/
|
||||
["com.affine.settings.workspace.experimental-features.enable-callout.description"](): string;
|
||||
/**
|
||||
* `Emoji Folder Icon`
|
||||
*/
|
||||
|
||||
@@ -1345,6 +1345,8 @@
|
||||
"com.affine.settings.workspace.experimental-features.enable-mind-map-import.description": "Enables mind map import.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-block-meta.name": "Block Meta",
|
||||
"com.affine.settings.workspace.experimental-features.enable-block-meta.description": "Once enabled, all blocks will have created time, updated time, created by and updated by.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-callout.name": "Callout",
|
||||
"com.affine.settings.workspace.experimental-features.enable-callout.description": "Let your words stand out.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name": "Emoji Folder Icon",
|
||||
"com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.description": "Once enabled, you can use an emoji as the folder icon. When the first character of the folder name is an emoji, it will be extracted and used as its icon.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.name": "Emoji Doc Icon",
|
||||
|
||||
43
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
43
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import { type } from '@affine-test/kit/utils/page-logic';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('add callout block using slash menu and change emoji', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await page.getByTestId('settings-modal-trigger').click();
|
||||
await page.getByText('Experimental features').click();
|
||||
await page.getByText('I am aware of the risks, and').click();
|
||||
await page.getByTestId('experimental-confirm-button').click();
|
||||
await page.getByTestId('enable_callout').locator('span').click();
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
await page.getByTestId('sidebar-new-page-button').click();
|
||||
await page.locator('affine-paragraph v-line div').click();
|
||||
|
||||
await type(page, '/callout\naaaa\nbbbb');
|
||||
const callout = page.locator('affine-callout');
|
||||
const emoji = page.locator('affine-callout .affine-callout-emoji');
|
||||
await expect(callout).toBeVisible();
|
||||
await expect(emoji).toContainText('😀');
|
||||
|
||||
const paragraph = page.locator('affine-callout affine-paragraph');
|
||||
await expect(paragraph).toHaveCount(1);
|
||||
|
||||
const vLine = page.locator('affine-callout v-line');
|
||||
await expect(vLine).toHaveCount(2);
|
||||
expect(await vLine.nth(0).innerText()).toBe('aaaa');
|
||||
expect(await vLine.nth(1).innerText()).toBe('bbbb');
|
||||
|
||||
await emoji.click();
|
||||
const emojiMenu = page.locator('affine-emoji-menu');
|
||||
await expect(emojiMenu).toBeVisible();
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^😀😃😄😁😆😅🤣😂🙂$/ })
|
||||
.getByLabel('😆')
|
||||
.click();
|
||||
await page.getByTestId('page-editor-blank').click();
|
||||
await expect(emojiMenu).not.toBeVisible();
|
||||
await expect(emoji).toContainText('😆');
|
||||
});
|
||||
@@ -42,6 +42,19 @@ export const PackageList = [
|
||||
'blocksuite/framework/store',
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'blocksuite/affine/block-callout',
|
||||
name: '@blocksuite/affine-block-callout',
|
||||
workspaceDependencies: [
|
||||
'blocksuite/affine/components',
|
||||
'blocksuite/affine/model',
|
||||
'blocksuite/affine/shared',
|
||||
'blocksuite/framework/block-std',
|
||||
'blocksuite/framework/global',
|
||||
'blocksuite/framework/inline',
|
||||
'blocksuite/framework/store',
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'blocksuite/affine/block-code',
|
||||
name: '@blocksuite/affine-block-code',
|
||||
@@ -361,6 +374,7 @@ export const PackageList = [
|
||||
location: 'blocksuite/affine/widget-drag-handle',
|
||||
name: '@blocksuite/affine-widget-drag-handle',
|
||||
workspaceDependencies: [
|
||||
'blocksuite/affine/block-callout',
|
||||
'blocksuite/affine/block-list',
|
||||
'blocksuite/affine/block-note',
|
||||
'blocksuite/affine/block-paragraph',
|
||||
@@ -426,6 +440,7 @@ export const PackageList = [
|
||||
workspaceDependencies: [
|
||||
'blocksuite/affine/block-attachment',
|
||||
'blocksuite/affine/block-bookmark',
|
||||
'blocksuite/affine/block-callout',
|
||||
'blocksuite/affine/block-code',
|
||||
'blocksuite/affine/block-data-view',
|
||||
'blocksuite/affine/block-database',
|
||||
@@ -816,6 +831,7 @@ export type PackageName =
|
||||
| '@blocksuite/affine'
|
||||
| '@blocksuite/affine-block-attachment'
|
||||
| '@blocksuite/affine-block-bookmark'
|
||||
| '@blocksuite/affine-block-callout'
|
||||
| '@blocksuite/affine-block-code'
|
||||
| '@blocksuite/affine-block-data-view'
|
||||
| '@blocksuite/affine-block-database'
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
{ "path": "./blocksuite/affine/all" },
|
||||
{ "path": "./blocksuite/affine/block-attachment" },
|
||||
{ "path": "./blocksuite/affine/block-bookmark" },
|
||||
{ "path": "./blocksuite/affine/block-callout" },
|
||||
{ "path": "./blocksuite/affine/block-code" },
|
||||
{ "path": "./blocksuite/affine/block-data-view" },
|
||||
{ "path": "./blocksuite/affine/block-database" },
|
||||
|
||||
42
yarn.lock
42
yarn.lock
@@ -2244,6 +2244,30 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@blocksuite/affine-block-callout@workspace:*, @blocksuite/affine-block-callout@workspace:blocksuite/affine/block-callout":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@blocksuite/affine-block-callout@workspace:blocksuite/affine/block-callout"
|
||||
dependencies:
|
||||
"@blocksuite/affine-components": "workspace:*"
|
||||
"@blocksuite/affine-model": "workspace:*"
|
||||
"@blocksuite/affine-shared": "workspace:*"
|
||||
"@blocksuite/block-std": "workspace:*"
|
||||
"@blocksuite/global": "workspace:*"
|
||||
"@blocksuite/inline": "workspace:*"
|
||||
"@blocksuite/store": "workspace:*"
|
||||
"@emoji-mart/data": "npm:^1.2.1"
|
||||
"@floating-ui/dom": "npm:^1.6.10"
|
||||
"@lit/context": "npm:^1.1.2"
|
||||
"@preact/signals-core": "npm:^1.8.0"
|
||||
"@toeverything/theme": "npm:^1.1.12"
|
||||
"@types/mdast": "npm:^4.0.4"
|
||||
emoji-mart: "npm:^5.6.0"
|
||||
lit: "npm:^3.2.0"
|
||||
minimatch: "npm:^10.0.1"
|
||||
zod: "npm:^3.23.8"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@blocksuite/affine-block-code@workspace:*, @blocksuite/affine-block-code@workspace:blocksuite/affine/block-code":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@blocksuite/affine-block-code@workspace:blocksuite/affine/block-code"
|
||||
@@ -2811,6 +2835,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@blocksuite/affine-widget-drag-handle@workspace:blocksuite/affine/widget-drag-handle"
|
||||
dependencies:
|
||||
"@blocksuite/affine-block-callout": "workspace:*"
|
||||
"@blocksuite/affine-block-list": "workspace:*"
|
||||
"@blocksuite/affine-block-note": "workspace:*"
|
||||
"@blocksuite/affine-block-paragraph": "workspace:*"
|
||||
@@ -2944,6 +2969,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@blocksuite/affine-block-attachment": "workspace:*"
|
||||
"@blocksuite/affine-block-bookmark": "workspace:*"
|
||||
"@blocksuite/affine-block-callout": "workspace:*"
|
||||
"@blocksuite/affine-block-code": "workspace:*"
|
||||
"@blocksuite/affine-block-data-view": "workspace:*"
|
||||
"@blocksuite/affine-block-database": "workspace:*"
|
||||
@@ -4223,6 +4249,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emoji-mart/data@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@emoji-mart/data@npm:1.2.1"
|
||||
checksum: 10/a9f50edaf354aadfede604fb26d80055a085e9160db2c924fd5e6afc27033cd5beb0006a9ee48240ce9c543e58e1bf1cf9ed83baba5db83a395154984b30bd91
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emotion/babel-plugin@npm:^11.13.5":
|
||||
version: 11.13.5
|
||||
resolution: "@emotion/babel-plugin@npm:11.13.5"
|
||||
@@ -4864,7 +4897,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/dom@npm:^1.0.0, @floating-ui/dom@npm:^1.6.12, @floating-ui/dom@npm:^1.6.13":
|
||||
"@floating-ui/dom@npm:^1.0.0, @floating-ui/dom@npm:^1.6.10, @floating-ui/dom@npm:^1.6.12, @floating-ui/dom@npm:^1.6.13":
|
||||
version: 1.6.13
|
||||
resolution: "@floating-ui/dom@npm:1.6.13"
|
||||
dependencies:
|
||||
@@ -19225,6 +19258,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-mart@npm:^5.6.0":
|
||||
version: 5.6.0
|
||||
resolution: "emoji-mart@npm:5.6.0"
|
||||
checksum: 10/fbbd6ce6fe6bc30020a7de4bfd6375f3c00da9880a147fdbee37303d985856e5463a48a61177166ad056a355e0bd589df9c0866bd54f9c065d60a9efa80639dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-regex-xs@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "emoji-regex-xs@npm:1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user