mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
refactor(editor): extract frame title widget (#9458)
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import type { RichText } from '@blocksuite/affine-components/rich-text';
|
||||
import { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
type AffineFrameTitleWidget,
|
||||
frameTitleStyleVars,
|
||||
} from '@blocksuite/affine-widget-frame-title';
|
||||
import {
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
@@ -10,11 +15,6 @@ 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 '../../../widgets/frame-title/index.js';
|
||||
import { frameTitleStyleVars } from '../../../widgets/frame-title/styles.js';
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ThemeService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
|
||||
import { AFFINE_FRAME_TITLE_WIDGET } from '@blocksuite/affine-widget-frame-title';
|
||||
import {
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
@@ -31,7 +32,6 @@ import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-to
|
||||
import { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from '../widgets/element-toolbar/index.js';
|
||||
import { AFFINE_EMBED_CARD_TOOLBAR_WIDGET } from '../widgets/embed-card-toolbar/embed-card-toolbar.js';
|
||||
import { AFFINE_FORMAT_BAR_WIDGET } from '../widgets/format-bar/format-bar.js';
|
||||
import { AFFINE_FRAME_TITLE_WIDGET } from '../widgets/frame-title/index.js';
|
||||
import { AFFINE_INNER_MODAL_WIDGET } from '../widgets/inner-modal/inner-modal.js';
|
||||
import { AFFINE_LINKED_DOC_WIDGET } from '../widgets/linked-doc/index.js';
|
||||
import { AFFINE_MODAL_WIDGET } from '../widgets/modal/modal.js';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
|
||||
import type { AFFINE_FRAME_TITLE_WIDGET } from '@blocksuite/affine-widget-frame-title';
|
||||
import type {
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
@@ -10,7 +11,6 @@ import type { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from './widgets/edgeless-zoo
|
||||
import type { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from './widgets/element-toolbar/index.js';
|
||||
import type { AFFINE_EMBED_CARD_TOOLBAR_WIDGET } from './widgets/embed-card-toolbar/embed-card-toolbar.js';
|
||||
import type { AFFINE_FORMAT_BAR_WIDGET } from './widgets/format-bar/format-bar.js';
|
||||
import type { AFFINE_FRAME_TITLE_WIDGET } from './widgets/frame-title/index.js';
|
||||
import type { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from './widgets/index.js';
|
||||
import type { AFFINE_INNER_MODAL_WIDGET } from './widgets/inner-modal/inner-modal.js';
|
||||
import type { AFFINE_LINKED_DOC_WIDGET } from './widgets/linked-doc/index.js';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
import { parseStringToRgba } from '@blocksuite/affine-components/color-picker';
|
||||
import {
|
||||
ColorScheme,
|
||||
FrameBlockModel,
|
||||
isTransparent,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
PropTypes,
|
||||
requiredProperties,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
Bound,
|
||||
type SerializedXYWH,
|
||||
SignalWatcher,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { consume } from '@lit/context';
|
||||
import { themeToVar } from '@toeverything/theme/v2';
|
||||
import { LitElement } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
import type { EdgelessRootService } from '../../edgeless/index.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.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);
|
||||
}
|
||||
|
||||
get rootService() {
|
||||
return this.std.getService('affine:page') as EdgelessRootService;
|
||||
}
|
||||
|
||||
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, rootService } = this;
|
||||
|
||||
this._nestedFrame = this._isInsideFrame();
|
||||
|
||||
_disposables.add(
|
||||
doc.slots.blockUpdated.on(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.on(() => {
|
||||
this._xywh = this.model.xywh;
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
rootService.selection.slots.updated.on(() => {
|
||||
this._editing =
|
||||
rootService.selection.selectedIds[0] === this.model.id &&
|
||||
rootService.selection.editing;
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
gfx.viewport.viewportUpdated.on(({ zoom }) => {
|
||||
this._zoom = zoom;
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this._zoom = gfx.viewport.zoom;
|
||||
|
||||
const updateTitle = () => {
|
||||
this._frameTitle = this.model.title.toString().trim();
|
||||
};
|
||||
_disposables.add(() => {
|
||||
this.model.title.yText.unobserve(updateTitle);
|
||||
});
|
||||
this.model.title.yText.observe(updateTitle);
|
||||
|
||||
this._frameTitle = this.model.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;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../index.js';
|
||||
import type { AffineFrameTitle } from './frame-title.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<
|
||||
RootBlockModel,
|
||||
EdgelessRootBlockComponent
|
||||
> {
|
||||
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>`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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%);
|
||||
}
|
||||
`;
|
||||
@@ -25,7 +25,6 @@ export {
|
||||
AFFINE_FORMAT_BAR_WIDGET,
|
||||
AffineFormatBarWidget,
|
||||
} from './format-bar/format-bar.js';
|
||||
export { AffineFrameTitleWidget } from './frame-title/index.js';
|
||||
export { AffineImageToolbarWidget } from './image-toolbar/index.js';
|
||||
export { AffineInnerModalWidget } from './inner-modal/inner-modal.js';
|
||||
export * from './keyboard-toolbar/index.js';
|
||||
@@ -52,3 +51,4 @@ export {
|
||||
type AffineSlashSubMenu,
|
||||
} from './slash-menu/index.js';
|
||||
export { AffineSurfaceRefToolbar } from './surface-ref-toolbar/surface-ref-toolbar.js';
|
||||
export { AffineFrameTitleWidget } from '@blocksuite/affine-widget-frame-title';
|
||||
|
||||
Reference in New Issue
Block a user