mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 23:37:15 +08:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Press Enter inside a callout splits the paragraph at the cursor into a new focused paragraph. - Clicking an empty callout inserts and focuses a new paragraph; emoji menu behavior unchanged. - New command to convert a callout paragraph to callout/selection flow for Backspace handling. - New native API: ShareableContent.isUsingMicrophone(processId). - Bug Fixes - Backspace inside callout paragraphs now merges or deletes text predictably and selects the callout when appropriate. - Style - Callout layout refined: top-aligned content and adjusted emoji spacing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
|
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
|
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
|
|
import { type CalloutBlockModel } from '@blocksuite/affine-model';
|
|
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
|
import { EDGELESS_TOP_CONTENTEDITABLE_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/std';
|
|
import { flip, offset } from '@floating-ui/dom';
|
|
import { css, html } from 'lit';
|
|
import { query } from 'lit/decorators.js';
|
|
import { styleMap } from 'lit/directives/style-map.js';
|
|
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
|
|
static override styles = css`
|
|
:host {
|
|
display: block;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.affine-callout-block-container {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
padding: 5px 10px;
|
|
border-radius: 8px;
|
|
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
|
|
}
|
|
|
|
.affine-callout-emoji-container {
|
|
user-select: none;
|
|
font-size: 1.2em;
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-top: 10px;
|
|
margin-bottom: 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
.affine-callout-emoji:hover {
|
|
cursor: pointer;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.affine-callout-children {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding-left: 10px;
|
|
}
|
|
`;
|
|
|
|
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: { native: string }) => {
|
|
this.model.props.emoji = data.native;
|
|
}}
|
|
></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,
|
|
});
|
|
};
|
|
|
|
private readonly _handleBlockClick = (event: MouseEvent) => {
|
|
// Check if the click target is emoji related element
|
|
const target = event.target as HTMLElement;
|
|
if (
|
|
target.closest('.affine-callout-emoji-container') ||
|
|
target.classList.contains('affine-callout-emoji')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Only handle clicks when there are no children
|
|
if (this.model.children.length > 0) {
|
|
return;
|
|
}
|
|
|
|
// Prevent event bubbling
|
|
event.stopPropagation();
|
|
|
|
// Create a new paragraph block
|
|
const paragraphId = this.store.addBlock('affine:paragraph', {}, this.model);
|
|
|
|
// Focus the new paragraph
|
|
focusTextModel(this.std, paragraphId);
|
|
};
|
|
|
|
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>(
|
|
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
|
|
);
|
|
}
|
|
return this.rootComponent;
|
|
}
|
|
|
|
override renderBlock() {
|
|
const emoji = this.model.props.emoji$.value;
|
|
return html`
|
|
<div
|
|
class="affine-callout-block-container"
|
|
@click=${this._handleBlockClick}
|
|
>
|
|
<div
|
|
@click=${this._toggleEmojiMenu}
|
|
contenteditable="false"
|
|
class="affine-callout-emoji-container"
|
|
style=${styleMap({
|
|
display: emoji.length === 0 ? 'none' : undefined,
|
|
})}
|
|
>
|
|
<span class="affine-callout-emoji">${emoji}</span>
|
|
</div>
|
|
<div class="affine-callout-children">
|
|
${this.renderChildren(this.model)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|