mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
refactor(editor): move gfx frame title to widget (#11021)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import type { AffineFrameTitle } from './frame-title.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
|
||||
private get _frames() {
|
||||
return Object.values(this.doc.blocks.value)
|
||||
.map(({ model }) => model)
|
||||
.filter(model => model instanceof FrameBlockModel);
|
||||
}
|
||||
|
||||
getFrameTitle(frame: FrameBlockModel | string) {
|
||||
const id = typeof frame === 'string' ? frame : frame.id;
|
||||
const frameTitle = this.shadowRoot?.querySelector(
|
||||
`affine-frame-title[data-id="${id}"]`
|
||||
) as AffineFrameTitle | null;
|
||||
return frameTitle;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
this._frames,
|
||||
({ id }) => id,
|
||||
frame =>
|
||||
html`<affine-frame-title
|
||||
.model=${frame}
|
||||
data-id=${frame.id}
|
||||
></affine-frame-title>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const frameTitleWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
|
||||
);
|
||||
@@ -0,0 +1,186 @@
|
||||
import { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import type { RichText } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
type BlockComponent,
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
type AffineFrameTitleWidget,
|
||||
} from './affine-frame-title-widget';
|
||||
import { frameTitleStyleVars } from './styles';
|
||||
|
||||
export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
ShadowlessElement
|
||||
) {
|
||||
static override styles = css`
|
||||
.frame-title-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform-origin: top left;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
padding: 0 4px;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--affine-primary-color);
|
||||
box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3);
|
||||
overflow: hidden;
|
||||
font-family: var(--affine-font-family);
|
||||
}
|
||||
`;
|
||||
|
||||
get editorHost() {
|
||||
return this.edgeless.host;
|
||||
}
|
||||
|
||||
get inlineEditor() {
|
||||
return this.richText?.inlineEditor;
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.edgeless.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.gfx.selection;
|
||||
}
|
||||
|
||||
private _unmount() {
|
||||
// dispose in advance to avoid execute `this.remove()` twice
|
||||
this.disposables.dispose();
|
||||
this.selection.set({
|
||||
elements: [],
|
||||
editing: false,
|
||||
});
|
||||
this.remove();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const dispatcher = this.edgeless.std.event;
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.inlineEditor) return;
|
||||
|
||||
this.inlineEditor.selectAll();
|
||||
|
||||
this.inlineEditor.slots.renderComplete.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.disposables.add(
|
||||
dispatcher.add('keyDown', ctx => {
|
||||
const state = ctx.get('keyboardState');
|
||||
if (state.raw.key === 'Enter' && !state.raw.isComposing) {
|
||||
this._unmount();
|
||||
return true;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
return false;
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
this.gfx.viewport.viewportUpdated.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(dispatcher.add('click', () => true));
|
||||
this.disposables.add(dispatcher.add('doubleClick', () => true));
|
||||
|
||||
if (!this.inlineEditor.rootElement) return;
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditor.rootElement,
|
||||
'blur',
|
||||
() => {
|
||||
this._unmount();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const rootBlockId = this.editorHost.doc.root?.id;
|
||||
if (!rootBlockId) return nothing;
|
||||
|
||||
const viewport = this.gfx.viewport;
|
||||
const bound = Bound.deserialize(this.frameModel.xywh);
|
||||
const [x, y] = viewport.toViewCoord(bound.x, bound.y);
|
||||
const isInner = this.gfx.grid.has(
|
||||
this.frameModel.elementBound,
|
||||
true,
|
||||
true,
|
||||
model => model !== this.frameModel && model instanceof FrameBlockModel
|
||||
);
|
||||
|
||||
const frameTitleWidget = this.edgeless.std.view.getWidget(
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
rootBlockId
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
if (!frameTitleWidget) return nothing;
|
||||
|
||||
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
|
||||
|
||||
const colors = frameTitle?.colors ?? {
|
||||
background: cssVarV2('edgeless/frame/background/white'),
|
||||
text: 'var(--affine-text-primary-color)',
|
||||
};
|
||||
|
||||
const inlineEditorStyle = styleMap({
|
||||
fontSize: frameTitleStyleVars.fontSize + 'px',
|
||||
position: 'absolute',
|
||||
left: (isInner ? x + 4 : x) + 'px',
|
||||
top: (isInner ? y + 4 : y - (frameTitleStyleVars.height + 8 / 2)) + 'px',
|
||||
minWidth: '8px',
|
||||
height: frameTitleStyleVars.height + 'px',
|
||||
background: colors.background,
|
||||
color: colors.text,
|
||||
});
|
||||
|
||||
const richTextStyle = styleMap({
|
||||
height: 'fit-content',
|
||||
});
|
||||
|
||||
return html`<div class="frame-title-editor" style=${inlineEditorStyle}>
|
||||
<rich-text
|
||||
.yText=${this.frameModel.props.title.yText}
|
||||
.enableFormat=${false}
|
||||
.enableAutoScrollHorizontally=${false}
|
||||
style=${richTextStyle}
|
||||
></rich-text>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: BlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frameModel!: FrameBlockModel;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText: RichText | null = null;
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
AffineFrameTitleWidget,
|
||||
} from './affine-frame-title-widget.js';
|
||||
import { EdgelessFrameTitleEditor } from './edgeless-frame-title-editor.js';
|
||||
import { AFFINE_FRAME_TITLE, AffineFrameTitle } from './frame-title.js';
|
||||
import { AFFINE_FRAME_TITLE_WIDGET, AffineFrameTitleWidget } from './index.js';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(AFFINE_FRAME_TITLE_WIDGET, AffineFrameTitleWidget);
|
||||
customElements.define(AFFINE_FRAME_TITLE, AffineFrameTitle);
|
||||
customElements.define(
|
||||
'edgeless-frame-title-editor',
|
||||
EdgelessFrameTitleEditor
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,4 @@
|
||||
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import type { AffineFrameTitle } from './frame-title.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
|
||||
private get _frames() {
|
||||
return Object.values(this.doc.blocks.value)
|
||||
.map(({ model }) => model)
|
||||
.filter(model => model instanceof FrameBlockModel);
|
||||
}
|
||||
|
||||
getFrameTitle(frame: FrameBlockModel | string) {
|
||||
const id = typeof frame === 'string' ? frame : frame.id;
|
||||
const frameTitle = this.shadowRoot?.querySelector(
|
||||
`affine-frame-title[data-id="${id}"]`
|
||||
) as AffineFrameTitle | null;
|
||||
return frameTitle;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
this._frames,
|
||||
({ id }) => id,
|
||||
frame =>
|
||||
html`<affine-frame-title
|
||||
.model=${frame}
|
||||
data-id=${frame.id}
|
||||
></affine-frame-title>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export * from './affine-frame-title-widget.js';
|
||||
export * from './edgeless-frame-title-editor.js';
|
||||
export * from './mount-frame-title-editor.js';
|
||||
export * from './styles.js';
|
||||
|
||||
export const frameTitleWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
||||
|
||||
import { EdgelessFrameTitleEditor } from './edgeless-frame-title-editor';
|
||||
|
||||
export function mountFrameTitleEditor(
|
||||
frame: FrameBlockModel,
|
||||
edgeless: BlockComponent
|
||||
) {
|
||||
const mountElm = edgeless.querySelector('.edgeless-mount-point');
|
||||
if (!mountElm) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||
"edgeless block's mount point does not exist"
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
|
||||
// @ts-expect-error TODO: refactor gfx tool
|
||||
gfx.tool.setTool('default');
|
||||
gfx.selection.set({
|
||||
elements: [frame.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
const frameEditor = new EdgelessFrameTitleEditor();
|
||||
frameEditor.frameModel = frame;
|
||||
frameEditor.edgeless = edgeless;
|
||||
|
||||
mountElm.append(frameEditor);
|
||||
}
|
||||
Reference in New Issue
Block a user