Merge branch 'canary' into zzj/feat/database-block/upgrade-prompt

This commit is contained in:
3720
2025-05-14 01:28:25 +08:00
committed by GitHub
352 changed files with 7012 additions and 6659 deletions
+3 -1
View File
@@ -274,7 +274,9 @@
"./model": "./src/model/index.ts",
"./sync": "./src/sync/index.ts",
"./extensions/store": "./src/extensions/store.ts",
"./extensions/view": "./src/extensions/view.ts"
"./extensions/view": "./src/extensions/view.ts",
"./foundation/store": "./src/foundation/store.ts",
"./foundation/view": "./src/foundation/view.ts"
},
"files": [
"src",
@@ -106,4 +106,65 @@ describe('notion-text to snapshot', () => {
});
expect(nanoidReplacement(target!)).toEqual(sliceSnapshot);
});
test('notion text with empty styles array', () => {
const notionText =
'{"blockType":"text","editing":[["a "],["bold text",[["b"]]],[" hello world"]],"selection":{"startIndex":0,"endIndex":23},"action":"copy"}';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'a ',
},
{
insert: 'bold text',
attributes: {
bold: true,
},
},
{
insert: ' hello world',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const ntAdapter = new NotionTextAdapter(createJob(), provider);
const target = ntAdapter.toSliceSnapshot({
file: notionText,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(target!)).toEqual(sliceSnapshot);
});
});
@@ -0,0 +1 @@
export * from '@blocksuite/affine-foundation/store';
@@ -0,0 +1 @@
export * from '@blocksuite/affine-foundation/view';
@@ -1,10 +1,14 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { AttachmentBlockStyles } from '@blocksuite/affine-model';
import {
AttachmentBlockSchema,
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { styleMap } from 'lit/directives/style-map.js';
import { AttachmentBlockComponent } from './attachment-block.js';
@@ -48,3 +52,21 @@ declare global {
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
}
}
export const AttachmentBlockInteraction = GfxViewInteractionExtension(
AttachmentBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: true,
},
handleRotate: () => {
return {
beforeRotate: context => {
context.set({
rotatable: false,
});
},
};
},
}
);
@@ -5,6 +5,7 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { AttachmentBlockAdapterExtensions } from './adapters/extension.js';
import { AttachmentBlockInteraction } from './attachment-edgeless-block.js';
import { AttachmentDropOption } from './attachment-service.js';
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
@@ -26,6 +27,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
AttachmentEmbedConfigExtension(),
AttachmentEmbedService,
AttachmentBlockAdapterExtensions,
AttachmentBlockInteraction,
createBuiltinToolbarConfigExtension(flavour),
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
].flat();
@@ -7,6 +7,7 @@ import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js';
import { AttachmentBlockInteraction } from './attachment-edgeless-block.js';
import { AttachmentDropOption } from './attachment-service.js';
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
@@ -44,6 +45,7 @@ export class AttachmentViewExtension extends ViewExtensionProvider {
]);
if (this.isEdgeless(context.scope)) {
context.register(EdgelessClipboardAttachmentConfig);
context.register(AttachmentBlockInteraction);
}
}
}
@@ -9,7 +9,7 @@ import type {
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import {
DocModeProvider,
LinkPreviewerService,
LinkPreviewServiceIdentifier,
} from '@blocksuite/affine-shared/services';
import { BlockSelection } from '@blocksuite/std';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
@@ -72,8 +72,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
this.loading = true;
this.error = false;
this.std.store
.get(LinkPreviewerService)
this.std
.get(LinkPreviewServiceIdentifier)
.query(this.model.props.url, this._fetchAbortController.signal)
.then(data => {
this._localLinkPreview$.value = {
@@ -1,8 +1,10 @@
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { BookmarkBlockComponent } from './bookmark-block.js';
@@ -50,6 +52,24 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
};
}
export const BookmarkBlockInteraction = GfxViewInteractionExtension(
BookmarkBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: true,
},
handleRotate: () => {
return {
beforeRotate(context) {
context.set({
rotatable: false,
});
},
};
},
}
);
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;
@@ -4,6 +4,7 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { BookmarkBlockAdapterExtensions } from './adapters/extension';
import { BookmarkBlockInteraction } from './bookmark-edgeless-block';
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
@@ -16,6 +17,7 @@ export const BookmarkBlockSpec: ExtensionType[] = [
? literal`affine-edgeless-bookmark`
: literal`affine-bookmark`;
}),
BookmarkBlockInteraction,
BookmarkBlockAdapterExtensions,
createBuiltinToolbarConfigExtension(flavour),
BookmarkSlashMenuConfigExtension,
@@ -1,4 +1,4 @@
import { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { LinkPreviewServiceIdentifier } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import type { BookmarkBlockComponent } from './bookmark-block.js';
@@ -15,7 +15,7 @@ export async function refreshBookmarkUrlData(
try {
bookmarkElement.loading = true;
const linkPreviewer = bookmarkElement.store.get(LinkPreviewerService);
const linkPreviewer = bookmarkElement.std.get(LinkPreviewServiceIdentifier);
const bookmarkUrlData = await linkPreviewer.query(
bookmarkElement.model.props.url,
signal
@@ -6,6 +6,7 @@ import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js';
import { BookmarkBlockInteraction } from './bookmark-edgeless-block';
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EdgelessClipboardBookmarkConfig } from './edgeless-clipboard-config';
@@ -36,6 +37,7 @@ export class BookmarkViewExtension extends ViewExtensionProvider {
const isEdgeless = this.isEdgeless(context.scope);
if (isEdgeless) {
context.register(EdgelessClipboardBookmarkConfig);
context.register(BookmarkBlockInteraction);
}
}
}
+8 -1
View File
@@ -11,6 +11,7 @@ import {
import { literal, unsafeStatic } from 'lit/static-html.js';
import { getCodeClipboardExtensions } from './clipboard/index.js';
import { CodeBlockConfigExtension } from './code-block-config';
import {
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
@@ -21,7 +22,7 @@ import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
import { codeSlashMenuConfig } from './configs/slash-menu.js';
import { effects } from './effects.js';
export const codeToolbarWidget = WidgetViewExtension(
const codeToolbarWidget = WidgetViewExtension(
'affine:code',
AFFINE_CODE_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}`
@@ -51,6 +52,12 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
]);
if (!this.isMobile(context.scope)) {
context.register(codeToolbarWidget);
} else {
context.register(
CodeBlockConfigExtension({
showLineNumbers: false,
})
);
}
}
}
@@ -1,7 +1,4 @@
import { createContextKey } from '@blocksuite/data-view';
import { createIdentifier } from '@blocksuite/global/di';
import type { EditorHost } from '@blocksuite/std';
export const HostContextKey = createContextKey<EditorHost | undefined>(
'editor-host',
undefined
);
export const EditorHostKey = createIdentifier<EditorHost>('editor-host');
@@ -57,6 +57,9 @@ type SpacialProperty = {
valueGet: (rowId: string, propertyId: string) => unknown;
};
export class DatabaseBlockDataSource extends DataSourceBase {
override get parentProvider() {
return this._model.store.provider;
}
spacialProperties: Record<string, SpacialProperty> = {
'created-time': {
valueSet: () => {},
@@ -186,9 +189,13 @@ export class DatabaseBlockDataSource extends DataSourceBase {
);
});
constructor(model: DatabaseBlockModel) {
constructor(
model: DatabaseBlockModel,
init?: (dataSource: DatabaseBlockDataSource) => void
) {
super();
this._model = model;
this._model = model; // ensure invariants first
init?.(this); // then allow external initialisation
}
private _runCapture() {
@@ -27,6 +27,7 @@ import {
type DataViewWidget,
type DataViewWidgetProps,
defineUniComponent,
ExternalGroupByConfigProvider,
renderUniLit,
type SingleView,
uniMap,
@@ -47,7 +48,7 @@ import { css, html, nothing, unsafeCSS } from 'lit';
import { popSideDetail } from './components/layout.js';
import { DatabaseConfigExtension } from './config.js';
import { HostContextKey } from './context/host-context.js';
import { EditorHostKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js';
import { BlockRenderer } from './detail-panel/block-renderer.js';
import { NoteRenderer } from './detail-panel/note-renderer.js';
@@ -333,8 +334,17 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
get dataSource(): DatabaseBlockDataSource {
if (!this._dataSource) {
this._dataSource = new DatabaseBlockDataSource(this.model);
this._dataSource.contextSet(HostContextKey, this.host);
this._dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
dataSource.serviceSet(EditorHostKey, this.host);
this.std.provider
.getAll(ExternalGroupByConfigProvider)
.forEach(config => {
dataSource.serviceSet(
ExternalGroupByConfigProvider(config.name),
config
);
});
});
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && this.dataSource.viewManager.viewGet(id)) {
this.dataSource.viewManager.setCurrentView(id);
@@ -1,14 +0,0 @@
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { DatabaseBlockAdapterExtensions } from './adapters/extension.js';
import { databaseSlashMenuConfig } from './configs/slash-menu.js';
export const DatabaseBlockSpec: ExtensionType[] = [
FlavourExtension('affine:database'),
BlockViewExtension('affine:database', literal`affine-database`),
DatabaseBlockAdapterExtensions,
SlashMenuConfigExtension('affine:database', databaseSlashMenuConfig),
].flat();
@@ -4,7 +4,6 @@ export * from './config';
export * from './context';
export * from './data-source';
export * from './database-block';
export * from './database-spec';
export * from './detail-panel/block-renderer';
export * from './detail-panel/note-renderer';
export * from './properties';
@@ -15,7 +15,7 @@ import { computed } from '@preact/signals-core';
import { html, nothing, type PropertyValues } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
import { HostContextKey } from '../../context/host-context.js';
import { EditorHostKey } from '../../context/host-context.js';
import {
inlineLinkNodeStyle,
linkCellStyle,
@@ -88,7 +88,7 @@ export class LinkCell extends BaseCellRenderer<string, string> {
};
get std() {
const host = this.view.contextGet(HostContextKey);
const host = this.view.serviceGet(EditorHostKey);
return host?.std;
}
@@ -24,7 +24,7 @@ import { computed, effect, signal } from '@preact/signals-core';
import { ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js';
import { EditorHostKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import {
richTextCellStyle,
@@ -87,7 +87,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
get inlineManager() {
return this.view
.contextGet(HostContextKey)
.serviceGet(EditorHostKey)
?.std.get(DefaultInlineManagerExtension.identifier);
}
@@ -98,7 +98,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
}
get host() {
return this.view.contextGet(HostContextKey);
return this.view.serviceGet(EditorHostKey);
}
private readonly richText$ = signal<RichText>();
@@ -398,7 +398,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
}
private get std() {
return this.view.contextGet(HostContextKey)?.std;
return this.view.serviceGet(EditorHostKey)?.std;
}
insertDelta = (delta: DeltaInsert<AffineTextAttributes>) => {
@@ -5,7 +5,7 @@ import { Text } from '@blocksuite/store';
import * as Y from 'yjs';
import zod from 'zod';
import { HostContextKey } from '../../context/host-context.js';
import { EditorHostKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js';
export const richTextColumnType = propertyType('rich-text');
@@ -43,7 +43,7 @@ export const richTextPropertyModelConfig = richTextColumnType.modelConfig({
},
toJson: ({ value, dataSource }) => {
if (!value) return null;
const host = dataSource.contextGet(HostContextKey);
const host = dataSource.serviceGet(EditorHostKey);
if (host) {
const collection = host.std.workspace;
const yText = toYText(value);
@@ -3,7 +3,7 @@ import { Text } from '@blocksuite/store';
import { Doc } from 'yjs';
import zod from 'zod';
import { HostContextKey } from '../../context/host-context.js';
import { EditorHostKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js';
export const titleColumnType = propertyType('title');
@@ -28,7 +28,7 @@ export const titlePropertyModelConfig = titleColumnType.modelConfig({
},
toJson: ({ value, dataSource }) => {
if (!value) return '';
const host = dataSource.contextGet(HostContextKey);
const host = dataSource.serviceGet(EditorHostKey);
if (host) {
const collection = host.std.workspace;
const deltas = value.deltas$.value;
@@ -17,7 +17,7 @@ import { property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js';
import { EditorHostKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
import {
@@ -32,7 +32,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
docId$ = signal<string>();
get host() {
return this.view.contextGet(HostContextKey);
return this.view.serviceGet(EditorHostKey);
}
get inlineEditor() {
@@ -50,7 +50,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
}
get std() {
return this.view.contextGet(HostContextKey)?.std;
return this.view.serviceGet(EditorHostKey)?.std;
}
private readonly _onCopy = (e: ClipboardEvent) => {
@@ -5,6 +5,7 @@ import {
EDGELESS_TEXT_BLOCK_MIN_HEIGHT,
EDGELESS_TEXT_BLOCK_MIN_WIDTH,
type EdgelessTextBlockModel,
EdgelessTextBlockSchema,
ListBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
@@ -21,7 +22,10 @@ import {
GfxBlockComponent,
TextSelection,
} from '@blocksuite/std';
import type { SelectedContext } from '@blocksuite/std/gfx';
import {
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
@@ -420,3 +424,69 @@ declare global {
'affine-edgeless-text': EdgelessTextBlockComponent;
}
}
export const EdgelessTextInteraction =
GfxViewInteractionExtension<EdgelessTextBlockComponent>(
EdgelessTextBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
allowedHandlers: [
'top-left',
'top-right',
'left',
'right',
'bottom-left',
'bottom-right',
],
minWidth: EDGELESS_TEXT_BLOCK_MIN_WIDTH,
},
handleResize: context => {
const { model, view } = context;
const initialScale = model.props.scale;
return {
onResizeStart(context) {
context.default(context);
model.stash('scale');
model.stash('hasMaxWidth');
},
onResizeMove(context) {
const { originalBound, newBound, constraint, lockRatio } = context;
if (lockRatio) {
const originalRealWidth = originalBound.w / initialScale;
const newScale = newBound.w / originalRealWidth;
model.props.scale = newScale;
model.props.xywh = newBound.serialize();
} else {
if (!view.checkWidthOverflow(newBound.w)) {
return;
}
const newRealWidth = clamp(
newBound.w / initialScale,
constraint.minWidth,
constraint.maxWidth
);
const curBound = Bound.deserialize(model.xywh);
model.props.xywh = Bound.serialize({
...newBound,
w: newRealWidth * initialScale,
h: curBound.h,
});
model.props.hasMaxWidth = true;
}
},
onResizeEnd(context) {
context.default(context);
model.pop('scale');
model.pop('hasMaxWidth');
},
};
},
}
);
@@ -2,6 +2,9 @@ import { BlockViewExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { EdgelessTextInteraction } from './edgeless-text-block';
export const EdgelessTextBlockSpec: ExtensionType[] = [
BlockViewExtension('affine:edgeless-text', literal`affine-edgeless-text`),
EdgelessTextInteraction,
];
@@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js';
import { EdgelessClipboardEdgelessTextConfig } from './edgeless-clipboard-config';
import { EdgelessTextInteraction } from './edgeless-text-block';
import { edgelessTextToolbarExtension } from './edgeless-toolbar';
import { effects } from './effects';
@@ -30,6 +31,7 @@ export class EdgelessTextViewExtension extends ViewExtensionProvider {
]);
context.register(edgelessTextToolbarExtension);
context.register(EdgelessClipboardEdgelessTextConfig);
context.register(EdgelessTextInteraction);
}
}
}
@@ -1,8 +1,12 @@
import { toEdgelessEmbedBlock } from '@blocksuite/affine-block-embed';
import {
createEmbedEdgelessBlockInteraction,
toEdgelessEmbedBlock,
} from '@blocksuite/affine-block-embed';
import {
EdgelessCRUDIdentifier,
reassociateConnectorsCommand,
} from '@blocksuite/affine-block-surface';
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
@@ -61,3 +65,7 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
}
};
}
export const EmbedLinkedDocInteraction = createEmbedEdgelessBlockInteraction(
EmbedLinkedDocBlockSchema.model.flavour
);
@@ -6,6 +6,7 @@ import { literal } from 'lit/static-html.js';
import { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension';
import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EmbedLinkedDocInteraction } from './embed-edgeless-linked-doc-block';
const flavour = EmbedLinkedDocBlockSchema.model.flavour;
@@ -27,5 +28,6 @@ export const EmbedLinkedDocViewExtensions: ExtensionType[] = [
: literal`affine-embed-linked-doc-block`;
}),
createBuiltinToolbarConfigExtension(flavour),
EmbedLinkedDocInteraction,
LinkedDocSlashMenuConfigExtension,
].flat();
@@ -2,5 +2,6 @@ export * from './adapters';
export * from './commands';
export { LinkedDocSlashMenuConfigIdentifier } from './configs/slash-menu';
export * from './edgeless-clipboard-config';
export * from './embed-edgeless-linked-doc-block';
export * from './embed-linked-doc-block';
export * from './embed-linked-doc-spec';
@@ -3,7 +3,12 @@ import {
EdgelessCRUDIdentifier,
reassociateConnectorsCommand,
} from '@blocksuite/affine-block-surface';
import type { AliasInfo } from '@blocksuite/affine-model';
import {
type AliasInfo,
EmbedSyncedDocBlockSchema,
SYNCED_MIN_HEIGHT,
SYNCED_MIN_WIDTH,
} from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
@@ -12,8 +17,9 @@ import {
ThemeExtensionIdentifier,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import { Bound } from '@blocksuite/global/gfx';
import { Bound, clamp } from '@blocksuite/global/gfx';
import { type BlockComponent, BlockStdScope } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { html, nothing } from 'lit';
import { query, queryAsync } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
@@ -199,3 +205,60 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock(
override accessor useCaptionEditor = true;
}
export const EmbedSyncedDocInteraction =
GfxViewInteractionExtension<EmbedEdgelessSyncedDocBlockComponent>(
EmbedSyncedDocBlockSchema.model.flavour,
{
resizeConstraint: {
minWidth: SYNCED_MIN_WIDTH,
minHeight: SYNCED_MIN_HEIGHT,
},
handleRotate: () => {
return {
beforeRotate(context) {
context.set({
rotatable: false,
});
},
};
},
handleResize: ({ model }) => {
const initialScale = model.props.scale ?? 1;
return {
onResizeStart: context => {
context.default(context);
model.stash('scale');
},
onResizeMove: context => {
const { lockRatio, originalBound, constraint, newBound } = context;
let scale = initialScale;
const realWidth = originalBound.w / initialScale;
if (lockRatio) {
scale = newBound.w / realWidth;
}
const newWidth = newBound.w / scale;
newBound.w =
clamp(newWidth, constraint.minWidth, constraint.maxWidth) * scale;
newBound.h =
clamp(newBound.h, constraint.minHeight, constraint.maxHeight) *
scale;
model.props.scale = scale;
model.xywh = newBound.serialize();
},
onResizeEnd: context => {
context.default(context);
model.pop('scale');
},
};
},
}
);
@@ -5,6 +5,7 @@ import { literal } from 'lit/static-html.js';
import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EmbedSyncedDocInteraction } from './embed-edgeless-synced-doc-block';
import { HeightInitializationExtension } from './init-height-extension';
const flavour = EmbedSyncedDocBlockSchema.model.flavour;
@@ -29,4 +30,5 @@ export const EmbedSyncedDocViewExtensions: ExtensionType[] = [
}),
createBuiltinToolbarConfigExtension(flavour),
HeightInitializationExtension,
EmbedSyncedDocInteraction,
].flat();
@@ -2,6 +2,7 @@ export * from './adapters';
export * from './commands';
export * from './configs';
export * from './edgeless-clipboard-config';
export * from './embed-edgeless-synced-doc-block';
export * from './embed-synced-doc-block';
export * from './embed-synced-doc-spec';
export { SYNCED_MIN_HEIGHT, SYNCED_MIN_WIDTH } from '@blocksuite/affine-model';
@@ -6,10 +6,12 @@ import {
import { effects } from './effects';
import {
EdgelessClipboardEmbedLinkedDocConfig,
EmbedLinkedDocInteraction,
EmbedLinkedDocViewExtensions,
} from './embed-linked-doc-block';
import {
EdgelessClipboardEmbedSyncedDocConfig,
EmbedSyncedDocInteraction,
EmbedSyncedDocViewExtensions,
} from './embed-synced-doc-block';
@@ -30,6 +32,8 @@ export class EmbedDocViewExtension extends ViewExtensionProvider {
context.register([
EdgelessClipboardEmbedLinkedDocConfig,
EdgelessClipboardEmbedSyncedDocConfig,
EmbedLinkedDocInteraction,
EmbedSyncedDocInteraction,
]);
}
}
@@ -11,7 +11,11 @@ import {
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { findAncestorModel } from '@blocksuite/affine-shared/utils';
import type { BlockService } from '@blocksuite/std';
import type { GfxCompatibleProps } from '@blocksuite/std/gfx';
import {
type GfxCompatibleProps,
GfxViewInteractionExtension,
type ResizeConstraint,
} from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
@@ -163,3 +167,31 @@ export class EmbedBlockComponent<
override accessor useZeroWidth = true;
}
export const createEmbedEdgelessBlockInteraction = (
flavour: string,
config?: {
resizeConstraint?: ResizeConstraint;
}
) => {
const resizeConstraint = Object.assign(
{
lockRatio: true,
},
config?.resizeConstraint ?? {}
);
const rotateConstraint = {
rotatable: false,
};
return GfxViewInteractionExtension(flavour, {
resizeConstraint,
handleRotate() {
return {
beforeRotate(context) {
context.set(rotateConstraint);
},
};
},
});
};
@@ -1,6 +1,13 @@
import { EmbedFigmaBlockSchema } from '@blocksuite/affine-model';
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
import { EmbedFigmaBlockComponent } from './embed-figma-block.js';
export class EmbedEdgelessBlockComponent extends toEdgelessEmbedBlock(
EmbedFigmaBlockComponent
) {}
export const EmbedFigmaBlockInteraction = createEmbedEdgelessBlockInteraction(
EmbedFigmaBlockSchema.model.flavour
);
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedFigmaBlockAdapterExtensions } from './adapters/extension';
import { embedFigmaSlashMenuConfig } from './configs/slash-menu';
import { EmbedFigmaBlockInteraction } from './embed-edgeless-figma-block';
import { EmbedFigmaBlockComponent } from './embed-figma-block';
import { EmbedFigmaBlockOptionConfig } from './embed-figma-service';
@@ -35,4 +36,5 @@ export const EmbedFigmaViewExtensions: ExtensionType[] = [
EmbedFigmaBlockOptionConfig,
createBuiltinToolbarConfigExtension(flavour, EmbedFigmaBlockComponent),
SlashMenuConfigExtension(flavour, embedFigmaSlashMenuConfig),
EmbedFigmaBlockInteraction,
].flat();
@@ -1,6 +1,13 @@
import { EmbedGithubBlockSchema } from '@blocksuite/affine-model';
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
import { EmbedGithubBlockComponent } from './embed-github-block.js';
export class EmbedEdgelessGithubBlockComponent extends toEdgelessEmbedBlock(
EmbedGithubBlockComponent
) {}
export const EmbedGithubBlockInteraction = createEmbedEdgelessBlockInteraction(
EmbedGithubBlockSchema.model.flavour
);
@@ -5,7 +5,7 @@ import {
} from '@blocksuite/affine-model';
import {
EmbedOptionConfig,
LinkPreviewerService,
LinkPreviewServiceIdentifier,
} from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/std';
@@ -22,7 +22,7 @@ export class EmbedGithubBlockService extends BlockService {
queryUrlData = (embedGithubModel: EmbedGithubModel, signal?: AbortSignal) => {
return queryEmbedGithubData(
embedGithubModel,
this.doc.get(LinkPreviewerService),
this.std.get(LinkPreviewServiceIdentifier),
signal
);
};
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedGithubBlockAdapterExtensions } from './adapters/extension';
import { embedGithubSlashMenuConfig } from './configs/slash-menu';
import { EmbedGithubBlockInteraction } from './embed-edgeless-github-block';
import { EmbedGithubBlockComponent } from './embed-github-block';
import {
EmbedGithubBlockOptionConfig,
@@ -38,6 +39,7 @@ export const EmbedGithubViewExtensions: ExtensionType[] = [
: literal`affine-embed-github-block`;
}),
EmbedGithubBlockOptionConfig,
EmbedGithubBlockInteraction,
createBuiltinToolbarConfigExtension(flavour, EmbedGithubBlockComponent),
SlashMenuConfigExtension(flavour, embedGithubSlashMenuConfig),
].flat();
@@ -2,7 +2,7 @@ import type {
EmbedGithubBlockUrlData,
EmbedGithubModel,
} from '@blocksuite/affine-model';
import type { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import type { LinkPreviewProvider } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import { nothing } from 'lit';
@@ -19,7 +19,7 @@ import {
export async function queryEmbedGithubData(
embedGithubModel: EmbedGithubModel,
linkPreviewer: LinkPreviewerService,
linkPreviewer: LinkPreviewProvider,
signal?: AbortSignal
): Promise<Partial<EmbedGithubBlockUrlData>> {
const [githubApiData, openGraphData] = await Promise.all([
@@ -1,6 +1,19 @@
import { EmbedHtmlBlockSchema } from '@blocksuite/affine-model';
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
import { EmbedHtmlBlockComponent } from './embed-html-block.js';
import { EMBED_HTML_MIN_HEIGHT, EMBED_HTML_MIN_WIDTH } from './styles.js';
export class EmbedEdgelessHtmlBlockComponent extends toEdgelessEmbedBlock(
EmbedHtmlBlockComponent
) {}
export const EmbedEdgelessHtmlBlockInteraction =
createEmbedEdgelessBlockInteraction(EmbedHtmlBlockSchema.model.flavour, {
resizeConstraint: {
minWidth: EMBED_HTML_MIN_WIDTH,
minHeight: EMBED_HTML_MIN_HEIGHT,
lockRatio: false,
},
});
@@ -4,6 +4,7 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EmbedEdgelessHtmlBlockInteraction } from './embed-edgeless-html-block';
const flavour = EmbedHtmlBlockSchema.model.flavour;
@@ -23,4 +24,5 @@ export const EmbedHtmlViewExtensions: ExtensionType[] = [
: literal`affine-embed-html-block`;
}),
createBuiltinToolbarConfigExtension(flavour),
EmbedEdgelessHtmlBlockInteraction,
].flat();
@@ -1,6 +1,8 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { Bound } from '@blocksuite/global/gfx';
import { EmbedIframeBlockSchema } from '@blocksuite/affine-model';
import { Bound, clamp } from '@blocksuite/global/gfx';
import { toGfxBlockComponent } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
@@ -53,3 +55,65 @@ export class EmbedEdgelessIframeBlockComponent extends toGfxBlockComponent(
`;
}
}
export const EmbedIframeInteraction =
GfxViewInteractionExtension<EmbedEdgelessIframeBlockComponent>(
EmbedIframeBlockSchema.model.flavour,
{
resizeConstraint: {
minWidth: 218,
minHeight: 44,
maxWidth: 3400,
maxHeight: 2200,
},
handleResize: context => {
const { model } = context;
const initialScale = model.props.scale$.peek();
return {
onResizeStart(context) {
context.default(context);
model.stash('scale');
},
onResizeMove(context) {
const { newBound, originalBound, lockRatio, constraint } = context;
const { minWidth, maxWidth, minHeight, maxHeight } = constraint;
let scale = initialScale;
const originalRealWidth = originalBound.w / scale;
// update scale if resize is proportional
if (lockRatio) {
scale = newBound.w / originalRealWidth;
}
let newRealWidth = clamp(newBound.w / scale, minWidth, maxWidth);
let newRealHeight = clamp(newBound.h / scale, minHeight, maxHeight);
newBound.w = newRealWidth * scale;
newBound.h = newRealHeight * scale;
model.props.xywh = newBound.serialize();
if (scale !== initialScale) {
model.props.scale = scale;
}
},
onResizeEnd(context) {
context.default(context);
model.pop('scale');
},
};
},
handleRotate: () => {
return {
beforeRotate(context) {
context.set({
rotatable: false,
});
},
};
},
}
);
@@ -9,7 +9,7 @@ import {
type EmbedIframeData,
EmbedIframeService,
type IframeOptions,
LinkPreviewerService,
LinkPreviewServiceIdentifier,
NotificationProvider,
} from '@blocksuite/affine-shared/services';
import { matchModels } from '@blocksuite/affine-shared/utils';
@@ -94,7 +94,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
}
get linkPreviewService() {
return this.std.get(LinkPreviewerService);
return this.std.get(LinkPreviewServiceIdentifier);
}
get notificationService() {
@@ -156,7 +156,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
if (!embedIframeService || !linkPreviewService) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
'EmbedIframeService or LinkPreviewerService not found'
'EmbedIframeService or LinkPreviewService not found'
);
}
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
import { EmbedIframeBlockAdapterExtensions } from './adapters';
import { embedIframeSlashMenuConfig } from './configs/slash-menu/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EmbedIframeInteraction } from './embed-edgeless-iframe-block';
const flavour = EmbedIframeBlockSchema.model.flavour;
@@ -31,4 +32,5 @@ export const EmbedIframeViewExtensions: ExtensionType[] = [
}),
createBuiltinToolbarConfigExtension(flavour),
SlashMenuConfigExtension(flavour, embedIframeSlashMenuConfig),
EmbedIframeInteraction,
].flat();
@@ -1,6 +1,13 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
import { EmbedLoomBlockComponent } from './embed-loom-block.js';
export class EmbedEdgelessLoomBlockComponent extends toEdgelessEmbedBlock(
EmbedLoomBlockComponent
) {}
export const EmbedLoomBlockInteraction = createEmbedEdgelessBlockInteraction(
EmbedLoomBlockSchema.model.flavour
);
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedLoomBlockAdapterExtensions } from './adapters/extension';
import { embedLoomSlashMenuConfig } from './configs/slash-menu';
import { EmbedLoomBlockInteraction } from './embed-edgeless-loom-bock';
import { EmbedLoomBlockComponent } from './embed-loom-block';
import {
EmbedLoomBlockOptionConfig,
@@ -40,4 +41,5 @@ export const EmbedLoomViewExtensions: ExtensionType[] = [
EmbedLoomBlockOptionConfig,
createBuiltinToolbarConfigExtension(flavour, EmbedLoomBlockComponent),
SlashMenuConfigExtension(flavour, embedLoomSlashMenuConfig),
EmbedLoomBlockInteraction,
].flat();
@@ -1,6 +1,13 @@
import { EmbedYoutubeBlockSchema } from '@blocksuite/affine-model';
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
import { EmbedYoutubeBlockComponent } from './embed-youtube-block.js';
export class EmbedEdgelessYoutubeBlockComponent extends toEdgelessEmbedBlock(
EmbedYoutubeBlockComponent
) {}
export const EmbedYoutubeBlockInteraction = createEmbedEdgelessBlockInteraction(
EmbedYoutubeBlockSchema.model.flavour
);
@@ -5,7 +5,7 @@ import {
} from '@blocksuite/affine-model';
import {
EmbedOptionConfig,
LinkPreviewerService,
LinkPreviewServiceIdentifier,
} from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/std';
@@ -21,7 +21,7 @@ export class EmbedYoutubeBlockService extends BlockService {
) => {
return queryEmbedYoutubeData(
embedYoutubeModel,
this.doc.get(LinkPreviewerService),
this.std.get(LinkPreviewServiceIdentifier),
signal
);
};
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
import { EmbedYoutubeBlockAdapterExtensions } from './adapters/extension';
import { embedYoutubeSlashMenuConfig } from './configs/slash-menu';
import { EmbedYoutubeBlockInteraction } from './embed-edgeless-youtube-block';
import { EmbedYoutubeBlockComponent } from './embed-youtube-block';
import {
EmbedYoutubeBlockOptionConfig,
@@ -40,4 +41,5 @@ export const EmbedYoutubeViewExtensions: ExtensionType[] = [
EmbedYoutubeBlockOptionConfig,
createBuiltinToolbarConfigExtension(flavour, EmbedYoutubeBlockComponent),
SlashMenuConfigExtension('affine:embed-youtube', embedYoutubeSlashMenuConfig),
EmbedYoutubeBlockInteraction,
].flat();
@@ -2,14 +2,14 @@ import type {
EmbedYoutubeBlockUrlData,
EmbedYoutubeModel,
} from '@blocksuite/affine-model';
import type { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import type { LinkPreviewProvider } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import type { EmbedYoutubeBlockComponent } from './embed-youtube-block.js';
export async function queryEmbedYoutubeData(
embedYoutubeModel: EmbedYoutubeModel,
linkPreviewer: LinkPreviewerService,
linkPreviewer: LinkPreviewProvider,
signal?: AbortSignal
): Promise<Partial<EmbedYoutubeBlockUrlData>> {
const url = embedYoutubeModel.props.url;
+4 -1
View File
@@ -20,7 +20,10 @@ export const EmbedExtensions: ExtensionType[] = [
export { createEmbedBlockHtmlAdapterMatcher } from './common/adapters/html';
export { createEmbedBlockMarkdownAdapterMatcher } from './common/adapters/markdown';
export { createEmbedBlockPlainTextAdapterMatcher } from './common/adapters/plain-text';
export { EmbedBlockComponent } from './common/embed-block-element';
export {
createEmbedEdgelessBlockInteraction,
EmbedBlockComponent,
} from './common/embed-block-element';
export * from './common/embed-note-content-styles';
export { insertEmbedCard } from './common/insert-embed-card';
export * from './common/render-linked-doc';
@@ -8,26 +8,32 @@ import {
EdgelessClipboardEmbedFigmaConfig,
EmbedFigmaViewExtensions,
} from './embed-figma-block';
import { EmbedFigmaBlockInteraction } from './embed-figma-block/embed-edgeless-figma-block';
import {
EdgelessClipboardEmbedGithubConfig,
EmbedGithubViewExtensions,
} from './embed-github-block';
import { EmbedGithubBlockInteraction } from './embed-github-block/embed-edgeless-github-block';
import {
EdgelessClipboardEmbedHtmlConfig,
EmbedHtmlViewExtensions,
} from './embed-html-block';
import { EmbedEdgelessHtmlBlockInteraction } from './embed-html-block/embed-edgeless-html-block';
import {
EdgelessClipboardEmbedIframeConfig,
EmbedIframeViewExtensions,
} from './embed-iframe-block';
import { EmbedIframeInteraction } from './embed-iframe-block/embed-edgeless-iframe-block';
import {
EdgelessClipboardEmbedLoomConfig,
EmbedLoomViewExtensions,
} from './embed-loom-block';
import { EmbedLoomBlockInteraction } from './embed-loom-block/embed-edgeless-loom-bock';
import {
EdgelessClipboardEmbedYoutubeConfig,
EmbedYoutubeViewExtensions,
} from './embed-youtube-block';
import { EmbedYoutubeBlockInteraction } from './embed-youtube-block/embed-edgeless-youtube-block';
export class EmbedViewExtension extends ViewExtensionProvider {
override name = 'affine-embed-block';
@@ -54,6 +60,12 @@ export class EmbedViewExtension extends ViewExtensionProvider {
EdgelessClipboardEmbedLoomConfig,
EdgelessClipboardEmbedYoutubeConfig,
EdgelessClipboardEmbedIframeConfig,
EmbedFigmaBlockInteraction,
EmbedGithubBlockInteraction,
EmbedEdgelessHtmlBlockInteraction,
EmbedLoomBlockInteraction,
EmbedYoutubeBlockInteraction,
EmbedIframeInteraction,
]);
}
}
@@ -1,13 +1,28 @@
import { DefaultTheme, type FrameBlockModel } from '@blocksuite/affine-model';
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
import {
DefaultTheme,
type FrameBlockModel,
FrameBlockSchema,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { Bound } from '@blocksuite/global/gfx';
import { GfxBlockComponent } from '@blocksuite/std';
import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
import {
type BoxSelectionContext,
getTopElements,
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import {
EdgelessFrameManagerIdentifier,
type FrameOverlay,
} from './frame-manager';
export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
override connectedCallback() {
super.connectedCallback();
@@ -115,3 +130,64 @@ declare global {
'affine-frame': FrameBlockComponent;
}
}
export const FrameBlockInteraction =
GfxViewInteractionExtension<FrameBlockComponent>(
FrameBlockSchema.model.flavour,
{
handleResize: context => {
const { model, std } = context;
return {
onResizeStart(context): void {
context.default(context);
model.stash('childElementIds');
},
onResizeMove(context): void {
const { newBound } = context;
const frameManager = std.getOptional(
EdgelessFrameManagerIdentifier
);
const overlay = std.getOptional(
OverlayIdentifier('frame')
) as FrameOverlay;
model.xywh = newBound.serialize();
if (!frameManager) {
return;
}
const oldChildren = frameManager.getChildElementsInFrame(model);
const newChildren = getTopElements(
frameManager.getElementsInFrameBound(model)
).concat(
oldChildren.filter(oldChild => {
return model.intersectsBound(oldChild.elementBound);
})
);
frameManager.removeAllChildrenFromFrame(model);
frameManager.addElementsToFrame(model, newChildren);
overlay?.highlight(model, true, false);
},
onResizeEnd(context): void {
context.default(context);
model.pop('childElementIds');
},
};
},
handleRotate: () => {
return {
beforeRotate(context): void {
context.set({
rotatable: false,
});
},
};
},
}
);
@@ -3,6 +3,7 @@ import { BlockViewExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { FrameBlockInteraction } from './frame-block';
import { EdgelessFrameManager, FrameOverlay } from './frame-manager';
const flavour = FrameBlockSchema.model.flavour;
@@ -11,4 +12,5 @@ export const FrameBlockSpec: ExtensionType[] = [
BlockViewExtension(flavour, literal`affine-frame`),
FrameOverlay,
EdgelessFrameManager,
FrameBlockInteraction,
];
@@ -6,6 +6,7 @@ import {
import { EdgelessClipboardFrameConfig } from './edgeless-clipboard-config';
import { frameQuickTool } from './edgeless-toolbar';
import { effects } from './effects';
import { FrameBlockInteraction } from './frame-block';
import { FrameHighlightManager } from './frame-highlight-manager';
import { FrameBlockSpec } from './frame-spec';
import { FrameTool } from './frame-tool';
@@ -32,6 +33,7 @@ export class FrameViewExtension extends ViewExtensionProvider {
context.register(frameToolbarExtension);
context.register(edgelessNavigatorBgWidget);
context.register(EdgelessClipboardFrameConfig);
context.register(FrameBlockInteraction);
}
}
}
@@ -2,12 +2,16 @@ import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import { ResourceController } from '@blocksuite/affine-components/resource';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import {
type ImageBlockModel,
ImageBlockSchema,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
import { GfxBlockComponent } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { computed } from '@preact/signals-core';
import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
@@ -172,6 +176,15 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
accessor resizableImg!: HTMLDivElement;
}
export const ImageEdgelessBlockInteraction = GfxViewInteractionExtension(
ImageBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: true,
},
}
);
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-image': ImageEdgelessBlockComponent;
@@ -6,6 +6,7 @@ import { literal } from 'lit/static-html.js';
import { imageSlashMenuConfig } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { ImageEdgelessBlockInteraction } from './image-edgeless-block';
import { ImageDropOption } from './image-service';
const flavour = ImageBlockSchema.model.flavour;
@@ -22,6 +23,7 @@ export const ImageBlockSpec: ExtensionType[] = [
return literal`affine-image`;
}),
ImageDropOption,
ImageEdgelessBlockInteraction,
createBuiltinToolbarConfigExtension(flavour),
SlashMenuConfigExtension(flavour, imageSlashMenuConfig),
].flat();
@@ -5,6 +5,7 @@ import {
import { EdgelessClipboardImageConfig } from './edgeless-clipboard-config';
import { effects } from './effects';
import { ImageEdgelessBlockInteraction } from './image-edgeless-block';
import { ImageBlockSpec } from './image-spec';
export class ImageViewExtension extends ViewExtensionProvider {
@@ -20,6 +21,7 @@ export class ImageViewExtension extends ViewExtensionProvider {
context.register(ImageBlockSpec);
if (this.isEdgeless(context.scope)) {
context.register(EdgelessClipboardImageConfig);
context.register(ImageEdgelessBlockInteraction);
}
}
}
@@ -1,6 +1,6 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { DocTitle } from '@blocksuite/affine-fragment-doc-title';
import { NoteDisplayMode } from '@blocksuite/affine-model';
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
@@ -10,7 +10,11 @@ import {
} from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx';
import { toGfxBlockComponent } from '@blocksuite/std';
import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
import {
type BoxSelectionContext,
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import { html, nothing, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -432,3 +436,62 @@ declare global {
[AFFINE_EDGELESS_NOTE]: EdgelessNoteBlockComponent;
}
}
export const EdgelessNoteInteraction =
GfxViewInteractionExtension<EdgelessNoteBlockComponent>(
NoteBlockSchema.model.flavour,
{
resizeConstraint: {
minWidth: 170 + 24 * 2,
minHeight: 92,
},
handleRotate: () => {
return {
beforeRotate(context) {
context.set({
rotatable: false,
});
},
};
},
handleResize: ({ model }) => {
const initialScale: number = model.props.edgeless.scale ?? 1;
return {
onResizeStart(context): void {
context.default(context);
model.stash('edgeless');
},
onResizeMove(context): void {
const { originalBound, newBound, lockRatio, constraint } = context;
const { minWidth, minHeight } = constraint;
let scale = initialScale;
let edgelessProp = { ...model.props.edgeless };
const originalRealWidth = originalBound.w / scale;
if (lockRatio) {
scale = newBound.w / originalRealWidth;
edgelessProp.scale = scale;
}
newBound.w = clamp(newBound.w, minWidth, Number.MAX_SAFE_INTEGER);
newBound.h = clamp(newBound.h, minHeight, Number.MAX_SAFE_INTEGER);
if (newBound.h > minHeight * scale) {
edgelessProp.collapse = true;
edgelessProp.collapsedHeight = newBound.h / scale;
}
model.props.edgeless = edgelessProp;
model.props.xywh = newBound.serialize();
},
onResizeEnd(context): void {
context.default(context);
model.pop('edgeless');
},
};
},
}
);
@@ -9,6 +9,7 @@ import {
} from './adapters/index';
import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EdgelessNoteInteraction } from './note-edgeless-block';
import { NoteKeymapExtension } from './note-keymap.js';
const flavour = NoteBlockSchema.model.flavour;
@@ -28,4 +29,5 @@ export const EdgelessNoteBlockSpec: ExtensionType[] = [
NoteSlashMenuConfigExtension,
createBuiltinToolbarConfigExtension(flavour),
NoteKeymapExtension,
EdgelessNoteInteraction,
].flat();
@@ -10,6 +10,7 @@ import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EdgelessClipboardNoteConfig } from './edgeless-clipboard-config';
import { effects } from './effects';
import { EdgelessNoteInteraction } from './note-edgeless-block';
import { NoteKeymapExtension } from './note-keymap';
const flavour = NoteBlockSchema.model.flavour;
@@ -38,6 +39,7 @@ export class NoteViewExtension extends ViewExtensionProvider {
);
context.register(createBuiltinToolbarConfigExtension(flavour));
context.register(EdgelessClipboardNoteConfig);
context.register(EdgelessNoteInteraction);
} else {
context.register(BlockViewExtension(flavour, literal`affine-note`));
}
@@ -28,10 +28,9 @@ import { z } from 'zod';
import { effects } from './effects';
const optionsSchema = z.object({
getPlaceholder: z
.function()
.args(z.instanceof(ParagraphBlockModel))
.returns(z.string()),
getPlaceholder: z.optional(
z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string())
),
});
export class ParagraphViewExtension extends ViewExtensionProvider<
@@ -50,7 +49,7 @@ export class ParagraphViewExtension extends ViewExtensionProvider<
context: ViewExtensionContext,
options?: z.infer<typeof optionsSchema>
) {
super.setup(context);
super.setup(context, options);
const getPlaceholder =
options?.getPlaceholder ?? (model => placeholders[model.props.type]);
@@ -691,6 +691,12 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
})
);
_disposables.add(
gfx.selection.slots.updated.subscribe(() => {
this.requestUpdate();
})
);
_disposables.add(() => this.removeOverlay());
_disposables.add(
@@ -716,6 +722,15 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
});
}
private _canAutoComplete() {
const selection = this.gfx.selection;
return (
selection.selectedElements.length === 1 &&
(selection.selectedElements[0] instanceof ShapeElementModel ||
isNoteBlock(selection.selectedElements[0]))
);
}
removeOverlay() {
this._timer && clearTimeout(this._timer);
const surface = getSurfaceComponent(this.std);
@@ -727,7 +742,10 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
const isShape = this.current instanceof ShapeElementModel;
const isMindMap = this.current.group instanceof MindmapElementModel;
if (this._isMoving || (this._isHover && !isShape)) {
if (
this._isMoving ||
(this._isHover && !isShape && this._canAutoComplete())
) {
this.removeOverlay();
return nothing;
}
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
import type { IVec } from '@blocksuite/global/gfx';
import type { ResizeHandle } from '@blocksuite/std/gfx';
import { html, nothing } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
export enum HandleDirection {
Bottom = 'bottom',
@@ -12,62 +13,51 @@ export enum HandleDirection {
TopRight = 'top-right',
}
function ResizeHandle(
handleDirection: HandleDirection,
onPointerDown?: (e: PointerEvent, direction: HandleDirection) => void,
updateCursor?: (
dragging: boolean,
options?: {
type: 'resize' | 'rotate';
target?: HTMLElement;
point?: IVec;
}
) => void,
hideEdgeHandle?: boolean
function ResizeHandleRenderer(
handle: ResizeHandle,
rotatable: boolean,
onPointerDown?: (e: PointerEvent, direction: ResizeHandle) => void,
updateCursor?: (options?: {
type: 'resize' | 'rotate';
handle: ResizeHandle;
}) => void
) {
const handlerPointerDown = (e: PointerEvent) => {
e.stopPropagation();
onPointerDown && onPointerDown(e, handleDirection);
onPointerDown && onPointerDown(e, handle);
};
const pointerEnter = (type: 'resize' | 'rotate') => (e: PointerEvent) => {
e.stopPropagation();
if (e.buttons === 1 || !updateCursor) return;
const { clientX, clientY } = e;
const target = e.target as HTMLElement;
const point: IVec = [clientX, clientY];
updateCursor(true, { type, point, target });
updateCursor({ type, handle });
};
const pointerLeave = (e: PointerEvent) => {
e.stopPropagation();
if (e.buttons === 1 || !updateCursor) return;
updateCursor(false);
updateCursor();
};
const rotationTpl =
handleDirection === HandleDirection.Top ||
handleDirection === HandleDirection.Bottom ||
handleDirection === HandleDirection.Left ||
handleDirection === HandleDirection.Right
? nothing
: html`<div
handle.length > 6 && rotatable
? html`<div
class="rotate"
@pointerover=${pointerEnter('rotate')}
@pointerout=${pointerLeave}
></div>`;
></div>`
: nothing;
return html`<div
class="handle"
aria-label=${handleDirection}
aria-label=${handle}
@pointerdown=${handlerPointerDown}
>
${rotationTpl}
<div
class="resize${hideEdgeHandle && ' transparent-handle'}"
class="resize transparent-handle"
@pointerover=${pointerEnter('resize')}
@pointerout=${pointerLeave}
></div>
@@ -85,135 +75,21 @@ function ResizeHandle(
*/
export type ResizeMode = 'edge' | 'all' | 'none' | 'corner' | 'edgeAndCorner';
export function ResizeHandles(
resizeMode: ResizeMode,
onPointerDown: (e: PointerEvent, direction: HandleDirection) => void,
updateCursor?: (
dragging: boolean,
options?: {
type: 'resize' | 'rotate';
target?: HTMLElement;
point?: IVec;
}
) => void
export function RenderResizeHandles(
resizeHandles: ResizeHandle[],
rotatable: boolean,
onPointerDown: (e: PointerEvent, direction: ResizeHandle) => void,
updateCursor?: (options?: {
type: 'resize' | 'rotate';
handle: ResizeHandle;
}) => void
) {
const getCornerHandles = () => {
const handleTopLeft = ResizeHandle(
HandleDirection.TopLeft,
onPointerDown,
updateCursor
);
const handleTopRight = ResizeHandle(
HandleDirection.TopRight,
onPointerDown,
updateCursor
);
const handleBottomLeft = ResizeHandle(
HandleDirection.BottomLeft,
onPointerDown,
updateCursor
);
const handleBottomRight = ResizeHandle(
HandleDirection.BottomRight,
onPointerDown,
updateCursor
);
return {
handleTopLeft,
handleTopRight,
handleBottomLeft,
handleBottomRight,
};
};
const getEdgeHandles = (hideEdgeHandle?: boolean) => {
const handleLeft = ResizeHandle(
HandleDirection.Left,
onPointerDown,
updateCursor,
hideEdgeHandle
);
const handleRight = ResizeHandle(
HandleDirection.Right,
onPointerDown,
updateCursor,
hideEdgeHandle
);
return { handleLeft, handleRight };
};
const getEdgeVerticalHandles = (hideEdgeHandle?: boolean) => {
const handleTop = ResizeHandle(
HandleDirection.Top,
onPointerDown,
updateCursor,
hideEdgeHandle
);
const handleBottom = ResizeHandle(
HandleDirection.Bottom,
onPointerDown,
updateCursor,
hideEdgeHandle
);
return { handleTop, handleBottom };
};
switch (resizeMode) {
case 'corner': {
const {
handleTopLeft,
handleTopRight,
handleBottomLeft,
handleBottomRight,
} = getCornerHandles();
// prettier-ignore
return html`
${handleTopLeft}
${handleTopRight}
${handleBottomLeft}
${handleBottomRight}
`;
}
case 'edge': {
const { handleLeft, handleRight } = getEdgeHandles();
return html`${handleLeft} ${handleRight}`;
}
case 'all': {
const {
handleTopLeft,
handleTopRight,
handleBottomLeft,
handleBottomRight,
} = getCornerHandles();
const { handleLeft, handleRight } = getEdgeHandles(true);
const { handleTop, handleBottom } = getEdgeVerticalHandles(true);
// prettier-ignore
return html`
${handleTopLeft}
${handleTop}
${handleTopRight}
${handleRight}
${handleBottomRight}
${handleBottom}
${handleBottomLeft}
${handleLeft}
`;
}
case 'edgeAndCorner': {
const {
handleTopLeft,
handleTopRight,
handleBottomLeft,
handleBottomRight,
} = getCornerHandles();
const { handleLeft, handleRight } = getEdgeHandles(true);
return html`
${handleTopLeft} ${handleTopRight} ${handleRight} ${handleBottomRight}
${handleBottomLeft} ${handleLeft}
`;
}
case 'none': {
return nothing;
}
}
return html`
${repeat(
resizeHandles,
handle => handle,
handle =>
ResizeHandleRenderer(handle, rotatable, onPointerDown, updateCursor)
)}
`;
}
@@ -1,705 +0,0 @@
import { NOTE_MIN_WIDTH } from '@blocksuite/affine-model';
import {
Bound,
getQuadBoundWithRotation,
type IPoint,
type IVec,
type PointLocation,
rotatePoints,
} from '@blocksuite/global/gfx';
import type { SelectableProps } from '../../utils/query.js';
import { HandleDirection, type ResizeMode } from './resize-handles.js';
// 15deg
const SHIFT_LOCKING_ANGLE = Math.PI / 12;
type DragStartHandler = () => void;
type DragEndHandler = () => void;
type ResizeMoveHandler = (
bounds: Map<
string,
{
bound: Bound;
path?: PointLocation[];
matrix?: DOMMatrix;
}
>,
direction: HandleDirection
) => void;
type RotateMoveHandler = (point: IPoint, rotate: number) => void;
export class HandleResizeManager {
private _aspectRatio = 1;
private _bounds = new Map<
string,
{
bound: Bound;
rotate: number;
}
>();
/**
* Current rect of selected elements, it may change during resizing or moving
*/
private _currentRect = new DOMRect();
private _dragDirection: HandleDirection = HandleDirection.Left;
private _dragging = false;
private _dragPos: {
start: { x: number; y: number };
end: { x: number; y: number };
} = {
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
};
private _locked = false;
private readonly _onDragEnd: DragEndHandler;
private readonly _onDragStart: DragStartHandler;
private readonly _onResizeMove: ResizeMoveHandler;
private readonly _onRotateMove: RotateMoveHandler;
private _origin: { x: number; y: number } = { x: 0, y: 0 };
/**
* Record inital rect of selected elements
*/
private _originalRect = new DOMRect();
private _proportion = false;
private _proportional = false;
private _resizeMode: ResizeMode = 'none';
private _rotate = 0;
private _rotation = false;
private _shiftKey = false;
private _target: HTMLElement | null = null;
private _zoom = 1;
onPointerDown = (
e: PointerEvent,
direction: HandleDirection,
proportional = false
) => {
// Prevent selection action from being triggered
e.stopPropagation();
this._locked = false;
this._target = e.target as HTMLElement;
this._dragDirection = direction;
this._dragPos.start = { x: e.x, y: e.y };
this._dragPos.end = { x: e.x, y: e.y };
this._rotation = this._target.classList.contains('rotate');
this._proportional = proportional;
if (this._rotation) {
const rect = this._target
.closest('.affine-edgeless-selected-rect')
?.getBoundingClientRect();
if (!rect) {
return;
}
const { left, top, right, bottom } = rect;
const x = (left + right) / 2;
const y = (top + bottom) / 2;
// center of `selected-rect` in viewport
this._origin = { x, y };
}
this._dragging = true;
this._onDragStart();
const _onPointerMove = ({ x, y, shiftKey }: PointerEvent) => {
if (this._resizeMode === 'none') return;
this._shiftKey = shiftKey;
this._dragPos.end = { x, y };
const proportional = this._proportional || this._shiftKey;
if (this._rotation) {
this._onRotate(proportional);
return;
}
this._onResize(proportional);
};
const _onPointerUp = (_: PointerEvent) => {
this._dragging = false;
this._onDragEnd();
const { x, y, width, height } = this._currentRect;
this._originalRect = new DOMRect(x, y, width, height);
this._locked = true;
this._shiftKey = false;
this._rotation = false;
this._dragPos = {
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
};
document.removeEventListener('pointermove', _onPointerMove);
document.removeEventListener('pointerup', _onPointerUp);
};
document.addEventListener('pointermove', _onPointerMove);
document.addEventListener('pointerup', _onPointerUp);
};
get bounds() {
return this._bounds;
}
get currentRect() {
return this._currentRect;
}
get dragDirection() {
return this._dragDirection;
}
get dragging() {
return this._dragging;
}
get originalRect() {
return this._originalRect;
}
get rotation() {
return this._rotation;
}
constructor(
onDragStart: DragStartHandler,
onResizeMove: ResizeMoveHandler,
onRotateMove: RotateMoveHandler,
onDragEnd: DragEndHandler
) {
this._onDragStart = onDragStart;
this._onResizeMove = onResizeMove;
this._onRotateMove = onRotateMove;
this._onDragEnd = onDragEnd;
}
private _onResize(proportion: boolean) {
const {
_aspectRatio,
_dragDirection,
_dragPos,
_rotate,
_resizeMode,
_zoom,
_originalRect,
_currentRect,
} = this;
proportion ||= this._proportion;
const isAll = _resizeMode === 'all';
const isCorner = _resizeMode === 'corner';
const isEdgeAndCorner = _resizeMode === 'edgeAndCorner';
const {
start: { x: startX, y: startY },
end: { x: endX, y: endY },
} = _dragPos;
const { left: minX, top: minY, right: maxX, bottom: maxY } = _originalRect;
const original = {
w: maxX - minX,
h: maxY - minY,
cx: (minX + maxX) / 2,
cy: (minY + maxY) / 2,
};
const rect = { ...original };
const scale = { x: 1, y: 1 };
const flip = { x: 1, y: 1 };
const direction = { x: 1, y: 1 };
const fixedPoint = new DOMPoint(0, 0);
const draggingPoint = new DOMPoint(0, 0);
const deltaX = (endX - startX) / _zoom;
const deltaY = (endY - startY) / _zoom;
const m0 = new DOMMatrix()
.translateSelf(original.cx, original.cy)
.rotateSelf(_rotate)
.translateSelf(-original.cx, -original.cy);
if (isCorner || isAll || isEdgeAndCorner) {
switch (_dragDirection) {
case HandleDirection.TopLeft: {
direction.x = -1;
direction.y = -1;
fixedPoint.x = maxX;
fixedPoint.y = maxY;
draggingPoint.x = minX;
draggingPoint.y = minY;
break;
}
case HandleDirection.TopRight: {
direction.x = 1;
direction.y = -1;
fixedPoint.x = minX;
fixedPoint.y = maxY;
draggingPoint.x = maxX;
draggingPoint.y = minY;
break;
}
case HandleDirection.BottomRight: {
direction.x = 1;
direction.y = 1;
fixedPoint.x = minX;
fixedPoint.y = minY;
draggingPoint.x = maxX;
draggingPoint.y = maxY;
break;
}
case HandleDirection.BottomLeft: {
direction.x = -1;
direction.y = 1;
fixedPoint.x = maxX;
fixedPoint.y = minY;
draggingPoint.x = minX;
draggingPoint.y = maxY;
break;
}
case HandleDirection.Left: {
direction.x = -1;
direction.y = 1;
fixedPoint.x = maxX;
fixedPoint.y = original.cy;
draggingPoint.x = minX;
draggingPoint.y = original.cy;
break;
}
case HandleDirection.Right: {
direction.x = 1;
direction.y = 1;
fixedPoint.x = minX;
fixedPoint.y = original.cy;
draggingPoint.x = maxX;
draggingPoint.y = original.cy;
break;
}
case HandleDirection.Top: {
const cx = (minX + maxX) / 2;
direction.x = 1;
direction.y = -1;
fixedPoint.x = cx;
fixedPoint.y = maxY;
draggingPoint.x = cx;
draggingPoint.y = minY;
break;
}
case HandleDirection.Bottom: {
const cx = (minX + maxX) / 2;
direction.x = 1;
direction.y = 1;
fixedPoint.x = cx;
fixedPoint.y = minY;
draggingPoint.x = cx;
draggingPoint.y = maxY;
break;
}
}
// force adjustment by aspect ratio
proportion ||= this._bounds.size > 1;
const fp = fixedPoint.matrixTransform(m0);
let dp = draggingPoint.matrixTransform(m0);
dp.x += deltaX;
dp.y += deltaY;
if (
_dragDirection === HandleDirection.Left ||
_dragDirection === HandleDirection.Right ||
_dragDirection === HandleDirection.Top ||
_dragDirection === HandleDirection.Bottom
) {
const dpo = draggingPoint.matrixTransform(m0);
const coorPoint: IVec = [0, 0];
const [[x1, y1]] = rotatePoints([[dpo.x, dpo.y]], coorPoint, -_rotate);
const [[x2, y2]] = rotatePoints([[dp.x, dp.y]], coorPoint, -_rotate);
const point = { x: 0, y: 0 };
if (
_dragDirection === HandleDirection.Left ||
_dragDirection === HandleDirection.Right
) {
point.x = x2;
point.y = y1;
} else {
point.x = x1;
point.y = y2;
}
const [[x3, y3]] = rotatePoints(
[[point.x, point.y]],
coorPoint,
_rotate
);
dp.x = x3;
dp.y = y3;
}
const cx = (fp.x + dp.x) / 2;
const cy = (fp.y + dp.y) / 2;
const m1 = new DOMMatrix()
.translateSelf(cx, cy)
.rotateSelf(-_rotate)
.translateSelf(-cx, -cy);
const f = fp.matrixTransform(m1);
const d = dp.matrixTransform(m1);
switch (_dragDirection) {
case HandleDirection.TopLeft: {
rect.w = f.x - d.x;
rect.h = f.y - d.y;
break;
}
case HandleDirection.TopRight: {
rect.w = d.x - f.x;
rect.h = f.y - d.y;
break;
}
case HandleDirection.BottomRight: {
rect.w = d.x - f.x;
rect.h = d.y - f.y;
break;
}
case HandleDirection.BottomLeft: {
rect.w = f.x - d.x;
rect.h = d.y - f.y;
break;
}
case HandleDirection.Left: {
rect.w = f.x - d.x;
break;
}
case HandleDirection.Right: {
rect.w = d.x - f.x;
break;
}
case HandleDirection.Top: {
rect.h = f.y - d.y;
break;
}
case HandleDirection.Bottom: {
rect.h = d.y - f.y;
break;
}
}
rect.cx = (d.x + f.x) / 2;
rect.cy = (d.y + f.y) / 2;
scale.x = rect.w / original.w;
scale.y = rect.h / original.h;
flip.x = scale.x < 0 ? -1 : 1;
flip.y = scale.y < 0 ? -1 : 1;
const isDraggingCorner =
_dragDirection === HandleDirection.TopLeft ||
_dragDirection === HandleDirection.TopRight ||
_dragDirection === HandleDirection.BottomRight ||
_dragDirection === HandleDirection.BottomLeft;
// lock aspect ratio
if (proportion && isDraggingCorner) {
const newAspectRatio = Math.abs(rect.w / rect.h);
if (_aspectRatio < newAspectRatio) {
scale.y = Math.abs(scale.x) * flip.y;
rect.h = scale.y * original.h;
} else {
scale.x = Math.abs(scale.y) * flip.x;
rect.w = scale.x * original.w;
}
draggingPoint.x = fixedPoint.x + rect.w * direction.x;
draggingPoint.y = fixedPoint.y + rect.h * direction.y;
dp = draggingPoint.matrixTransform(m0);
rect.cx = (fp.x + dp.x) / 2;
rect.cy = (fp.y + dp.y) / 2;
}
} else {
// handle notes
switch (_dragDirection) {
case HandleDirection.Left: {
direction.x = -1;
fixedPoint.x = maxX;
draggingPoint.x = minX + deltaX;
rect.w = fixedPoint.x - draggingPoint.x;
break;
}
case HandleDirection.Right: {
direction.x = 1;
fixedPoint.x = minX;
draggingPoint.x = maxX + deltaX;
rect.w = draggingPoint.x - fixedPoint.x;
break;
}
}
scale.x = rect.w / original.w;
flip.x = scale.x < 0 ? -1 : 1;
if (Math.abs(rect.w) < NOTE_MIN_WIDTH) {
rect.w = NOTE_MIN_WIDTH * flip.x;
scale.x = rect.w / original.w;
draggingPoint.x = fixedPoint.x + rect.w * direction.x;
}
rect.cx = (draggingPoint.x + fixedPoint.x) / 2;
}
const width = Math.abs(rect.w);
const height = Math.abs(rect.h);
const x = rect.cx - width / 2;
const y = rect.cy - height / 2;
_currentRect.x = x;
_currentRect.y = y;
_currentRect.width = width;
_currentRect.height = height;
const newBounds = new Map<
string,
{
bound: Bound;
path?: PointLocation[];
matrix?: DOMMatrix;
}
>();
let process: (value: SelectableProps, key: string) => void;
if (isCorner || isAll || isEdgeAndCorner) {
if (this._bounds.size === 1) {
process = (_, id) => {
newBounds.set(id, {
bound: new Bound(x, y, width, height),
});
};
} else {
const fp = fixedPoint.matrixTransform(m0);
const m2 = new DOMMatrix()
.translateSelf(fp.x, fp.y)
.rotateSelf(_rotate)
.translateSelf(-fp.x, -fp.y)
.scaleSelf(scale.x, scale.y, 1, fp.x, fp.y, 0)
.translateSelf(fp.x, fp.y)
.rotateSelf(-_rotate)
.translateSelf(-fp.x, -fp.y);
// TODO: on same rotate
process = ({ bound: { x, y, w, h }, path }, id) => {
const cx = x + w / 2;
const cy = y + h / 2;
const center = new DOMPoint(cx, cy).matrixTransform(m2);
const newWidth = Math.abs(w * scale.x);
const newHeight = Math.abs(h * scale.y);
newBounds.set(id, {
bound: new Bound(
center.x - newWidth / 2,
center.y - newHeight / 2,
newWidth,
newHeight
),
matrix: m2,
path,
});
};
}
} else {
// include notes, <---->
const m2 = new DOMMatrix().scaleSelf(
scale.x,
scale.y,
1,
fixedPoint.x,
fixedPoint.y,
0
);
process = ({ bound: { x, y, w, h }, rotate = 0, path }, id) => {
const cx = x + w / 2;
const cy = y + h / 2;
const center = new DOMPoint(cx, cy).matrixTransform(m2);
let newWidth: number;
let newHeight: number;
// TODO: determine if it is a note
if (rotate) {
const { width } = getQuadBoundWithRotation({ x, y, w, h, rotate });
const hrw = width / 2;
center.y = cy;
if (_currentRect.width <= width) {
newWidth = w * (_currentRect.width / width);
newHeight = newWidth / (w / h);
center.x = _currentRect.left + _currentRect.width / 2;
} else {
const p = (cx - hrw - _originalRect.left) / _originalRect.width;
const lx = _currentRect.left + p * _currentRect.width + hrw;
center.x = Math.max(
_currentRect.left + hrw,
Math.min(lx, _currentRect.left + _currentRect.width - hrw)
);
newWidth = w;
newHeight = h;
}
} else {
newWidth = Math.abs(w * scale.x);
newHeight = Math.abs(h * scale.y);
}
newBounds.set(id, {
bound: new Bound(
center.x - newWidth / 2,
center.y - newHeight / 2,
newWidth,
newHeight
),
matrix: m2,
path,
});
};
}
this._bounds.forEach(process);
this._onResizeMove(newBounds, this._dragDirection);
}
private _onRotate(shiftKey = false) {
const {
_originalRect: { left: minX, top: minY, right: maxX, bottom: maxY },
_dragPos: {
start: { x: startX, y: startY },
end: { x: endX, y: endY },
},
_origin: { x: centerX, y: centerY },
_rotate,
} = this;
const startRad = Math.atan2(startY - centerY, startX - centerX);
const endRad = Math.atan2(endY - centerY, endX - centerX);
let deltaRad = endRad - startRad;
// snap angle
// 15deg * n = 0, 15, 30, 45, ... 360
if (shiftKey) {
const prevRad = (_rotate * Math.PI) / 180;
let angle = prevRad + deltaRad;
angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE;
deltaRad = angle - prevRad;
}
const delta = (deltaRad * 180) / Math.PI;
let x = endX;
let y = endY;
if (shiftKey) {
const point = new DOMPoint(startX, startY).matrixTransform(
new DOMMatrix()
.translateSelf(centerX, centerY)
.rotateSelf(delta)
.translateSelf(-centerX, -centerY)
);
x = point.x;
y = point.y;
}
this._onRotateMove(
// center of element in suface
{ x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
delta
);
this._dragPos.start = { x, y };
this._rotate += delta;
}
onPressShiftKey(pressed: boolean) {
if (!this._target) return;
if (this._locked) return;
if (this._shiftKey === pressed) return;
this._shiftKey = pressed;
const proportional = this._proportional || this._shiftKey;
if (this._rotation) {
this._onRotate(proportional);
return;
}
this._onResize(proportional);
}
updateBounds(bounds: Map<string, SelectableProps>) {
this._bounds = bounds;
}
updateRectPosition(delta: { x: number; y: number }) {
this._currentRect.x += delta.x;
this._currentRect.y += delta.y;
this._originalRect.x = this._currentRect.x;
this._originalRect.y = this._currentRect.y;
return this._originalRect;
}
updateState(
resizeMode: ResizeMode,
rotate: number,
zoom: number,
position?: { x: number; y: number },
originalRect?: DOMRect,
proportion = false
) {
this._resizeMode = resizeMode;
this._rotate = rotate;
this._zoom = zoom;
this._proportion = proportion;
if (position) {
this._currentRect.x = position.x;
this._currentRect.y = position.y;
this._originalRect.x = this._currentRect.x;
this._originalRect.y = this._currentRect.y;
}
if (originalRect) {
this._originalRect = originalRect;
this._aspectRatio = originalRect.width / originalRect.height;
this._currentRect = DOMRect.fromRect(originalRect);
}
}
}
@@ -1,128 +1,65 @@
import type { IVec } from '@blocksuite/global/gfx';
import { normalizeDegAngle, Vec } from '@blocksuite/global/gfx';
import type { CursorType, StandardCursor } from '@blocksuite/std/gfx';
import type {
CursorType,
ResizeHandle,
StandardCursor,
} from '@blocksuite/std/gfx';
const rotateCursorMap: {
[key in ResizeHandle]: number;
} = {
'top-right': 0,
'bottom-right': 90,
'bottom-left': 180,
'top-left': 270,
// not used
left: 0,
right: 0,
top: 0,
bottom: 0,
};
export function generateCursorUrl(
angle = 0,
handle: ResizeHandle,
fallback: StandardCursor = 'default'
): CursorType {
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg transform='rotate(${angle} 16 16)'%3E%3Cpath fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/%3E%3Cpath d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/%3E%3C/g%3E%3C/svg%3E") 16 16, ${fallback}`;
angle = ((angle % 360) + 360) % 360;
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg transform='rotate(${rotateCursorMap[handle] + angle} 16 16)'%3E%3Cpath fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/%3E%3Cpath d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/%3E%3C/g%3E%3C/svg%3E") 16 16, ${fallback}`;
}
const RESIZE_CURSORS: CursorType[] = [
'ew-resize',
'nwse-resize',
'ns-resize',
'nesw-resize',
];
export function rotateResizeCursor(angle: number): StandardCursor {
const a = Math.round(angle / (Math.PI / 4));
const cursor = RESIZE_CURSORS[a % RESIZE_CURSORS.length];
return cursor as StandardCursor;
}
export function calcAngle(target: HTMLElement, point: IVec, offset = 0) {
const rect = target
.closest('.affine-edgeless-selected-rect')
?.getBoundingClientRect();
if (!rect) {
console.error('rect not found when calc angle');
return 0;
}
const { left, top, right, bottom } = rect;
const center = Vec.med([left, top], [right, bottom]);
return normalizeDegAngle(
((Vec.angle(center, point) + offset) * 180) / Math.PI
);
}
export function calcAngleWithRotation(
target: HTMLElement,
point: IVec,
rect: DOMRect,
rotate: number
) {
const handle = target.parentElement;
const ariaLabel = handle?.getAttribute('aria-label');
const { left, top, right, bottom, width, height } = rect;
const size = Math.min(width, height);
const sx = size / width;
const sy = size / height;
const center = Vec.med([left, top], [right, bottom]);
const draggingPoint = [0, 0];
switch (ariaLabel) {
case 'top-left': {
draggingPoint[0] = left;
draggingPoint[1] = top;
break;
}
case 'top-right': {
draggingPoint[0] = right;
draggingPoint[1] = top;
break;
}
case 'bottom-right': {
draggingPoint[0] = right;
draggingPoint[1] = bottom;
break;
}
case 'bottom-left': {
draggingPoint[0] = left;
draggingPoint[1] = bottom;
break;
}
}
const dp = new DOMMatrix()
.translateSelf(center[0], center[1])
.rotateSelf(rotate)
.translateSelf(-center[0], -center[1])
.transformPoint(new DOMPoint(...draggingPoint));
const m = new DOMMatrix()
.translateSelf(dp.x, dp.y)
.rotateSelf(rotate)
.translateSelf(-dp.x, -dp.y)
.scaleSelf(sx, sy, 1, dp.x, dp.y, 0)
.translateSelf(dp.x, dp.y)
.rotateSelf(-rotate)
.translateSelf(-dp.x, -dp.y);
const c = new DOMPoint(...center).matrixTransform(m);
return normalizeDegAngle((Vec.angle([c.x, c.y], point) * 180) / Math.PI);
}
export function calcAngleEdgeWithRotation(target: HTMLElement, rotate: number) {
let angleWithEdge = 0;
const handle = target.parentElement;
const ariaLabel = handle?.getAttribute('aria-label');
switch (ariaLabel) {
case 'top': {
angleWithEdge = 270;
break;
}
case 'bottom': {
angleWithEdge = 90;
break;
}
case 'left': {
angleWithEdge = 180;
break;
}
case 'right': {
angleWithEdge = 0;
break;
}
}
return angleWithEdge + rotate;
}
export function getResizeLabel(target: HTMLElement) {
const handle = target.parentElement;
const ariaLabel = handle?.getAttribute('aria-label');
return ariaLabel;
const handleToRotateMap: {
[key in ResizeHandle]: number;
} = {
'top-left': 45,
'top-right': 135,
'bottom-right': 45,
'bottom-left': 135,
left: 0,
right: 0,
top: 90,
bottom: 90,
};
const rotateToHandleMap: {
[key: number]: StandardCursor;
} = {
0: 'ew-resize',
45: 'nwse-resize',
90: 'ns-resize',
135: 'nesw-resize',
};
export function getRotatedResizeCursor(option: {
handle: ResizeHandle;
angle: number;
}) {
const angle =
(Math.round(
(handleToRotateMap[option.handle] + ((option.angle + 360) % 360)) / 45
) %
4) *
45;
return rotateToHandleMap[angle] || 'default';
}
@@ -9,13 +9,3 @@ export const ATTACHED_DISTANCE = 20;
export const SurfaceColor = '#6046FE';
export const NoteColor = '#1E96EB';
export const BlendColor = '#7D91FF';
export const AI_CHAT_BLOCK_MIN_WIDTH = 260;
export const AI_CHAT_BLOCK_MIN_HEIGHT = 160;
export const AI_CHAT_BLOCK_MAX_WIDTH = 320;
export const AI_CHAT_BLOCK_MAX_HEIGHT = 300;
export const EMBED_IFRAME_BLOCK_MIN_WIDTH = 218;
export const EMBED_IFRAME_BLOCK_MIN_HEIGHT = 44;
export const EMBED_IFRAME_BLOCK_MAX_WIDTH = 3400;
export const EMBED_IFRAME_BLOCK_MAX_HEIGHT = 2200;
@@ -10,7 +10,7 @@ export const PeekViewProvider = createIdentifier<PeekViewService>(
export function PeekViewExtension(service: PeekViewService): ExtensionType {
return {
setup: di => {
di.addImpl(PeekViewProvider, () => service);
di.override(PeekViewProvider, () => service);
},
};
}
@@ -1,5 +1,11 @@
import type { ColumnDataType } from '@blocksuite/affine-model';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import {
Container,
createScope,
type GeneralServiceIdentifier,
type ServiceProvider,
} from '@blocksuite/global/di';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { TypeInstance } from '../logical/type.js';
@@ -8,7 +14,6 @@ import type { DatabaseFlags } from '../types.js';
import type { ViewConvertConfig } from '../view/convert.js';
import type { DataViewDataType, ViewMeta } from '../view/data-view.js';
import type { ViewManager } from '../view-manager/view-manager.js';
import type { DataViewContextKey } from './context.js';
export interface DataSource {
readonly$: ReadonlySignal<boolean>;
@@ -65,7 +70,9 @@ export interface DataSource {
propertyDelete(id: string): void;
propertyCanDelete(propertyId: string): boolean;
contextGet<T>(key: DataViewContextKey<T>): T;
provider: ServiceProvider;
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null;
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T;
viewConverts: ViewConvertConfig[];
viewManager: ViewManager;
@@ -91,6 +98,8 @@ export interface DataSource {
viewMetaGetById$(viewId: string): ReadonlySignal<ViewMeta | undefined>;
}
export const DataSourceScope = createScope('data-source');
export abstract class DataSourceBase implements DataSource {
propertyTypeCanSet(propertyId: string): boolean {
return !this.isFixedProperty(propertyId);
@@ -101,7 +110,9 @@ export abstract class DataSourceBase implements DataSource {
propertyCanDelete(propertyId: string): boolean {
return !this.isFixedProperty(propertyId);
}
context = new Map<symbol, unknown>();
protected container = new Container();
abstract get parentProvider(): ServiceProvider;
abstract featureFlags$: ReadonlySignal<DatabaseFlags>;
@@ -144,12 +155,26 @@ export abstract class DataSourceBase implements DataSource {
return computed(() => this.cellValueGet(rowId, propertyId));
}
contextGet<T>(key: DataViewContextKey<T>): T {
return (this.context.get(key.key) as T) ?? key.defaultValue;
get provider() {
return this.container.provider(DataSourceScope, this.parentProvider);
}
contextSet<T>(key: DataViewContextKey<T>, value: T): void {
this.context.set(key.key, value);
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null {
return this.provider.getOptional(key);
}
serviceSet<T>(key: GeneralServiceIdentifier<T>, value: T): void {
this.container.addValue(key, value, { scope: DataSourceScope });
}
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T {
const result = this.serviceGet(key);
if (result != null) {
return result;
}
const value = create();
this.serviceSet(key, value);
return value;
}
abstract propertyAdd(
@@ -1,12 +0,0 @@
export interface DataViewContextKey<T> {
key: symbol;
defaultValue: T;
}
export const createContextKey = <T>(
name: string,
defaultValue: T
): DataViewContextKey<T> => ({
key: Symbol(name),
defaultValue,
});
@@ -1,2 +1 @@
export * from './base.js';
export * from './context.js';
@@ -1,7 +1,7 @@
import type { GroupBy } from '../common/types.js';
import type { DataSource } from '../data-source/index.js';
import type { PropertyMetaConfig } from '../property/property-config.js';
import { groupByMatcher } from './matcher.js';
import { getGroupByService } from './matcher.js';
export const defaultGroupBy = (
dataSource: DataSource,
@@ -9,7 +9,8 @@ export const defaultGroupBy = (
propertyId: string,
data: NonNullable<unknown>
): GroupBy | undefined => {
const name = groupByMatcher.match(
const groupByService = getGroupByService(dataSource);
const name = groupByService?.matcher.match(
propertyMeta.config.jsonValue.type({ data, dataSource })
)?.name;
return name != null
@@ -1,6 +1,6 @@
import hash from '@emotion/hash';
import { MatcherCreator } from '../logical/matcher.js';
import type { TypeInstance } from '../logical/type.js';
import { t } from '../logical/type-presets.js';
import { createUniComponentFromWebComponent } from '../utils/uni-component/uni-component.js';
import { BooleanGroupView } from './renderer/boolean-group.js';
@@ -8,15 +8,23 @@ import { NumberGroupView } from './renderer/number-group.js';
import { SelectGroupView } from './renderer/select-group.js';
import { StringGroupView } from './renderer/string-group.js';
import type { GroupByConfig } from './types.js';
const groupByMatcherCreator = new MatcherCreator<GroupByConfig>();
const ungroups = {
export const createGroupByConfig = <
Data extends Record<string, unknown>,
MatchType extends TypeInstance,
GroupValue = unknown,
>(
config: GroupByConfig<Data, MatchType, GroupValue>
): GroupByConfig => {
return config as never as GroupByConfig;
};
export const ungroups = {
key: 'Ungroups',
value: null,
};
export const groupByMatchers = [
groupByMatcherCreator.createMatcher(t.tag.instance(), {
createGroupByConfig({
name: 'select',
matchType: t.tag.instance(),
groupName: (type, value) => {
if (t.tag.is(type) && type.data) {
return type.data.find(v => v.id === value)?.value ?? '';
@@ -48,11 +56,12 @@ export const groupByMatchers = [
},
view: createUniComponentFromWebComponent(SelectGroupView),
}),
groupByMatcherCreator.createMatcher(t.array.instance(t.tag.instance()), {
createGroupByConfig({
name: 'multi-select',
groupName: (type, value) => {
if (t.tag.is(type) && type.data) {
return type.data.find(v => v.id === value)?.value ?? '';
matchType: t.array.instance(t.tag.instance()),
groupName: (type, value: string | null) => {
if (t.array.is(type) && t.tag.is(type.element) && type.element.data) {
return type.element.data.find(v => v.id === value)?.value ?? '';
}
return '';
},
@@ -94,8 +103,9 @@ export const groupByMatchers = [
},
view: createUniComponentFromWebComponent(SelectGroupView),
}),
groupByMatcherCreator.createMatcher(t.string.instance(), {
createGroupByConfig({
name: 'text',
matchType: t.string.instance(),
groupName: (_type, value) => {
return `${value ?? ''}`;
},
@@ -115,15 +125,16 @@ export const groupByMatchers = [
},
view: createUniComponentFromWebComponent(StringGroupView),
}),
groupByMatcherCreator.createMatcher(t.number.instance(), {
createGroupByConfig({
name: 'number',
groupName: (_type, value) => {
matchType: t.number.instance(),
groupName: (_type, value: number | null) => {
return `${value ?? ''}`;
},
defaultKeys: _type => {
return [ungroups];
},
valuesGroup: (value, _type) => {
valuesGroup: (value: number | null, _type) => {
if (typeof value !== 'number') {
return [ungroups];
}
@@ -137,8 +148,9 @@ export const groupByMatchers = [
addToGroup: value => (typeof value === 'number' ? value * 10 : null),
view: createUniComponentFromWebComponent(NumberGroupView),
}),
groupByMatcherCreator.createMatcher(t.boolean.instance(), {
createGroupByConfig({
name: 'boolean',
matchType: t.boolean.instance(),
groupName: (_type, value) => {
return `${value?.toString() ?? ''}`;
},
@@ -5,10 +5,10 @@ import { nothing } from 'lit';
import { html } from 'lit/static-html.js';
import { renderUniLit } from '../utils/uni-component/uni-component.js';
import type { GroupData } from './trait.js';
import type { Group } from './trait.js';
import type { GroupRenderProps } from './types.js';
function GroupHeaderCount(group: GroupData) {
function GroupHeaderCount(group: Group) {
const cards = group.rows;
if (!cards.length) {
return;
@@ -16,32 +16,25 @@ function GroupHeaderCount(group: GroupData) {
return html` <div class="group-header-count">${cards.length}</div>`;
}
const GroupTitleMobile = (
groupData: GroupData,
groupData: Group,
ops: {
readonly: boolean;
clickAdd: (evt: MouseEvent) => void;
clickOps: (evt: MouseEvent) => void;
}
) => {
const data = groupData.manager.config$.value;
if (!data) return nothing;
const type = groupData.tType;
if (!type) return nothing;
const icon =
groupData.value == null
? ''
: html` <uni-lit
class="group-header-icon"
.uni="${groupData.manager.property$.value?.icon}"
.uni="${groupData.property.icon}"
></uni-lit>`;
const props: GroupRenderProps = {
value: groupData.value,
data: groupData.property.data$.value,
updateData: groupData.manager.updateData,
updateValue: value =>
groupData.manager.updateValue(
groupData.rows.map(row => row.rowId),
value
),
group: groupData,
readonly: ops.readonly,
};
@@ -103,7 +96,7 @@ const GroupTitleMobile = (
<div
style="display:flex;align-items:center;gap: 8px;overflow: hidden;height: 22px;"
>
${icon} ${renderUniLit(data.view, props)} ${columnName}
${icon} ${renderUniLit(groupData.view, props)} ${columnName}
${GroupHeaderCount(groupData)}
</div>
${ops.readonly
@@ -120,7 +113,7 @@ const GroupTitleMobile = (
};
export const GroupTitle = (
groupData: GroupData,
groupData: Group,
ops: {
readonly: boolean;
clickAdd: (evt: MouseEvent) => void;
@@ -130,25 +123,18 @@ export const GroupTitle = (
if (IS_MOBILE) {
return GroupTitleMobile(groupData, ops);
}
const data = groupData.manager.config$.value;
if (!data) return nothing;
const type = groupData.tType;
if (!type) return nothing;
const icon =
groupData.value == null
? ''
: html` <uni-lit
class="group-header-icon"
.uni="${groupData.manager.property$.value?.icon}"
.uni="${groupData.property.icon}"
></uni-lit>`;
const props: GroupRenderProps = {
value: groupData.value,
data: groupData.property.data$.value,
updateData: groupData.manager.updateData,
updateValue: value =>
groupData.manager.updateValue(
groupData.rows.map(row => row.rowId),
value
),
group: groupData,
readonly: ops.readonly,
};
@@ -228,7 +214,7 @@ export const GroupTitle = (
<div
style="display:flex;align-items:center;gap: 8px;overflow: hidden;height: 22px;"
>
${icon} ${renderUniLit(data.view, props)} ${columnName}
${icon} ${renderUniLit(groupData.view, props)} ${columnName}
${GroupHeaderCount(groupData)}
</div>
${ops.readonly
@@ -1 +1,3 @@
export * from './define.js';
export * from './matcher.js';
export * from './trait.js';
@@ -1,5 +1,41 @@
import { Matcher } from '../logical/matcher.js';
import { createIdentifier } from '@blocksuite/global/di';
import type { DataSource } from '../data-source/base.js';
import { Matcher_ } from '../logical/matcher.js';
import { groupByMatchers } from './define.js';
import type { GroupByConfig } from './types.js';
export const groupByMatcher = new Matcher<GroupByConfig>(groupByMatchers);
export const createGroupByMatcher = (list: GroupByConfig[]) => {
return new Matcher_(list, v => v.matchType);
};
export class GroupByService {
constructor(private readonly dataSource: DataSource) {}
allExternalGroupByConfig(): GroupByConfig[] {
return Array.from(
this.dataSource.provider.getAll(ExternalGroupByConfigProvider).values()
);
}
get matcher() {
return createGroupByMatcher([
...this.allExternalGroupByConfig(),
...groupByMatchers,
]);
}
}
export const GroupByProvider =
createIdentifier<GroupByService>('group-by-service');
export const getGroupByService = (dataSource: DataSource) => {
return dataSource.serviceGetOrCreate(
GroupByProvider,
() => new GroupByService(dataSource)
);
};
export const ExternalGroupByConfigProvider = createIdentifier<GroupByConfig>(
'external-group-by-config'
);
@@ -2,24 +2,39 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { property } from 'lit/decorators.js';
import type { Group } from '../trait.js';
import type { GroupRenderProps } from '../types.js';
export class BaseGroup<Data extends NonNullable<unknown>, Value>
export class BaseGroup<JsonValue, Data extends Record<string, unknown>>
extends SignalWatcher(WithDisposable(ShadowlessElement))
implements GroupRenderProps<Data, Value>
implements GroupRenderProps<JsonValue, Data>
{
@property({ attribute: false })
accessor data!: Data;
accessor group!: Group<unknown, JsonValue, Data>;
@property({ attribute: false })
accessor readonly!: boolean;
@property({ attribute: false })
accessor updateData: ((data: Data) => void) | undefined = undefined;
updateData(data: Data) {
this.group.manager.updateData(data);
}
@property({ attribute: false })
accessor updateValue: ((value: Value) => void) | undefined = undefined;
updateValue(value: JsonValue) {
this.group.manager.updateValue(
this.group.rows.map(row => row.rowId),
value
);
}
@property({ attribute: false })
accessor value!: Value;
get value(): JsonValue {
return this.group.value as JsonValue;
}
get type() {
return this.group.tType;
}
get data() {
return this.group.property.data$.value;
}
}
@@ -3,7 +3,7 @@ import { css, html } from 'lit';
import { BaseGroup } from './base.js';
export class BooleanGroupView extends BaseGroup<NonNullable<unknown>, boolean> {
export class BooleanGroupView extends BaseGroup<boolean, NonNullable<unknown>> {
static override styles = css`
.data-view-group-title-boolean-view {
display: flex;
@@ -7,7 +7,7 @@ import { css, html } from 'lit';
import { BaseGroup } from './base.js';
export class NumberGroupView extends BaseGroup<NonNullable<unknown>, number> {
export class NumberGroupView extends BaseGroup<number, NonNullable<unknown>> {
static override styles = css`
.data-view-group-title-number-view {
border-radius: 8px;
@@ -12,10 +12,10 @@ import type { SelectTag } from '../../logical/index.js';
import { BaseGroup } from './base.js';
export class SelectGroupView extends BaseGroup<
string,
{
options: SelectTag[];
},
string
}
> {
static override styles = css`
data-view-group-title-select-view {
@@ -7,7 +7,7 @@ import { css, html } from 'lit';
import { BaseGroup } from './base.js';
export class StringGroupView extends BaseGroup<NonNullable<unknown>, string> {
export class StringGroupView extends BaseGroup<string, NonNullable<unknown>> {
static override styles = css`
.data-view-group-title-string-view {
border-radius: 8px;
@@ -24,7 +24,7 @@ import {
sortable,
} from '../utils/wc-dnd/sort/sort-context.js';
import { verticalListSortingStrategy } from '../utils/wc-dnd/sort/strategies/index.js';
import { groupByMatcher } from './matcher.js';
import { getGroupByService } from './matcher.js';
import type { GroupTrait } from './trait.js';
import type { GroupRenderProps } from './types.js';
@@ -142,21 +142,22 @@ export class GroupSetting extends SignalWatcher(
groups,
group => group?.key ?? 'default key',
group => {
if (!group) return;
const type = group.property.dataType$.value;
if (!type) return;
const props: GroupRenderProps = {
value: group.value,
data: group.property.data$.value,
group,
readonly: true,
};
const config = group.manager.config$.value;
return html` <div
${sortable(group.key)}
${dragHandler(group.key)}
class="dv-hover dv-round-4 group-item"
>
<div class="group-item-drag-bar"></div>
<div style="padding: 0 4px;position:relative;">
${renderUniLit(config?.view, props)}
<div
style="padding: 0 4px;position:relative;pointer-events: none"
>
${renderUniLit(group.view, props)}
<div
style="position:absolute;left: 0;top: 0;right: 0;bottom: 0;"
></div>
@@ -198,7 +199,8 @@ export const selectGroupByProperty = (
if (!dataType) {
return false;
}
return !!groupByMatcher.match(dataType);
const groupByService = getGroupByService(view.manager.dataSource);
return !!groupByService?.matcher.match(dataType);
})
.map<MenuConfig>(property => {
return menu.action({
@@ -12,89 +12,112 @@ import type { Property } from '../view-manager/property.js';
import type { Row } from '../view-manager/row.js';
import type { SingleView } from '../view-manager/single-view.js';
import { defaultGroupBy } from './default.js';
import { groupByMatcher } from './matcher.js';
export type GroupData = {
manager: GroupTrait;
property: Property;
key: string;
name: string;
type: TypeInstance;
value: unknown;
rows: Row[];
import { getGroupByService } from './matcher.js';
import type { GroupByConfig } from './types.js';
export type GroupInfo<
RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>,
> = {
config: GroupByConfig;
property: Property<RawValue, JsonValue, Data>;
tType: TypeInstance;
};
export class Group<
RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>,
> {
rows: Row[] = [];
constructor(
public readonly key: string,
public readonly value: JsonValue,
private readonly groupInfo: GroupInfo<RawValue, JsonValue, Data>,
public readonly manager: GroupTrait
) {}
get property() {
return this.groupInfo.property;
}
name$ = computed(() => {
const type = this.property.dataType$.value;
if (!type) {
return '';
}
return this.groupInfo.config.groupName(type, this.value);
});
private get config() {
return this.groupInfo.config;
}
get tType() {
return this.groupInfo.tType;
}
get view() {
return this.config.view;
}
}
export class GroupTrait {
config$ = computed(() => {
groupInfo$ = computed<GroupInfo | undefined>(() => {
const groupBy = this.groupBy$.value;
if (!groupBy) {
return;
}
const result = groupByMatcher.find(v => v.data.name === groupBy.name);
const property = this.view.propertyGetOrCreate(groupBy.columnId);
if (!property) {
return;
}
const tType = property.dataType$.value;
if (!tType) {
return;
}
const groupByService = getGroupByService(this.view.manager.dataSource);
const result = groupByService?.matcher.match(tType);
if (!result) {
return;
}
return result.data;
return {
config: result,
property,
tType: tType,
};
});
property$ = computed(() => {
const groupBy = this.groupBy$.value;
if (!groupBy) {
staticInfo$ = computed(() => {
const groupInfo = this.groupInfo$.value;
if (!groupInfo) {
return;
}
return this.view.propertyGetOrCreate(groupBy.columnId);
});
staticGroupDataMap$ = computed<
Record<string, Omit<GroupData, 'rows'>> | undefined
>(() => {
const config = this.config$.value;
const property = this.property$.value;
const tType = property?.dataType$.value;
if (!config || !tType || !property) {
return;
}
return Object.fromEntries(
config.defaultKeys(tType).map(({ key, value }) => [
key,
{
key,
property,
name: config.groupName(tType, value),
manager: this,
type: tType,
value,
},
])
const staticMap = Object.fromEntries(
groupInfo.config
.defaultKeys(groupInfo.tType)
.map(({ key, value }) => [key, new Group(key, value, groupInfo, this)])
);
return {
staticMap,
groupInfo,
};
});
groupDataMap$ = computed<Record<string, GroupData> | undefined>(() => {
const staticGroupMap = this.staticGroupDataMap$.value;
const config = this.config$.value;
const groupBy = this.groupBy$.value;
const property = this.property$.value;
const tType = property?.dataType$.value;
if (!staticGroupMap || !config || !groupBy || !tType || !property) {
groupDataMap$ = computed(() => {
const staticInfo = this.staticInfo$.value;
if (!staticInfo) {
return;
}
const groupMap: Record<string, GroupData> = Object.fromEntries(
Object.entries(staticGroupMap).map(([k, v]) => [k, { ...v, rows: [] }])
);
const { staticMap, groupInfo } = staticInfo;
const groupMap: Record<string, Group> = { ...staticMap };
this.view.rows$.value.forEach(row => {
const value = this.view.cellGetOrCreate(row.rowId, groupBy.columnId)
const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id)
.jsonValue$.value;
const keys = config.valuesGroup(value, tType);
const keys = groupInfo.config.valuesGroup(value, groupInfo.tType);
keys.forEach(({ key, value }) => {
if (!groupMap[key]) {
groupMap[key] = {
key,
property: property,
name: config.groupName(tType, value),
manager: this,
value,
rows: [],
type: tType,
};
groupMap[key] = new Group(key, value, groupInfo, this);
}
groupMap[key].rows.push(row);
});
@@ -115,30 +138,30 @@ export class GroupTrait {
});
return sortedGroup
.map(key => groupMap[key])
.filter((v): v is GroupData => v != null);
.filter((v): v is Group => v != null);
}),
this.view.isLocked$
);
updateData = (data: NonNullable<unknown>) => {
const propertyId = this.propertyId;
if (!propertyId) {
const property = this.property$.value;
if (!property) {
return;
}
this.view.propertyGetOrCreate(propertyId).dataUpdate(() => data);
this.view.propertyGetOrCreate(property.id).dataUpdate(() => data);
};
get addGroup() {
const type = this.property$.value?.type$.value;
if (!type) {
return;
}
return this.view.manager.dataSource.propertyMetaGet(type)?.config.addGroup;
return this.property$.value?.meta$.value?.config.addGroup;
}
get propertyId() {
return this.groupBy$.value?.columnId;
}
property$ = computed(() => {
const groupInfo = this.groupInfo$.value;
if (!groupInfo) {
return;
}
return groupInfo.property;
});
constructor(
private readonly groupBy$: ReadonlySignal<GroupBy | undefined>,
@@ -158,18 +181,20 @@ export class GroupTrait {
addToGroup(rowId: string, key: string) {
const groupMap = this.groupDataMap$.value;
const propertyId = this.propertyId;
if (!groupMap || !propertyId) {
const groupInfo = this.groupInfo$.value;
if (!groupMap || !groupInfo) {
return;
}
const addTo = this.config$.value?.addToGroup ?? (value => value);
const addTo = groupInfo.config.addToGroup ?? (value => value);
const v = groupMap[key]?.value;
if (v != null) {
const newValue = addTo(
v,
this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value
this.view.cellGetOrCreate(rowId, groupInfo.property.id).jsonValue$.value
);
this.view.cellGetOrCreate(rowId, propertyId).valueSet(newValue);
this.view
.cellGetOrCreate(rowId, groupInfo.property.id)
.valueSet(newValue);
}
}
@@ -229,11 +254,12 @@ export class GroupTrait {
return;
}
if (fromGroupKey !== toGroupKey) {
const propertyId = this.propertyId;
const propertyId = this.property$.value?.id;
if (!propertyId) {
return;
}
const remove = this.config$.value?.removeFromGroup ?? (() => null);
const remove =
this.groupInfo$.value?.config.removeFromGroup ?? (() => null);
const group = fromGroupKey != null ? groupMap[fromGroupKey] : undefined;
let newValue: unknown = null;
if (group) {
@@ -242,7 +268,8 @@ export class GroupTrait {
this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value
);
}
const addTo = this.config$.value?.addToGroup ?? (value => value);
const addTo =
this.groupInfo$.value?.config.addToGroup ?? (value => value);
newValue = addTo(groupMap[toGroupKey]?.value ?? null, newValue);
this.view.cellGetOrCreate(rowId, propertyId).jsonValueSet(newValue);
}
@@ -275,11 +302,12 @@ export class GroupTrait {
if (!groupMap) {
return;
}
const propertyId = this.propertyId;
const propertyId = this.property$.value?.id;
if (!propertyId) {
return;
}
const remove = this.config$.value?.removeFromGroup ?? (() => undefined);
const remove =
this.groupInfo$.value?.config.removeFromGroup ?? (() => undefined);
const newValue = remove(
groupMap[key]?.value ?? null,
this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value
@@ -288,7 +316,7 @@ export class GroupTrait {
}
updateValue(rows: string[], value: unknown) {
const propertyId = this.propertyId;
const propertyId = this.property$.value?.id;
if (!propertyId) {
return;
}
@@ -1,35 +1,41 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { TypeInstance } from '../logical/type.js';
import type { TypeInstance, ValueTypeOf } from '../logical/type.js';
import type { Group } from './trait.js';
export interface GroupRenderProps<
Data extends NonNullable<unknown> = NonNullable<unknown>,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>,
> {
data: Data;
updateData?: (data: Data) => void;
value: JsonValue;
updateValue?: (value: JsonValue) => void;
group: Group<unknown, JsonValue, Data>;
readonly: boolean;
}
export type GroupByConfig<
JsonValue = unknown,
Data extends NonNullable<unknown> = NonNullable<unknown>,
MatchType extends TypeInstance = TypeInstance,
GroupValue = unknown,
> = {
name: string;
groupName: (type: TypeInstance, value: unknown) => string;
defaultKeys: (type: TypeInstance) => {
matchType: MatchType;
groupName: (type: MatchType, value: GroupValue | null) => string;
defaultKeys: (type: MatchType) => {
key: string;
value: JsonValue;
value: GroupValue | null;
}[];
valuesGroup: (
value: unknown,
type: TypeInstance
value: ValueTypeOf<MatchType> | null,
type: MatchType
) => {
key: string;
value: JsonValue;
value: GroupValue | null;
}[];
addToGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue;
removeFromGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue;
view: UniComponent<GroupRenderProps<Data, JsonValue>>;
addToGroup?: (
value: GroupValue | null,
oldValue: ValueTypeOf<MatchType> | null
) => ValueTypeOf<MatchType> | null;
removeFromGroup?: (
value: GroupValue | null,
oldValue: ValueTypeOf<MatchType> | null
) => ValueTypeOf<MatchType> | null;
view: UniComponent<GroupRenderProps<GroupValue | null, Data>>;
};
@@ -68,3 +68,46 @@ export class Matcher<Data, Type extends TypeInstance = TypeInstance> {
return;
}
}
export class Matcher_<Value, Type extends TypeInstance> {
constructor(
private readonly list: Value[],
private readonly getType: (value: Value) => Type,
private readonly matchFunc: (
type: Type,
target: TypeInstance
) => boolean = (type, target) => typeSystem.unify(target, type)
) {}
all(): Value[] {
return this.list;
}
allMatched(type: TypeInstance): Value[] {
const result: Value[] = [];
for (const t of this.list) {
const tType = this.getType(t);
if (this.matchFunc(tType, type)) {
result.push(t);
}
}
return result;
}
find(f: (data: Value) => boolean): Value | undefined {
return this.list.find(f);
}
isMatched(type: Type, target: TypeInstance) {
return this.matchFunc(type, target);
}
match(type: TypeInstance) {
for (const t of this.list) {
const tType = this.getType(t);
if (this.matchFunc(tType, type)) {
return t;
}
}
return;
}
}
@@ -1,3 +1,7 @@
import type {
UserListService,
UserService,
} from '@blocksuite/affine-shared/services';
import * as zod from 'zod';
import Zod from 'zod';
@@ -12,6 +16,11 @@ export const SelectTagSchema = Zod.object({
color: Zod.string(),
value: Zod.string(),
});
export const UserInfoSchema = Zod.object({
userService: Zod.custom<UserService>(() => true),
userListService: Zod.custom<UserListService>(() => true),
});
export type UserInfo = Zod.TypeOf<typeof UserInfoSchema>;
export const unknown = defineDataType('Unknown', zod.never(), zod.unknown());
export const dt = {
number: defineDataType('Number', zod.number(), zod.number()),
@@ -22,6 +31,7 @@ export const dt = {
url: defineDataType('URL', zod.string(), zod.string()),
image: defineDataType('Image', zod.string(), zod.string()),
tag: defineDataType('Tag', zod.array(SelectTagSchema), zod.string()),
user: defineDataType('User', UserInfoSchema, zod.string()),
};
export const t = {
unknown,
@@ -53,4 +63,5 @@ export const converts: TypeConvertConfig[] = [
),
createTypeConvert(t.richText.instance(), t.string.instance(), value => value),
createTypeConvert(t.url.instance(), t.string.instance(), value => value),
createTypeConvert(t.user.instance(), t.string.instance(), value => value),
];
@@ -1,7 +1,7 @@
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { GeneralServiceIdentifier } from '@blocksuite/global/di';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import type { DataViewContextKey } from '../data-source/context.js';
import type { Variable } from '../expression/types.js';
import type { PropertyMetaConfig } from '../property/property-config.js';
import type { TraitKey } from '../traits/key.js';
@@ -61,7 +61,8 @@ export interface SingleView {
type?: string
): string | undefined;
contextGet<T>(key: DataViewContextKey<T>): T;
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null;
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T;
traitGet<T>(key: TraitKey<T>): T | undefined;
@@ -201,8 +202,12 @@ export abstract class SingleViewBase<
return new CellBase(this, propertyId, rowId);
}
contextGet<T>(key: DataViewContextKey<T>): T {
return this.dataSource.contextGet(key);
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null {
return this.dataSource.serviceGet(key);
}
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T {
return this.dataSource.serviceGetOrCreate(key, create);
}
dataUpdate(updater: (viewData: ViewData) => Partial<ViewData>): void {
@@ -2,7 +2,7 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { GroupBy, GroupProperty } from '../../core/common/types.js';
import type { FilterGroup } from '../../core/filter/types.js';
import { defaultGroupBy, groupByMatcher, t } from '../../core/index.js';
import { defaultGroupBy, getGroupByService, t } from '../../core/index.js';
import type { Sort } from '../../core/sort/types.js';
import { type BasicViewDataType, viewType } from '../../core/view/data-view.js';
import { KanbanSingleView } from './kanban-view-manager.js';
@@ -34,10 +34,11 @@ export const kanbanViewModel = kanbanViewType.createModel<KanbanViewData>({
defaultName: 'Kanban View',
dataViewManager: KanbanSingleView,
defaultData: viewManager => {
const groupByService = getGroupByService(viewManager.dataSource);
const columns = viewManager.dataSource.properties$.value;
const allowList = columns.filter(columnId => {
const dataType = viewManager.dataSource.propertyDataTypeGet(columnId);
return dataType && !!groupByMatcher.match(dataType);
return dataType && !!groupByService?.matcher.match(dataType);
});
const getWeight = (columnId: string) => {
const dataType = viewManager.dataSource.propertyDataTypeGet(columnId);
@@ -13,7 +13,7 @@ import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { GroupData } from '../../../core/group-by/trait.js';
import type { Group } from '../../../core/group-by/trait.js';
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
@@ -137,7 +137,7 @@ export class MobileKanbanGroup extends SignalWatcher(
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group!: GroupData;
accessor group!: Group;
@property({ attribute: false })
accessor view!: KanbanSingleView;
@@ -55,7 +55,7 @@ export const popCardMenu = (
})
.map(group => {
return menu.action({
name: group.value != null ? group.name : 'Ungroup',
name: group.value != null ? group.name$.value : 'Ungroup',
select: () => {
groupTrait.moveCardTo(
cardId,
@@ -13,7 +13,7 @@ import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { GroupData } from '../../../core/group-by/trait.js';
import type { Group } from '../../../core/group-by/trait.js';
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
@@ -201,7 +201,7 @@ export class KanbanGroup extends SignalWatcher(
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group!: GroupData;
accessor group!: Group;
@property({ attribute: false })
accessor view!: KanbanSingleView;
@@ -60,7 +60,7 @@ export const popCardMenu = (
})
.map(group => {
return menu.action({
name: group.value != null ? group.name : 'Ungroup',
name: group.value != null ? group.name$.value : 'Ungroup',
select: () => {
selection.moveCard(rowId, group.key);
},
@@ -13,7 +13,7 @@ import { repeat } from 'lit/directives/repeat.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { GroupData } from '../../../core/group-by/trait.js';
import type { Group } from '../../../core/group-by/trait.js';
import type { Row } from '../../../core/index.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { DataViewTable } from '../pc/table-view.js';
@@ -185,7 +185,7 @@ export class MobileTableGroup extends SignalWatcher(
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group: GroupData | undefined = undefined;
accessor group: Group | undefined = undefined;
@query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null;
@@ -4,7 +4,7 @@ import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { GroupData } from '../../../../../../core/group-by/trait';
import type { Group } from '../../../../../../core/group-by/trait';
import { LEFT_TOOL_BAR_WIDTH, STATS_BAR_HEIGHT } from '../../../../consts';
import type { TableSingleView } from '../../../../table-view-manager';
@@ -38,7 +38,7 @@ export class VirtualDataBaseColumnStats extends SignalWatcher(
}
@property({ attribute: false })
accessor group: GroupData | undefined = undefined;
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor view!: TableSingleView;
@@ -15,7 +15,7 @@ import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { typeSystem } from '../../../../../../core';
import type { GroupData } from '../../../../../../core/group-by/trait';
import type { Group } from '../../../../../../core/group-by/trait';
import { statsFunctions } from '../../../../../../core/statistics';
import type { StatisticsConfig } from '../../../../../../core/statistics/types';
import type { TableProperty } from '../../../../table-view-manager';
@@ -236,7 +236,7 @@ export class VirtualDatabaseColumnStatsCell extends SignalWatcher(
}
@property({ attribute: false })
accessor group: GroupData | undefined = undefined;
accessor group: Group | undefined = undefined;
}
declare global {
@@ -4,7 +4,7 @@ import { nothing } from 'lit';
import { html } from 'lit/static-html.js';
import {
type GroupData,
type Group,
type GroupRenderProps,
renderUniLit,
} from '../../../../../core';
@@ -18,7 +18,7 @@ import {
show,
} from './group-title-css';
function GroupHeaderCount(group: GroupData) {
function GroupHeaderCount(group: Group) {
const cards = group.rows;
if (!cards.length) {
return;
@@ -27,7 +27,7 @@ function GroupHeaderCount(group: GroupData) {
}
export const GroupTitle = (
groupData: GroupData,
groupData: Group,
ops: {
groupHover: boolean;
readonly: boolean;
@@ -35,24 +35,20 @@ export const GroupTitle = (
clickOps: (evt: MouseEvent) => void;
}
) => {
const data = groupData.manager.config$.value;
if (!data) return nothing;
const view = groupData.view;
const type = groupData.property.dataType$.value;
if (!view || !type) {
return nothing;
}
const icon =
groupData.value == null
? ''
: html` <uni-lit
class="${groupHeaderIcon}"
.uni="${groupData.manager.property$.value?.icon}"
.uni="${groupData.property.icon}"
></uni-lit>`;
const props: GroupRenderProps = {
value: groupData.value,
data: groupData.property.data$.value,
updateData: groupData.manager.updateData,
updateValue: value =>
groupData.manager.updateValue(
groupData.rows.map(row => row.rowId),
value
),
group: groupData,
readonly: ops.readonly,
};
@@ -65,7 +61,7 @@ export const GroupTitle = (
const opsClass = clsx(ops.groupHover && show, groupHeaderOps);
return html`
<div class="${groupTitleRow}">
${icon} ${renderUniLit(data.view, props)} ${columnName}
${icon} ${renderUniLit(view, props)} ${columnName}
${GroupHeaderCount(groupData)}
</div>
${!ops.readonly
@@ -7,7 +7,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { GroupData } from '../../../../../../core/group-by/trait';
import type { Group } from '../../../../../../core/group-by/trait';
import type { Row } from '../../../../../../core/view-manager/row';
import type {
TableProperty,
@@ -84,7 +84,7 @@ export class DataViewColumnPreview extends SignalWatcher(
accessor container!: HTMLElement;
@property({ attribute: false })
accessor group: GroupData | undefined = undefined;
accessor group: Group | undefined = undefined;
}
declare global {
@@ -40,7 +40,10 @@ import {
} from '../../../../../../core/utils/wc-dnd/dnd-context';
import type { Property } from '../../../../../../core/view-manager/property';
import { numberFormats } from '../../../../../../property-presets/number/utils/formats';
import { ShowQuickSettingBarContextKey } from '../../../../../../widget-presets/quick-setting-bar/context';
import {
createDefaultShowQuickSettingBar,
ShowQuickSettingBarKey,
} from '../../../../../../widget-presets/quick-setting-bar/context';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts';
import type {
TableProperty,
@@ -193,7 +196,10 @@ export class DatabaseHeaderColumn extends SignalWatcher(
}
private _toggleQuickSettingBar(show = true) {
const map = this.tableViewManager.contextGet(ShowQuickSettingBarContextKey);
const map = this.tableViewManager.serviceGetOrCreate(
ShowQuickSettingBarKey,
createDefaultShowQuickSettingBar
);
map.value = {
...map.value,
[this.tableViewManager.id]: show,

Some files were not shown because too many files have changed in this diff Show More