refactor(editor): unify directories naming (#11516)

**Directory Structure Changes**

- Renamed multiple block-related directories by removing the "block-" prefix:
  - `block-attachment` → `attachment`
  - `block-bookmark` → `bookmark`
  - `block-callout` → `callout`
  - `block-code` → `code`
  - `block-data-view` → `data-view`
  - `block-database` → `database`
  - `block-divider` → `divider`
  - `block-edgeless-text` → `edgeless-text`
  - `block-embed` → `embed`
This commit is contained in:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions
@@ -0,0 +1,43 @@
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/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,183 @@
import { FrameBlockModel } from '@blocksuite/affine-model';
import type { RichText } from '@blocksuite/affine-rich-text';
import { Bound } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { type BlockComponent, ShadowlessElement } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
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;
}
@@ -0,0 +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';
export function effects() {
customElements.define(AFFINE_FRAME_TITLE_WIDGET, AffineFrameTitleWidget);
customElements.define(AFFINE_FRAME_TITLE, AffineFrameTitle);
customElements.define(
'edgeless-frame-title-editor',
EdgelessFrameTitleEditor
);
}
@@ -0,0 +1,307 @@
import { parseStringToRgba } from '@blocksuite/affine-components/color-picker';
import {
ColorScheme,
FrameBlockModel,
isTransparent,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { on } from '@blocksuite/affine-shared/utils';
import { Bound, type SerializedXYWH } from '@blocksuite/global/gfx';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import {
type BlockStdScope,
PropTypes,
requiredProperties,
stdContext,
} from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { consume } from '@lit/context';
import { themeToVar } from '@toeverything/theme/v2';
import { LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import { mountFrameTitleEditor } from './mount-frame-title-editor.js';
import { frameTitleStyle, frameTitleStyleVars } from './styles.js';
export const AFFINE_FRAME_TITLE = 'affine-frame-title';
@requiredProperties({
model: PropTypes.instanceOf(FrameBlockModel),
})
export class AffineFrameTitle extends SignalWatcher(
WithDisposable(LitElement)
) {
static override styles = frameTitleStyle;
private _cachedHeight = 0;
private _cachedWidth = 0;
get colors() {
let backgroundColor = this.std
.get(ThemeProvider)
.getColorValue(this.model.props.background, undefined, true);
if (isTransparent(backgroundColor)) {
backgroundColor = this.std
.get(ThemeProvider)
.getCssVariableColor(themeToVar('edgeless/frame/background/white'));
}
const { r, g, b, a } = parseStringToRgba(backgroundColor);
const theme = this.std.get(ThemeProvider).theme;
let textColor: string;
{
let rPrime, gPrime, bPrime;
if (theme === ColorScheme.Light) {
rPrime = 1 - a + a * r;
gPrime = 1 - a + a * g;
bPrime = 1 - a + a * b;
} else {
rPrime = a * r;
gPrime = a * g;
bPrime = a * b;
}
// light
const L = 0.299 * rPrime + 0.587 * gPrime + 0.114 * bPrime;
textColor = L > 0.5 ? 'black' : 'white';
}
return {
background: backgroundColor,
text: textColor,
};
}
get doc() {
return this.model.doc;
}
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
private _isInsideFrame() {
return this.gfx.grid.has(
this.model.elementBound,
true,
true,
model => model !== this.model && model instanceof FrameBlockModel
);
}
private _updateFrameTitleSize() {
const { _nestedFrame, _zoom: zoom } = this;
const { elementBound } = this.model;
const width = this._cachedWidth / zoom;
const height = this._cachedHeight / zoom;
const { nestedFrameOffset } = frameTitleStyleVars;
if (width && height) {
this.model.externalXYWH = `[${
elementBound.x + (_nestedFrame ? nestedFrameOffset / zoom : 0)
},${
elementBound.y +
(_nestedFrame
? nestedFrameOffset / zoom
: -(height + nestedFrameOffset / zoom))
},${width},${height}]`;
this.gfx.grid.update(this.model);
} else {
this.model.externalXYWH = undefined;
}
}
private _updateStyle() {
if (
this._frameTitle.length === 0 ||
this._editing ||
this.gfx.tool.currentToolName$.value === 'frameNavigator'
) {
this.style.display = 'none';
return;
}
const model = this.model;
const bound = Bound.deserialize(model.xywh);
const { _zoom: zoom } = this;
const { nestedFrameOffset, height } = frameTitleStyleVars;
const nestedFrame = this._nestedFrame;
const maxWidth = nestedFrame
? bound.w * zoom - nestedFrameOffset / zoom
: bound.w * zoom;
const hidden = height / zoom >= bound.h;
const transformOperation = [
`translate(0%, ${nestedFrame ? 0 : -100}%)`,
`translate(${nestedFrame ? nestedFrameOffset : 0}px, ${
nestedFrame ? nestedFrameOffset : -nestedFrameOffset
}px)`,
];
const anchor = this.gfx.viewport.toViewCoord(bound.x, bound.y);
this.style.display = '';
this.style.setProperty('--bg-color', this.colors.background);
this.style.left = `${anchor[0]}px`;
this.style.top = `${anchor[1]}px`;
this.style.display = hidden ? 'none' : 'flex';
this.style.transform = transformOperation.join(' ');
this.style.maxWidth = `${maxWidth}px`;
this.style.transformOrigin = nestedFrame ? 'top left' : 'bottom left';
this.style.color = this.colors.text;
}
override connectedCallback() {
super.connectedCallback();
const { _disposables, doc, gfx } = this;
this._nestedFrame = this._isInsideFrame();
_disposables.add(
doc.slots.blockUpdated.subscribe(payload => {
if (
(payload.type === 'update' &&
payload.props.key === 'xywh' &&
doc.getBlock(payload.id)?.model instanceof FrameBlockModel) ||
(payload.type === 'add' && payload.flavour === 'affine:frame')
) {
this._nestedFrame = this._isInsideFrame();
}
if (
payload.type === 'delete' &&
payload.model instanceof FrameBlockModel &&
payload.model !== this.model
) {
this._nestedFrame = this._isInsideFrame();
}
})
);
_disposables.add(
this.model.propsUpdated.subscribe(() => {
this._xywh = this.model.xywh;
this.requestUpdate();
})
);
_disposables.add(
gfx.selection.slots.updated.subscribe(() => {
this._editing =
gfx.selection.selectedIds[0] === this.model.id &&
gfx.selection.editing;
})
);
_disposables.add(
gfx.viewport.viewportUpdated.subscribe(({ zoom }) => {
this._zoom = zoom;
this.requestUpdate();
})
);
_disposables.add(
on(this, 'click', evt => {
if (evt.shiftKey) {
this.gfx.selection.toggle(this.model);
} else {
this.gfx.selection.set({
elements: [this.model.id],
});
}
})
);
_disposables.add(
on(this, 'dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');
if (edgeless && !this.model.isLocked()) {
mountFrameTitleEditor(this.model, edgeless);
} else {
this.gfx.selection.set({
elements: [this.model.id],
});
}
})
);
this._zoom = gfx.viewport.zoom;
const updateTitle = () => {
this._frameTitle = this.model.props.title.toString().trim();
};
_disposables.add(() => {
this.model.props.title.yText.unobserve(updateTitle);
});
this.model.props.title.yText.observe(updateTitle);
this._frameTitle = this.model.props.title.toString().trim();
this._xywh = this.model.xywh;
}
override firstUpdated() {
this._cachedWidth = this.clientWidth;
this._cachedHeight = this.clientHeight;
this._updateFrameTitleSize();
}
override render() {
this._updateStyle();
return this._frameTitle;
}
override updated(_changedProperties: Map<string, unknown>) {
if (
!this.gfx.viewport.viewportBounds.contains(this.model.elementBound) &&
!this.gfx.viewport.viewportBounds.isIntersectWithBound(
this.model.elementBound
)
) {
return;
}
let sizeChanged = false;
if (
this._cachedWidth === 0 ||
this._cachedHeight === 0 ||
_changedProperties.has('_frameTitle') ||
_changedProperties.has('_nestedFrame') ||
_changedProperties.has('_xywh') ||
_changedProperties.has('_editing')
) {
this._cachedWidth = this.clientWidth;
this._cachedHeight = this.clientHeight;
sizeChanged = true;
}
if (sizeChanged || _changedProperties.has('_zoom')) {
this._updateFrameTitleSize();
}
}
@state()
private accessor _editing = false;
@state()
private accessor _frameTitle = '';
@state()
private accessor _nestedFrame = false;
@state()
private accessor _xywh: SerializedXYWH | null = null;
@state()
private accessor _zoom = 1;
@property({ attribute: false })
accessor model!: FrameBlockModel;
@consume({ context: stdContext })
accessor std!: BlockStdScope;
}
@@ -0,0 +1,4 @@
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';
@@ -0,0 +1,34 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import type { BlockComponent } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
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);
}
@@ -0,0 +1,35 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit';
export const frameTitleStyleVars = {
nestedFrameOffset: 4,
height: 22,
fontSize: 14,
};
export const frameTitleStyle = css`
:host {
position: absolute;
display: flex;
align-items: center;
z-index: 1;
border: 1px solid ${unsafeCSSVarV2('edgeless/frame/border/default')};
border-radius: 4px;
width: fit-content;
height: ${frameTitleStyleVars.height}px;
padding: 0px 4px;
transform-origin: left bottom;
background-color: var(--bg-color);
font-family: var(--affine-font-family);
font-size: ${frameTitleStyleVars.fontSize}px;
cursor: default;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:hover {
background-color: color-mix(in srgb, var(--bg-color), #000000 7%);
}
`;