mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
Merge branch 'canary' into zzj/feat/database-block/upgrade-prompt
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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';
|
||||
|
||||
+65
-2
@@ -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();
|
||||
|
||||
+65
-1
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
+19
-1
@@ -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;
|
||||
}
|
||||
|
||||
+209
-1023
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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+11
-15
@@ -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
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+8
-2
@@ -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
Reference in New Issue
Block a user