mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
feat(editor): add callout block (#10563)
- Add `CalloutBlockModel ` - Implement `CalloutBlockComponent ` - Integrate with slash menu (/)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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`),
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './callout-block.js';
|
||||
export * from './callout-spec.js';
|
||||
export * from './effects.js';
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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" },
|
||||
|
||||
+5
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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