mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-05-08 22:07:32 +08:00
feat(editor): add collapse/expand functionality to code block component (#14884)
This PR fixes #14040 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Code blocks can be collapsed and expanded via a toolbar toggle (visible when the document is editable). * Collapsed code blocks show a limited preview (~8 lines) with a bottom fade overlay and reduced padding. * Toolbar button updates icon and tooltip to reflect collapsed/expanded state. * Collapse state is preserved on the block so its current collapsed/expanded setting is retained. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -51,6 +51,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
return modelPreview;
|
||||
});
|
||||
|
||||
collapsed$: Signal<boolean> = computed(
|
||||
() => !!this.model.props.collapsed$.value
|
||||
);
|
||||
|
||||
highlightTokens$: Signal<ThemedToken[][]> = signal([]);
|
||||
|
||||
languageName$: Signal<string> = computed(() => {
|
||||
@@ -417,6 +421,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
|
||||
);
|
||||
const shouldRenderPreview = preview && previewContext;
|
||||
const collapsed = this.collapsed$.value;
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -426,6 +431,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
mobile: IS_MOBILE,
|
||||
wrap: this.model.props.wrap,
|
||||
'disable-line-numbers': !showLineNumbers,
|
||||
collapsed,
|
||||
})}
|
||||
>
|
||||
<rich-text
|
||||
@@ -453,9 +459,12 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
}}
|
||||
>
|
||||
</rich-text>
|
||||
${collapsed
|
||||
? html`<div class="code-collapsed-fade" aria-hidden="true"></div>`
|
||||
: nothing}
|
||||
<div
|
||||
style=${styleMap({
|
||||
display: shouldRenderPreview ? undefined : 'none',
|
||||
display: shouldRenderPreview && !collapsed ? undefined : 'none',
|
||||
})}
|
||||
contenteditable="false"
|
||||
class="affine-code-block-preview"
|
||||
@@ -471,6 +480,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
this.store.updateBlock(this.model, { wrap });
|
||||
}
|
||||
|
||||
setCollapsed(collapsed: boolean) {
|
||||
this.store.updateBlock(this.model, { collapsed });
|
||||
}
|
||||
|
||||
@query('rich-text')
|
||||
private accessor _richTextElement: RichText | null = null;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
@@ -108,6 +109,17 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
|
||||
this.closeCurrentMenu();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Mirror the collapsed$ signal from the block component into local @state
|
||||
// so this LitElement re-renders when it changes.
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
this._collapsed = this.context.blockComponent.collapsed$.value;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<editor-toolbar class="code-toolbar-container" data-without-bg>
|
||||
@@ -136,6 +148,9 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
|
||||
@state()
|
||||
private accessor _moreMenuOpen = false;
|
||||
|
||||
@state()
|
||||
private accessor _collapsed = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor context!: CodeBlockToolbarContext;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
CancelWrapIcon,
|
||||
CaptionIcon,
|
||||
CollapseCodeIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
ExpandCodeIcon,
|
||||
WrapIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
||||
@@ -85,6 +87,38 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'collapse',
|
||||
when: ({ doc }) => !doc.readonly,
|
||||
generate: ({ blockComponent }) => {
|
||||
return {
|
||||
action: () => {
|
||||
blockComponent.setCollapsed(!blockComponent.collapsed$.value);
|
||||
},
|
||||
render: item => {
|
||||
const collapsed = blockComponent.collapsed$.value;
|
||||
const icon = collapsed ? ExpandCodeIcon : CollapseCodeIcon;
|
||||
const label = collapsed ? 'Expand code' : 'Collapse code';
|
||||
return html`
|
||||
<editor-icon-button
|
||||
class="code-toolbar-button collapse"
|
||||
aria-label=${label}
|
||||
.tooltip=${label}
|
||||
.tooltipOffset=${4}
|
||||
.iconSize=${'16px'}
|
||||
.iconContainerPadding=${4}
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
</editor-icon-button>
|
||||
`;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'caption',
|
||||
label: 'Caption',
|
||||
|
||||
@@ -80,4 +80,35 @@ export const codeBlockStyles = css`
|
||||
affine-code .affine-code-block-preview {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* ── Collapsed state ──────────────────────────────────────────────── */
|
||||
|
||||
/* Clamp the rich-text to the first 8 lines */
|
||||
.affine-code-block-container.collapsed rich-text {
|
||||
display: block;
|
||||
max-height: calc(8 * var(--affine-line-height));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Reduce bottom padding so the fade sits flush with the border */
|
||||
.affine-code-block-container.collapsed {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Gradient overlay that fades to the block background */
|
||||
.affine-code-block-container .code-collapsed-fade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
var(--affine-background-code-block)
|
||||
);
|
||||
border-radius: 0 0 10px 10px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -265,6 +265,16 @@ export const CancelWrapIcon = icons.CancelWrapIcon({
|
||||
height: '20',
|
||||
});
|
||||
|
||||
export const CollapseCodeIcon = icons.CollapseIcon({
|
||||
width: '20',
|
||||
height: '20',
|
||||
});
|
||||
|
||||
export const ExpandCodeIcon = icons.ToggleRightIcon({
|
||||
width: '20',
|
||||
height: '20',
|
||||
});
|
||||
|
||||
// Attachment
|
||||
|
||||
export const ViewIcon = icons.ViewIcon({
|
||||
|
||||
@@ -14,6 +14,7 @@ type CodeBlockProps = {
|
||||
caption: string;
|
||||
preview?: boolean;
|
||||
lineNumber?: boolean;
|
||||
collapsed?: boolean;
|
||||
comments?: Record<string, boolean>;
|
||||
} & BlockMeta;
|
||||
|
||||
@@ -27,6 +28,7 @@ export const CodeBlockSchema = defineBlockSchema({
|
||||
caption: '',
|
||||
preview: undefined,
|
||||
lineNumber: undefined,
|
||||
collapsed: undefined,
|
||||
comments: undefined,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
|
||||
Reference in New Issue
Block a user