mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-23 15:50:43 +08:00
Compare commits
28 Commits
v0.22.0-ca
...
preview-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
914e4baf82 | ||
|
|
bd268044b4 | ||
|
|
b5edd7a6bb | ||
|
|
34039bc7d8 | ||
|
|
0f87136fd7 | ||
|
|
7e4af90c03 | ||
|
|
f5f7cbb105 | ||
|
|
83670ab335 | ||
|
|
a326eac1bb | ||
|
|
8b402dd49a | ||
|
|
e96fcf0c35 | ||
|
|
4c84e6bac7 | ||
|
|
be28038e94 | ||
|
|
d82d37b53d | ||
|
|
f177c64ca1 | ||
|
|
362f89b669 | ||
|
|
df565f2fbf | ||
|
|
468db9f3eb | ||
|
|
4e201ede17 | ||
|
|
d7be1b3424 | ||
|
|
d57b9372ae | ||
|
|
c555cca6a1 | ||
|
|
4662ee8da7 | ||
|
|
21dc550b9d | ||
|
|
9d21d13a5e | ||
|
|
a61c5fd458 | ||
|
|
cf5574caf6 | ||
|
|
e366f69707 |
6
.github/actions/server-test-env/action.yml
vendored
6
.github/actions/server-test-env/action.yml
vendored
@@ -24,8 +24,10 @@ runs:
|
||||
- name: Import config
|
||||
shell: bash
|
||||
run: |
|
||||
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"}}}' \
|
||||
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"},"providers.anthropic":{"apiKey":"%s"},"exa":{"key":"%s"}}}' \
|
||||
"$COPILOT_FAL_API_KEY" \
|
||||
"$COPILOT_GOOGLE_API_KEY" \
|
||||
"$COPILOT_OPENAI_API_KEY" \
|
||||
"$COPILOT_PERPLEXITY_API_KEY" > ./packages/backend/server/config.json
|
||||
"$COPILOT_PERPLEXITY_API_KEY" \
|
||||
"$COPILOT_ANTHROPIC_API_KEY" \
|
||||
"$COPILOT_EXA_API_KEY" > ./packages/backend/server/config.json
|
||||
|
||||
4
.github/workflows/build-test.yml
vendored
4
.github/workflows/build-test.yml
vendored
@@ -894,6 +894,8 @@ jobs:
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests
|
||||
@@ -991,6 +993,8 @@ jobs:
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
4
.github/workflows/copilot-test.yml
vendored
4
.github/workflows/copilot-test.yml
vendored
@@ -81,6 +81,8 @@ jobs:
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests
|
||||
@@ -150,6 +152,8 @@ jobs:
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
|
||||
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
|
||||
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
|
||||
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@blocksuite/affine-gfx-brush": "workspace:*",
|
||||
"@blocksuite/affine-gfx-connector": "workspace:*",
|
||||
"@blocksuite/affine-gfx-group": "workspace:*",
|
||||
"@blocksuite/affine-gfx-link": "workspace:*",
|
||||
"@blocksuite/affine-gfx-mindmap": "workspace:*",
|
||||
"@blocksuite/affine-gfx-note": "workspace:*",
|
||||
"@blocksuite/affine-gfx-pointer": "workspace:*",
|
||||
@@ -57,13 +58,16 @@
|
||||
"@blocksuite/affine-widget-drag-handle": "workspace:*",
|
||||
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
|
||||
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
|
||||
"@blocksuite/affine-widget-edgeless-zoom-toolbar": "workspace:*",
|
||||
"@blocksuite/affine-widget-frame-title": "workspace:*",
|
||||
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
|
||||
"@blocksuite/affine-widget-linked-doc": "workspace:*",
|
||||
"@blocksuite/affine-widget-page-dragging-area": "workspace:*",
|
||||
"@blocksuite/affine-widget-remote-selection": "workspace:*",
|
||||
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
|
||||
"@blocksuite/affine-widget-slash-menu": "workspace:*",
|
||||
"@blocksuite/affine-widget-toolbar": "workspace:*",
|
||||
"@blocksuite/affine-widget-viewport-overlay": "workspace:*",
|
||||
"@blocksuite/data-view": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
@@ -184,6 +188,10 @@
|
||||
"./widgets/toolbar/view": "./src/widgets/toolbar/view.ts",
|
||||
"./widgets/keyboard-toolbar": "./src/widgets/keyboard-toolbar/index.ts",
|
||||
"./widgets/keyboard-toolbar/view": "./src/widgets/keyboard-toolbar/view.ts",
|
||||
"./widgets/viewport-overlay": "./src/widgets/viewport-overlay/index.ts",
|
||||
"./widgets/viewport-overlay/view": "./src/widgets/viewport-overlay/view.ts",
|
||||
"./widgets/page-dragging-area": "./src/widgets/page-dragging-area/index.ts",
|
||||
"./widgets/page-dragging-area/view": "./src/widgets/page-dragging-area/view.ts",
|
||||
"./fragments/doc-title": "./src/fragments/doc-title.ts",
|
||||
"./fragments/frame-panel": "./src/fragments/frame-panel.ts",
|
||||
"./fragments/outline": "./src/fragments/outline.ts",
|
||||
@@ -198,6 +206,8 @@
|
||||
"./gfx/shape": "./src/gfx/shape/index.ts",
|
||||
"./gfx/shape/store": "./src/gfx/shape/store.ts",
|
||||
"./gfx/shape/view": "./src/gfx/shape/view.ts",
|
||||
"./gfx/link": "./src/gfx/link/index.ts",
|
||||
"./gfx/link/view": "./src/gfx/link/view.ts",
|
||||
"./gfx/note": "./src/gfx/note/index.ts",
|
||||
"./gfx/note/view": "./src/gfx/note/view.ts",
|
||||
"./gfx/mindmap": "./src/gfx/mindmap/index.ts",
|
||||
@@ -216,6 +226,7 @@
|
||||
"./components/block-zero-width": "./src/components/block-zero-width.ts",
|
||||
"./components/caption": "./src/components/caption.ts",
|
||||
"./components/card-style-dropdown-menu": "./src/components/card-style-dropdown-menu.ts",
|
||||
"./components/citation": "./src/components/citation.ts",
|
||||
"./components/color-picker": "./src/components/color-picker.ts",
|
||||
"./components/context-menu": "./src/components/context-menu.ts",
|
||||
"./components/date-picker": "./src/components/date-picker.ts",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AttachmentBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-attachment';
|
||||
import { BookmarkBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-bookmark';
|
||||
import { CodeBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-code';
|
||||
import { DatabaseBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-database';
|
||||
@@ -38,4 +39,5 @@ export const defaultBlockMarkdownAdapterMatchers = [
|
||||
DividerBlockMarkdownAdapterExtension,
|
||||
ImageBlockMarkdownAdapterExtension,
|
||||
LatexBlockMarkdownAdapterExtension,
|
||||
AttachmentBlockMarkdownAdapterExtension,
|
||||
];
|
||||
|
||||
@@ -21,6 +21,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection';
|
||||
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
|
||||
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
|
||||
import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu';
|
||||
import { effects as componentCitationEffects } from '@blocksuite/affine-components/citation';
|
||||
import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
|
||||
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
||||
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
||||
@@ -154,6 +155,7 @@ export function effects() {
|
||||
componentEmbedCardModalEffects();
|
||||
componentLinkPreviewEffects();
|
||||
componentLinkedDocTitleEffects();
|
||||
componentCitationEffects();
|
||||
componentCardStyleDropdownMenuEffects();
|
||||
componentHighlightDropdownMenuEffects();
|
||||
componentViewDropdownMenuEffects();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FoundationViewExtension } from '@blocksuite/affine-foundation/view';
|
||||
import { BrushViewExtension } from '@blocksuite/affine-gfx-brush/view';
|
||||
import { ConnectorViewExtension } from '@blocksuite/affine-gfx-connector/view';
|
||||
import { GroupViewExtension } from '@blocksuite/affine-gfx-group/view';
|
||||
import { LinkViewExtension as GfxLinkViewExtension } from '@blocksuite/affine-gfx-link/view';
|
||||
import { MindmapViewExtension } from '@blocksuite/affine-gfx-mindmap/view';
|
||||
import { NoteViewExtension as GfxNoteViewExtension } from '@blocksuite/affine-gfx-note/view';
|
||||
import { PointerViewExtension } from '@blocksuite/affine-gfx-pointer/view';
|
||||
@@ -35,13 +36,16 @@ import { ReferenceViewExtension } from '@blocksuite/affine-inline-reference/view
|
||||
import { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/view';
|
||||
import { EdgelessAutoConnectViewExtension } from '@blocksuite/affine-widget-edgeless-auto-connect/view';
|
||||
import { EdgelessToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-toolbar/view';
|
||||
import { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
|
||||
import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view';
|
||||
import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/view';
|
||||
import { LinkedDocViewExtension } from '@blocksuite/affine-widget-linked-doc/view';
|
||||
import { PageDraggingAreaViewExtension } from '@blocksuite/affine-widget-page-dragging-area/view';
|
||||
import { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view';
|
||||
import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view';
|
||||
import { SlashMenuViewExtension } from '@blocksuite/affine-widget-slash-menu/view';
|
||||
import { ToolbarViewExtension } from '@blocksuite/affine-widget-toolbar/view';
|
||||
import { ViewportOverlayViewExtension } from '@blocksuite/affine-widget-viewport-overlay/view';
|
||||
|
||||
import { MigratingViewExtension } from './migrating-view';
|
||||
|
||||
@@ -59,6 +63,7 @@ export function getInternalViewExtensions() {
|
||||
GroupViewExtension,
|
||||
TextViewExtension,
|
||||
TemplateViewExtension,
|
||||
GfxLinkViewExtension,
|
||||
|
||||
// Block
|
||||
AttachmentViewExtension,
|
||||
@@ -100,5 +105,8 @@ export function getInternalViewExtensions() {
|
||||
ScrollAnchoringViewExtension,
|
||||
SlashMenuViewExtension,
|
||||
ToolbarViewExtension,
|
||||
ViewportOverlayViewExtension,
|
||||
EdgelessZoomToolbarViewExtension,
|
||||
PageDraggingAreaViewExtension,
|
||||
];
|
||||
}
|
||||
|
||||
1
blocksuite/affine/all/src/gfx/link/index.ts
Normal file
1
blocksuite/affine/all/src/gfx/link/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-gfx-link';
|
||||
1
blocksuite/affine/all/src/gfx/link/view.ts
Normal file
1
blocksuite/affine/all/src/gfx/link/view.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-gfx-link/view';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-widget-edgeless-zoom-toolbar';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-widget-page-dragging-area';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-widget-page-dragging-area/view';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-widget-viewport-overlay';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-widget-viewport-overlay/view';
|
||||
@@ -35,6 +35,7 @@
|
||||
{ "path": "../gfx/brush" },
|
||||
{ "path": "../gfx/connector" },
|
||||
{ "path": "../gfx/group" },
|
||||
{ "path": "../gfx/link" },
|
||||
{ "path": "../gfx/mindmap" },
|
||||
{ "path": "../gfx/note" },
|
||||
{ "path": "../gfx/pointer" },
|
||||
@@ -54,13 +55,16 @@
|
||||
{ "path": "../widgets/drag-handle" },
|
||||
{ "path": "../widgets/edgeless-auto-connect" },
|
||||
{ "path": "../widgets/edgeless-toolbar" },
|
||||
{ "path": "../widgets/edgeless-zoom-toolbar" },
|
||||
{ "path": "../widgets/frame-title" },
|
||||
{ "path": "../widgets/keyboard-toolbar" },
|
||||
{ "path": "../widgets/linked-doc" },
|
||||
{ "path": "../widgets/page-dragging-area" },
|
||||
{ "path": "../widgets/remote-selection" },
|
||||
{ "path": "../widgets/scroll-anchoring" },
|
||||
{ "path": "../widgets/slash-menu" },
|
||||
{ "path": "../widgets/toolbar" },
|
||||
{ "path": "../widgets/viewport-overlay" },
|
||||
{ "path": "../data-view" },
|
||||
{ "path": "../../framework/global" },
|
||||
{ "path": "../../framework/std" },
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { AttachmentBlockMarkdownAdapterExtension } from './markdown.js';
|
||||
import { AttachmentBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
||||
|
||||
export const AttachmentBlockAdapterExtensions: ExtensionType[] = [
|
||||
AttachmentBlockNotionHtmlAdapterExtension,
|
||||
AttachmentBlockMarkdownAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './markdown.js';
|
||||
export * from './notion-html.js';
|
||||
93
blocksuite/affine/blocks/attachment/src/adapters/markdown.ts
Normal file
93
blocksuite/affine/blocks/attachment/src/adapters/markdown.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
AttachmentBlockSchema,
|
||||
FootNoteReferenceParamsSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
getFootnoteDefinitionText,
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
if (!isFootnoteDefinitionNode(node)) return false;
|
||||
const footnoteDefinition = getFootnoteDefinitionText(node);
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
return (
|
||||
footnoteDefinitionJson.type === 'attachment' &&
|
||||
!!footnoteDefinitionJson.blobId
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||
{
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
toMatch: o => isAttachmentFootnoteDefinitionNode(o.node),
|
||||
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { provider } = context;
|
||||
let enableCitation = false;
|
||||
try {
|
||||
const featureFlagService = provider?.get(FeatureFlagService);
|
||||
enableCitation = !!featureFlagService?.getFlag('enable_citation');
|
||||
} catch {
|
||||
enableCitation = false;
|
||||
}
|
||||
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { walkerContext, configs } = context;
|
||||
const footnoteIdentifier = o.node.identifier;
|
||||
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
|
||||
const footnoteDefinition = configs.get(footnoteDefinitionKey);
|
||||
if (!footnoteDefinition) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
const { blobId, fileName } = footnoteDefinitionJson;
|
||||
if (!blobId || !fileName) {
|
||||
return;
|
||||
}
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: AttachmentBlockSchema.model.flavour,
|
||||
props: {
|
||||
name: fileName,
|
||||
sourceId: blobId,
|
||||
footnoteIdentifier,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse attachment footnote definition:', err);
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {},
|
||||
};
|
||||
|
||||
export const AttachmentBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(attachmentBlockMarkdownAdapterMatcher);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
AttachmentIcon16,
|
||||
getAttachmentFileIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
CaptionedBlockComponent,
|
||||
SelectedStyle,
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import { getAttachmentFileIcon } from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
@@ -11,21 +11,31 @@ import {
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
FileSizeLimitService,
|
||||
FileSizeLimitProvider,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
AttachmentIcon,
|
||||
ResetIcon,
|
||||
UpgradeIcon,
|
||||
WarningIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { type BlobState } from '@blocksuite/sync';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
import { checkAttachmentBlob, downloadAttachmentBlob } from './utils';
|
||||
|
||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||
type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none';
|
||||
@Peekable({
|
||||
enableOn: ({ model }: AttachmentBlockComponent) => {
|
||||
return !model.doc.readonly && model.props.type.endsWith('pdf');
|
||||
@@ -36,6 +46,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
blockDraggable = true;
|
||||
|
||||
blobState$ = signal<Partial<BlobState>>({});
|
||||
|
||||
protected containerStyleMap = styleMap({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
@@ -43,7 +55,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
});
|
||||
|
||||
private get _maxFileSize() {
|
||||
return this.std.store.get(FileSizeLimitService).maxFileSize;
|
||||
return this.std.get(FileSizeLimitProvider).maxFileSize;
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return !!this.model.props.footnoteIdentifier;
|
||||
}
|
||||
|
||||
convertTo = () => {
|
||||
@@ -63,26 +79,45 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
};
|
||||
|
||||
embedded = () => {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.embedded(this.model, this._maxFileSize);
|
||||
return (
|
||||
Boolean(this.blobUrl) &&
|
||||
this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.embedded(this.model, this._maxFileSize)
|
||||
);
|
||||
};
|
||||
|
||||
open = () => {
|
||||
if (!this.blobUrl) {
|
||||
return;
|
||||
}
|
||||
window.open(this.blobUrl, '_blank');
|
||||
const blobUrl = this.blobUrl;
|
||||
if (!blobUrl) return;
|
||||
window.open(blobUrl, '_blank');
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
checkAttachmentBlob(this).catch(console.error);
|
||||
refreshData(this.std, this).catch(console.error);
|
||||
};
|
||||
|
||||
updateBlobState(state: Partial<BlobState>) {
|
||||
this.blobState$.value = { ...this.blobState$.value, ...state };
|
||||
}
|
||||
|
||||
determineState = (
|
||||
loading: boolean,
|
||||
uploading: boolean,
|
||||
overSize: boolean,
|
||||
error: boolean
|
||||
): State => {
|
||||
if (overSize) return 'oversize';
|
||||
if (error) return 'warning';
|
||||
if (uploading) return 'uploading';
|
||||
if (loading) return 'loading';
|
||||
return 'none';
|
||||
};
|
||||
|
||||
protected get embedView() {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
.render(this.model, this.blobUrl, this._maxFileSize);
|
||||
.render(this.model, this.blobUrl ?? undefined, this._maxFileSize);
|
||||
}
|
||||
|
||||
private _selectBlock() {
|
||||
@@ -96,38 +131,44 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.refreshData();
|
||||
|
||||
this.contentEditable = 'false';
|
||||
|
||||
if (!this.model.props.style) {
|
||||
this.refreshData();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const blobId = this.model.props.sourceId$.value;
|
||||
if (!blobId) return;
|
||||
|
||||
const blobState$ = this.std.store.blobSync.blobState$(blobId);
|
||||
if (!blobState$) return;
|
||||
|
||||
const subscription = blobState$.subscribe(state => {
|
||||
if (state.overSize || state.errorMessage) {
|
||||
state.uploading = false;
|
||||
state.downloading = false;
|
||||
}
|
||||
|
||||
this.updateBlobState(state);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
})
|
||||
);
|
||||
|
||||
if (!this.model.props.style && !this.doc.readonly) {
|
||||
this.doc.withoutTransact(() => {
|
||||
this.doc.updateBlock(this.model, {
|
||||
style: AttachmentBlockStyles[1],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.model.propsUpdated.subscribe(({ key }) => {
|
||||
if (key === 'sourceId') {
|
||||
// Reset the blob url when the sourceId is changed
|
||||
if (this.blobUrl) {
|
||||
URL.revokeObjectURL(this.blobUrl);
|
||||
this.blobUrl = undefined;
|
||||
}
|
||||
this.refreshData();
|
||||
}
|
||||
});
|
||||
|
||||
// Workaround for https://github.com/toeverything/blocksuite/issues/4724
|
||||
this.disposables.add(
|
||||
this.std.get(ThemeProvider).theme$.subscribe(() => this.requestUpdate())
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
if (this.blobUrl) {
|
||||
URL.revokeObjectURL(this.blobUrl);
|
||||
const blobUrl = this.blobUrl;
|
||||
if (blobUrl) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@@ -148,71 +189,207 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
}
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
protected renderUpgradeButton = () => {
|
||||
if (this.std.store.readonly) return null;
|
||||
|
||||
const onOverFileSize = this.std.get(FileSizeLimitProvider).onOverFileSize;
|
||||
|
||||
return when(
|
||||
onOverFileSize,
|
||||
() => html`
|
||||
<button
|
||||
class="affine-attachment-content-button"
|
||||
@click=${(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onOverFileSize?.();
|
||||
}}
|
||||
>
|
||||
${UpgradeIcon()} Upgrade
|
||||
</button>
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
protected renderReloadButton = () => {
|
||||
return html`
|
||||
<button
|
||||
class="affine-attachment-content-button"
|
||||
@click=${(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
this.refreshData();
|
||||
}}
|
||||
>
|
||||
${ResetIcon()} Reload
|
||||
</button>
|
||||
`;
|
||||
};
|
||||
|
||||
protected renderWithHorizontal(
|
||||
classInfo: ClassInfo,
|
||||
icon: TemplateResult,
|
||||
title: string,
|
||||
description: string,
|
||||
kind: TemplateResult,
|
||||
state: State
|
||||
) {
|
||||
return html`<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-description">
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
</div>
|
||||
${choose(state, [
|
||||
['oversize', this.renderUpgradeButton],
|
||||
['warning', this.renderReloadButton],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">${kind}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderWithVertical(
|
||||
classInfo: ClassInfo,
|
||||
icon: TemplateResult,
|
||||
title: string,
|
||||
description: string,
|
||||
kind: TemplateResult,
|
||||
state?: State
|
||||
) {
|
||||
return html`<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">
|
||||
${kind}
|
||||
${choose(state, [
|
||||
['oversize', this.renderUpgradeButton],
|
||||
['warning', this.renderReloadButton],
|
||||
])}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderCard = () => {
|
||||
const { name, size, style } = this.model.props;
|
||||
const cardStyle = style ?? AttachmentBlockStyles[1];
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
const { LoadingIcon } = getEmbedCardIcons(theme);
|
||||
|
||||
const titleIcon = this.loading ? LoadingIcon : AttachmentIcon16;
|
||||
const titleText = this.loading ? 'Loading...' : name;
|
||||
const infoText = this.error ? 'File loading failed.' : humanFileSize(size);
|
||||
const blobState = this.blobState$.value;
|
||||
const {
|
||||
uploading = false,
|
||||
downloading = false,
|
||||
overSize = false,
|
||||
errorMessage,
|
||||
} = blobState;
|
||||
const warning = !overSize && Boolean(errorMessage);
|
||||
const error = overSize || warning;
|
||||
const loading = !error && downloading;
|
||||
const state = this.determineState(loading, uploading, overSize, error);
|
||||
|
||||
const classInfo = {
|
||||
'affine-attachment-card': true,
|
||||
[cardStyle]: true,
|
||||
error,
|
||||
loading,
|
||||
};
|
||||
|
||||
const icon = loading
|
||||
? LoadingIcon
|
||||
: error
|
||||
? WarningIcon()
|
||||
: AttachmentIcon();
|
||||
const title = uploading ? 'Uploading...' : loading ? 'Loading...' : name;
|
||||
const description = errorMessage || humanFileSize(size);
|
||||
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
|
||||
|
||||
return when(
|
||||
cardStyle === 'cubeThick',
|
||||
() =>
|
||||
this.renderWithVertical(
|
||||
classInfo,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state
|
||||
),
|
||||
() =>
|
||||
this.renderWithHorizontal(
|
||||
classInfo,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _renderCitation = () => {
|
||||
const { name, footnoteIdentifier } = this.model.props;
|
||||
const fileType = name.split('.').pop() ?? '';
|
||||
const FileTypeIcon = getAttachmentFileIcon(fileType);
|
||||
|
||||
const embedView = this.embedView;
|
||||
const fileTypeIcon = getAttachmentFileIcon(fileType);
|
||||
return html`<affine-citation-card
|
||||
.icon=${fileTypeIcon}
|
||||
.citationTitle=${name}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
.active=${this.selected$.value}
|
||||
></affine-citation-card>`;
|
||||
};
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="affine-attachment-container" style=${this.containerStyleMap}>
|
||||
${embedView
|
||||
? html`<div class="affine-attachment-embed-container">
|
||||
${embedView}
|
||||
</div>`
|
||||
: html`<div
|
||||
class=${classMap({
|
||||
'affine-attachment-card': true,
|
||||
[cardStyle]: true,
|
||||
loading: this.loading,
|
||||
error: this.error,
|
||||
unsynced: false,
|
||||
})}
|
||||
>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">
|
||||
${titleIcon}
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-title-text">
|
||||
${titleText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-content-info">${infoText}</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">${FileTypeIcon}</div>
|
||||
</div>`}
|
||||
<div
|
||||
class=${classMap({
|
||||
'affine-attachment-container': true,
|
||||
focused: this.selected$.value,
|
||||
})}
|
||||
style=${this.containerStyleMap}
|
||||
>
|
||||
${when(
|
||||
this.isCitation,
|
||||
() => this._renderCitation(),
|
||||
() =>
|
||||
when(
|
||||
this.embedView,
|
||||
() =>
|
||||
html`<div class="affine-attachment-embed-container">
|
||||
${this.embedView}
|
||||
</div>`,
|
||||
this.renderCard
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor allowEmbed = false;
|
||||
accessor blobUrl: string | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor blobUrl: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor downloading = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor error = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor loading = false;
|
||||
override accessor selectedStyle = SelectedStyle.Border;
|
||||
|
||||
override accessor useCaptionEditor = true;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js';
|
||||
import { AttachmentBlockAdapterExtensions } from './adapters/extension.js';
|
||||
import { AttachmentDropOption } from './attachment-service.js';
|
||||
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
@@ -25,7 +25,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
|
||||
AttachmentDropOption,
|
||||
AttachmentEmbedConfigExtension(),
|
||||
AttachmentEmbedService,
|
||||
AttachmentBlockNotionHtmlAdapterExtension,
|
||||
AttachmentBlockAdapterExtensions,
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
|
||||
].flat();
|
||||
|
||||
@@ -69,6 +69,10 @@ export const attachmentViewDropdownMenu = {
|
||||
{
|
||||
id: 'embed',
|
||||
label: 'Embed view',
|
||||
disabled: ctx => {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
return block ? !block.embedded() : true;
|
||||
},
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return;
|
||||
@@ -135,6 +139,12 @@ const downloadAction = {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.download();
|
||||
},
|
||||
when: ctx => {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return false;
|
||||
// Current citation attachment block does not support download
|
||||
return model.props.style !== 'citation' && !model.props.footnoteIdentifier;
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const captionAction = {
|
||||
@@ -156,24 +166,24 @@ const builtinToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.rename',
|
||||
content(cx) {
|
||||
const block = cx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
content(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
if (!block) return null;
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortController.signal.onabort = () => cx.show();
|
||||
abortController.signal.onabort = () => ctx.show();
|
||||
|
||||
return html`
|
||||
<editor-icon-button
|
||||
aria-label="Rename"
|
||||
.tooltip="${'Rename'}"
|
||||
@click=${() => {
|
||||
cx.hide();
|
||||
ctx.hide();
|
||||
|
||||
createLitPortal({
|
||||
template: RenameModal({
|
||||
model: block.model,
|
||||
editorHost: cx.host,
|
||||
editorHost: ctx.host,
|
||||
abortController,
|
||||
}),
|
||||
computePosition: {
|
||||
@@ -327,7 +337,6 @@ const builtinSurfaceToolbarConfig = {
|
||||
id: 'e.caption',
|
||||
},
|
||||
],
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type ImageBlockProps,
|
||||
MAX_IMAGE_WIDTH,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
|
||||
import { FileSizeLimitProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
readImageSize,
|
||||
transformModel,
|
||||
@@ -68,7 +68,7 @@ export const AttachmentEmbedProvider = createIdentifier<AttachmentEmbedService>(
|
||||
|
||||
export class AttachmentEmbedService extends Extension {
|
||||
private get _maxFileSize() {
|
||||
return this.std.store.get(FileSizeLimitService).maxFileSize;
|
||||
return this.std.get(FileSizeLimitProvider).maxFileSize;
|
||||
}
|
||||
|
||||
get keys() {
|
||||
@@ -187,7 +187,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
|
||||
/**
|
||||
* Turn the attachment block into an image block.
|
||||
*/
|
||||
export async function turnIntoImageBlock(model: AttachmentBlockModel) {
|
||||
async function turnIntoImageBlock(model: AttachmentBlockModel) {
|
||||
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
|
||||
console.error('The image flavour is not supported!');
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './adapters/notion-html';
|
||||
export * from './adapters';
|
||||
export * from './attachment-block';
|
||||
export * from './attachment-service';
|
||||
export * from './attachment-spec';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { AttachmentBlockSchemaExtension } from '@blocksuite/affine-model';
|
||||
|
||||
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html';
|
||||
import { AttachmentBlockAdapterExtensions } from './adapters/extension';
|
||||
|
||||
export class AttachmentStoreExtension extends StoreExtensionProvider {
|
||||
override name = 'affine-attachment-block';
|
||||
@@ -12,6 +12,6 @@ export class AttachmentStoreExtension extends StoreExtensionProvider {
|
||||
override setup(context: StoreExtensionContext) {
|
||||
super.setup(context);
|
||||
context.register(AttachmentBlockSchemaExtension);
|
||||
context.register(AttachmentBlockNotionHtmlAdapterExtension);
|
||||
context.register(AttachmentBlockAdapterExtensions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { css } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
.affine-attachment-card {
|
||||
margin: 0 auto;
|
||||
.affine-attachment-container {
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')};
|
||||
background: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
overflow: hidden;
|
||||
|
||||
&.focused {
|
||||
border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')};
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--affine-background-tertiary-color);
|
||||
|
||||
opacity: var(--add, 1);
|
||||
background: var(--affine-background-primary-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.affine-attachment-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title {
|
||||
@@ -33,7 +35,6 @@ export const styles = css`
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
@@ -43,24 +44,18 @@ export const styles = css`
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--affine-background-primary-color);
|
||||
.truncate {
|
||||
align-self: stretch;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title-text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-text-primary-color);
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-sm);
|
||||
font-style: normal;
|
||||
@@ -68,17 +63,15 @@ export const styles = css`
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-info {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
flex: 1 0 0;
|
||||
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
color: var(--affine-text-secondary-color);
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
@@ -86,6 +79,26 @@ export const styles = css`
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.affine-attachment-content-button {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
padding: 0 4px;
|
||||
color: ${unsafeCSSVarV2('button/primary')};
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -93,16 +106,15 @@ export const styles = css`
|
||||
}
|
||||
|
||||
.affine-attachment-card.loading {
|
||||
background: var(--affine-background-secondary-color);
|
||||
|
||||
.affine-attachment-content-title-text {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-card.error,
|
||||
.affine-attachment-card.unsynced {
|
||||
background: var(--affine-background-secondary-color);
|
||||
.affine-attachment-card.error {
|
||||
.affine-attachment-content-title-icon {
|
||||
color: ${unsafeCSSVarV2('status/error')};
|
||||
}
|
||||
}
|
||||
|
||||
.affine-attachment-card.cubeThick {
|
||||
@@ -116,7 +128,7 @@ export const styles = css`
|
||||
}
|
||||
|
||||
.affine-attachment-banner {
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
type AttachmentUploadedEvent,
|
||||
FileSizeLimitService,
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
@@ -21,128 +21,19 @@ import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { AttachmentBlockComponent } from './attachment-block';
|
||||
|
||||
const attachmentUploads = new Set<string>();
|
||||
export function setAttachmentUploading(blockId: string) {
|
||||
attachmentUploads.add(blockId);
|
||||
}
|
||||
export function setAttachmentUploaded(blockId: string) {
|
||||
attachmentUploads.delete(blockId);
|
||||
}
|
||||
function isAttachmentUploading(blockId: string) {
|
||||
return attachmentUploads.has(blockId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will not verify the size of the file.
|
||||
*/
|
||||
// TODO(@fundon): should remove
|
||||
export async function uploadAttachmentBlob(
|
||||
std: BlockStdScope,
|
||||
blockId: string,
|
||||
blob: Blob,
|
||||
filetype: string,
|
||||
isEdgeless?: boolean
|
||||
): Promise<void> {
|
||||
if (isAttachmentUploading(blockId)) return;
|
||||
|
||||
let sourceId: string | undefined;
|
||||
|
||||
try {
|
||||
setAttachmentUploading(blockId);
|
||||
sourceId = await std.store.blobSync.set(blob);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
toast(
|
||||
std.host,
|
||||
`Failed to upload attachment! ${error.message || error.toString()}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setAttachmentUploaded(blockId);
|
||||
|
||||
const block = std.store.getBlock(blockId);
|
||||
|
||||
std.store.withoutTransact(() => {
|
||||
if (!block) return;
|
||||
|
||||
std.store.updateBlock(block.model, {
|
||||
sourceId,
|
||||
} satisfies Partial<AttachmentBlockProps>);
|
||||
});
|
||||
|
||||
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
|
||||
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
|
||||
module: 'attachment',
|
||||
segment: 'attachment',
|
||||
control: 'uploader',
|
||||
type: filetype,
|
||||
category: block && sourceId ? 'success' : 'failure',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAttachmentBlob(model: AttachmentBlockModel) {
|
||||
const sourceId = model.props.sourceId;
|
||||
if (!sourceId) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
sourceId$: { value: sourceId },
|
||||
type$: { value: type },
|
||||
} = model.props;
|
||||
if (!sourceId) return null;
|
||||
|
||||
const doc = model.doc;
|
||||
let blob = await doc.blobSync.get(sourceId);
|
||||
|
||||
if (blob) {
|
||||
blob = new Blob([blob], { type: model.props.type });
|
||||
}
|
||||
if (!blob) return null;
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
// TODO(@fundon): should remove
|
||||
export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
const model = block.model;
|
||||
const { id } = model;
|
||||
const { sourceId } = model.props;
|
||||
|
||||
if (isAttachmentUploading(id)) {
|
||||
block.loading = true;
|
||||
block.error = false;
|
||||
block.allowEmbed = false;
|
||||
if (block.blobUrl) {
|
||||
URL.revokeObjectURL(block.blobUrl);
|
||||
block.blobUrl = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!sourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await getAttachmentBlob(model);
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
block.loading = false;
|
||||
block.error = false;
|
||||
block.allowEmbed = block.embedded();
|
||||
if (block.blobUrl) {
|
||||
URL.revokeObjectURL(block.blobUrl);
|
||||
}
|
||||
block.blobUrl = URL.createObjectURL(blob);
|
||||
} catch (error) {
|
||||
console.warn(error, model, sourceId);
|
||||
|
||||
block.loading = false;
|
||||
block.error = true;
|
||||
block.allowEmbed = false;
|
||||
if (block.blobUrl) {
|
||||
URL.revokeObjectURL(block.blobUrl);
|
||||
block.blobUrl = undefined;
|
||||
}
|
||||
}
|
||||
return new Blob([blob], { type });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,26 +41,22 @@ export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
* the download process may take a long time!
|
||||
*/
|
||||
export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
const { host, model, loading, error, downloading, blobUrl } = block;
|
||||
if (downloading) {
|
||||
toast(host, 'Download in progress...');
|
||||
return;
|
||||
}
|
||||
const { host, model, blobUrl, blobState$ } = block;
|
||||
|
||||
if (loading) {
|
||||
toast(host, 'Please wait, file is loading...');
|
||||
if (blobState$.peek().downloading) {
|
||||
toast(host, 'Download in progress...');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = model.props.name;
|
||||
const shortName = name.length < 20 ? name : name.slice(0, 20) + '...';
|
||||
|
||||
if (error || !blobUrl) {
|
||||
if (!blobUrl) {
|
||||
toast(host, `Failed to download ${shortName}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
block.downloading = true;
|
||||
block.updateBlobState({ downloading: true });
|
||||
|
||||
toast(host, `Downloading ${shortName}`);
|
||||
|
||||
@@ -180,7 +67,34 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
tmpLink.dispatchEvent(event);
|
||||
tmpLink.remove();
|
||||
|
||||
block.downloading = false;
|
||||
block.updateBlobState({ downloading: false });
|
||||
}
|
||||
|
||||
export async function refreshData(
|
||||
std: BlockStdScope,
|
||||
block: AttachmentBlockComponent
|
||||
) {
|
||||
const model = block.model;
|
||||
const sourceId = model.props.sourceId$.peek();
|
||||
if (!sourceId) return;
|
||||
|
||||
const blobUrl = block.blobUrl;
|
||||
if (blobUrl) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
block.blobUrl = null;
|
||||
}
|
||||
|
||||
let blob = await std.store.blobSync.get(sourceId);
|
||||
if (!blob) {
|
||||
block.updateBlobState({ errorMessage: 'File not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const type = model.props.type$.peek();
|
||||
|
||||
blob = new Blob([blob], { type });
|
||||
|
||||
block.blobUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export async function getFileType(file: File) {
|
||||
@@ -196,7 +110,7 @@ export async function getFileType(file: File) {
|
||||
function hasExceeded(
|
||||
std: BlockStdScope,
|
||||
files: File[],
|
||||
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
|
||||
maxFileSize = std.get(FileSizeLimitProvider).maxFileSize
|
||||
) {
|
||||
const exceeded = files.some(file => file.size > maxFileSize);
|
||||
|
||||
@@ -219,6 +133,7 @@ async function buildPropsWith(
|
||||
|
||||
try {
|
||||
const { name, size } = file;
|
||||
// TODO(@fundon): should re-upload when upload timeout
|
||||
const sourceId = await std.store.blobSync.set(file);
|
||||
type = await getFileType(file);
|
||||
|
||||
@@ -233,6 +148,7 @@ async function buildPropsWith(
|
||||
category = 'failure';
|
||||
throw err;
|
||||
} finally {
|
||||
// TODO(@fundon): should change event name because this is just a local operation.
|
||||
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
|
||||
page: `${mode} editor`,
|
||||
module: 'attachment',
|
||||
@@ -303,7 +219,6 @@ export async function addAttachments(
|
||||
const gap = 32;
|
||||
const width = EMBED_CARD_WIDTH.cubeThick;
|
||||
const height = EMBED_CARD_HEIGHT.cubeThick;
|
||||
|
||||
const flavour = AttachmentBlockSchema.model.flavour;
|
||||
|
||||
const blocks = propsArray.map((props, index) => {
|
||||
@@ -312,7 +227,7 @@ export async function addAttachments(
|
||||
return { flavour, blockProps: { ...props, style, xywh } };
|
||||
});
|
||||
|
||||
const blockIds = std.store.addBlocks(blocks);
|
||||
const blockIds = std.store.addBlocks(blocks, gfx.surface);
|
||||
|
||||
gfx.selection.set({
|
||||
elements: blockIds,
|
||||
|
||||
@@ -1,9 +1,102 @@
|
||||
import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed';
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
BookmarkBlockSchema,
|
||||
FootNoteReferenceParamsSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
getFootnoteDefinitionText,
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
if (!isFootnoteDefinitionNode(node)) return false;
|
||||
const footnoteDefinition = getFootnoteDefinitionText(node);
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
return (
|
||||
footnoteDefinitionJson.type === 'url' && !!footnoteDefinitionJson.url
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const bookmarkBlockMarkdownAdapterMatcher =
|
||||
createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour);
|
||||
createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour, {
|
||||
toMatch: o => isUrlFootnoteDefinitionNode(o.node),
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { provider } = context;
|
||||
let enableCitation = false;
|
||||
try {
|
||||
const featureFlagService = provider?.get(FeatureFlagService);
|
||||
enableCitation = !!featureFlagService?.getFlag('enable_citation');
|
||||
} catch {
|
||||
enableCitation = false;
|
||||
}
|
||||
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { walkerContext, configs } = context;
|
||||
const footnoteIdentifier = o.node.identifier;
|
||||
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
|
||||
const footnoteDefinition = configs.get(footnoteDefinitionKey);
|
||||
if (!footnoteDefinition) {
|
||||
return;
|
||||
}
|
||||
let footnoteDefinitionJson;
|
||||
try {
|
||||
footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
// If the footnote definition contains url, decode it
|
||||
if (footnoteDefinitionJson.url) {
|
||||
footnoteDefinitionJson.url = decodeURIComponent(
|
||||
footnoteDefinitionJson.url
|
||||
);
|
||||
}
|
||||
if (footnoteDefinitionJson.favicon) {
|
||||
footnoteDefinitionJson.favicon = decodeURIComponent(
|
||||
footnoteDefinitionJson.favicon
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse or decode footnote definition:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, favicon, title, description } = footnoteDefinitionJson;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: BookmarkBlockSchema.model.flavour,
|
||||
props: {
|
||||
url,
|
||||
footnoteIdentifier,
|
||||
icon: favicon,
|
||||
title,
|
||||
description,
|
||||
style: 'citation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const BookmarkBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
@@ -27,6 +28,14 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
|
||||
protected containerStyleMap!: ReturnType<typeof styleMap>;
|
||||
|
||||
selectBlock = () => {
|
||||
const selectionManager = this.std.selection;
|
||||
const blockSelection = selectionManager.create(BlockSelection, {
|
||||
blockId: this.blockId,
|
||||
});
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
};
|
||||
|
||||
open = () => {
|
||||
let link = this.model.props.url;
|
||||
if (!link.match(/^[a-zA-Z]+:\/\//)) {
|
||||
@@ -41,6 +50,37 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
);
|
||||
};
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
}
|
||||
|
||||
private readonly _renderCitationView = () => {
|
||||
const { title, description, url, icon, footnoteIdentifier } =
|
||||
this.model.props;
|
||||
return html`
|
||||
<affine-citation-card
|
||||
.icon=${icon}
|
||||
.citationTitle=${title || url}
|
||||
.citationContent=${description}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
.onClickCallback=${this.selectBlock}
|
||||
.onDoubleClickCallback=${this.open}
|
||||
.active=${this.selected$.value}
|
||||
></affine-citation-card>
|
||||
`;
|
||||
};
|
||||
|
||||
private readonly _renderCardView = () => {
|
||||
return html`<bookmark-card
|
||||
.bookmark=${this}
|
||||
.loading=${this.loading}
|
||||
.error=${this.error}
|
||||
></bookmark-card>`;
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -58,6 +98,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
this.contentEditable = 'false';
|
||||
|
||||
if (!this.model.props.description && !this.model.props.title) {
|
||||
if (this.doc.readonly) {
|
||||
return;
|
||||
}
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
@@ -85,11 +128,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
})}
|
||||
style=${this.containerStyleMap}
|
||||
>
|
||||
<bookmark-card
|
||||
.bookmark=${this}
|
||||
.loading=${this.loading}
|
||||
.error=${this.error}
|
||||
></bookmark-card>
|
||||
${this.isCitation ? this._renderCitationView() : this._renderCardView()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { OpenInNewIcon } from '@blocksuite/icons/lit';
|
||||
import {
|
||||
BlockSelection,
|
||||
isGfxBlockComponent,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/std';
|
||||
import { isGfxBlockComponent, ShadowlessElement } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
@@ -27,7 +23,7 @@ export class BookmarkCard extends SignalWatcher(
|
||||
const model = this.bookmark.model;
|
||||
|
||||
if (model.parent?.flavour !== 'affine:surface') {
|
||||
this._selectBlock();
|
||||
this.bookmark.selectBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +32,6 @@ export class BookmarkCard extends SignalWatcher(
|
||||
this.bookmark.open();
|
||||
}
|
||||
|
||||
private _selectBlock() {
|
||||
const selectionManager = this.bookmark.host.selection;
|
||||
const blockSelection = selectionManager.create(BlockSelection, {
|
||||
blockId: this.bookmark.blockId,
|
||||
});
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
|
||||
@@ -1,16 +1,91 @@
|
||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EmbedLinkedDocBlockSchema,
|
||||
FootNoteReferenceParamsSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
AdapterTextUtils,
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
getFootnoteDefinitionText,
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isLinkedDocFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
if (!isFootnoteDefinitionNode(node)) return false;
|
||||
const footnoteDefinition = getFootnoteDefinitionText(node);
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
return (
|
||||
footnoteDefinitionJson.type === 'doc' && !!footnoteDefinitionJson.docId
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||
{
|
||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
toMatch: o => isLinkedDocFootnoteDefinitionNode(o.node),
|
||||
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { provider } = context;
|
||||
let enableCitation = false;
|
||||
try {
|
||||
const featureFlagService = provider?.get(FeatureFlagService);
|
||||
enableCitation = !!featureFlagService?.getFlag('enable_citation');
|
||||
} catch {
|
||||
enableCitation = false;
|
||||
}
|
||||
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { walkerContext, configs } = context;
|
||||
const footnoteIdentifier = o.node.identifier;
|
||||
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
|
||||
const footnoteDefinition = configs.get(footnoteDefinitionKey);
|
||||
if (!footnoteDefinition) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
const { docId } = footnoteDefinitionJson;
|
||||
if (!docId) {
|
||||
return;
|
||||
}
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||
props: {
|
||||
pageId: docId,
|
||||
footnoteIdentifier,
|
||||
style: 'citation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse linked doc footnote definition:', err);
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { configs, walkerContext } = context;
|
||||
|
||||
@@ -53,11 +53,11 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
|
||||
doc.deleteBlock(this.model);
|
||||
};
|
||||
|
||||
protected override _handleClick(evt: MouseEvent): void {
|
||||
protected override _handleClick = (evt: MouseEvent): void => {
|
||||
if (isNewTabTrigger(evt)) {
|
||||
this.open({ openMode: 'open-in-new-tab', event: evt });
|
||||
} else if (isNewViewTrigger(evt)) {
|
||||
this.open({ openMode: 'open-in-new-view', event: evt });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +55,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _load = async () => {
|
||||
// If this is a citation linked doc block, we don't need to load the linked doc and render linked doc content in card
|
||||
if (this.isCitation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
loading = true,
|
||||
isError = false,
|
||||
@@ -243,6 +248,17 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
return doc?.getStore({ id: this.model.props.pageId });
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return this.doc.readonly;
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDoubleClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
const openDocService = this.std.get(OpenDocExtensionIdentifier);
|
||||
@@ -264,105 +280,42 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
return !!linkedDoc && this.isNoteContentEmpty && this.isBannerEmpty;
|
||||
}
|
||||
|
||||
protected _handleClick(event: MouseEvent) {
|
||||
protected _handleClick = (event: MouseEvent) => {
|
||||
if (isNewTabTrigger(event)) {
|
||||
this.open({ openMode: 'open-in-new-tab', event });
|
||||
} else if (isNewViewTrigger(event)) {
|
||||
this.open({ openMode: 'open-in-new-view', event });
|
||||
}
|
||||
this._selectBlock();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._cardStyle = this.model.props.style;
|
||||
this._referenceToNode = referenceToNode(this.model.props);
|
||||
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
|
||||
const linkedDoc = this.linkedDoc;
|
||||
if (linkedDoc) {
|
||||
this.disposables.add(
|
||||
linkedDoc.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
})
|
||||
);
|
||||
// Should throttle the blockUpdated event to avoid too many re-renders
|
||||
// Because the blockUpdated event is triggered too frequently at some cases
|
||||
this.disposables.add(
|
||||
linkedDoc.slots.blockUpdated.subscribe(
|
||||
throttle(payload => {
|
||||
if (
|
||||
payload.type === 'update' &&
|
||||
['', 'caption', 'xywh'].includes(payload.props.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'add' && payload.init) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
}, RENDER_CARD_THROTTLE_MS)
|
||||
)
|
||||
);
|
||||
|
||||
this._setDocUpdatedAt();
|
||||
this.disposables.add(
|
||||
this.doc.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._setDocUpdatedAt();
|
||||
})
|
||||
);
|
||||
|
||||
if (this._referenceToNode) {
|
||||
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
|
||||
} else {
|
||||
const docMode = this.std.get(DocModeProvider);
|
||||
this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId);
|
||||
this.disposables.add(
|
||||
docMode.onPrimaryModeChange(mode => {
|
||||
this._linkedDocMode = mode;
|
||||
}, this.model.props.pageId)
|
||||
);
|
||||
}
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this._selectBlock();
|
||||
};
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.subscribe(({ key }) => {
|
||||
if (key === 'style') {
|
||||
this._cardStyle = this.model.props.style;
|
||||
}
|
||||
if (key === 'pageId' || key === 'style') {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
private readonly _renderCitationView = () => {
|
||||
const { footnoteIdentifier } = this.model.props;
|
||||
return html`<div
|
||||
draggable="${this.blockDraggable ? 'true' : 'false'}"
|
||||
class=${classMap({
|
||||
'embed-block-container': true,
|
||||
...this.selectedStyle$?.value,
|
||||
})}
|
||||
style=${styleMap({
|
||||
...this.embedContainerStyle,
|
||||
})}
|
||||
>
|
||||
<affine-citation-card
|
||||
.icon=${this.icon$.value}
|
||||
.citationTitle=${this.title$.value}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
.active=${this.selected$.value}
|
||||
.onClickCallback=${this._handleClick}
|
||||
></affine-citation-card>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
getInitialState(): {
|
||||
loading?: boolean;
|
||||
isError?: boolean;
|
||||
isNoteContentEmpty?: boolean;
|
||||
isBannerEmpty?: boolean;
|
||||
} {
|
||||
return {};
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
private readonly _renderEmbedView = () => {
|
||||
const linkedDoc = this.linkedDoc;
|
||||
const isDeleted = !linkedDoc;
|
||||
const isLoading = this._loading;
|
||||
@@ -502,9 +455,107 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
</div>
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._cardStyle = this.model.props.style;
|
||||
this._referenceToNode = referenceToNode(this.model.props);
|
||||
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
|
||||
const linkedDoc = this.linkedDoc;
|
||||
if (linkedDoc) {
|
||||
this.disposables.add(
|
||||
linkedDoc.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
})
|
||||
);
|
||||
// Should throttle the blockUpdated event to avoid too many re-renders
|
||||
// Because the blockUpdated event is triggered too frequently at some cases
|
||||
this.disposables.add(
|
||||
linkedDoc.slots.blockUpdated.subscribe(
|
||||
throttle(payload => {
|
||||
if (
|
||||
payload.type === 'update' &&
|
||||
['', 'caption', 'xywh'].includes(payload.props.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'add' && payload.init) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
}, RENDER_CARD_THROTTLE_MS)
|
||||
)
|
||||
);
|
||||
|
||||
this._setDocUpdatedAt();
|
||||
this.disposables.add(
|
||||
this.doc.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._setDocUpdatedAt();
|
||||
})
|
||||
);
|
||||
|
||||
if (this._referenceToNode) {
|
||||
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
|
||||
} else {
|
||||
const docMode = this.std.get(DocModeProvider);
|
||||
this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId);
|
||||
this.disposables.add(
|
||||
docMode.onPrimaryModeChange(mode => {
|
||||
this._linkedDocMode = mode;
|
||||
}, this.model.props.pageId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.subscribe(({ key }) => {
|
||||
if (key === 'style') {
|
||||
this._cardStyle = this.model.props.style;
|
||||
}
|
||||
if (key === 'pageId' || key === 'style') {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getInitialState(): {
|
||||
loading?: boolean;
|
||||
isError?: boolean;
|
||||
isNoteContentEmpty?: boolean;
|
||||
isBannerEmpty?: boolean;
|
||||
} {
|
||||
return {};
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return this.isCitation
|
||||
? this._renderCitationView()
|
||||
: this._renderEmbedView();
|
||||
}
|
||||
|
||||
override updated() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
// update card style when linked doc deleted
|
||||
const linkedDoc = this.linkedDoc;
|
||||
const { xywh, style } = this.model.props;
|
||||
|
||||
@@ -375,6 +375,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DEFAULT_NOTE_HEIGHT
|
||||
).serialize(),
|
||||
index: gfx.layer.generateIndex(),
|
||||
displayMode: NoteDisplayMode.EdgelessOnly,
|
||||
} satisfies Partial<NoteProps>,
|
||||
ctx.store.root
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
type ToolbarModuleConfig,
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
TextAlignCenterIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
@@ -49,6 +52,45 @@ const builtinToolbarConfig = {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.1.align-left',
|
||||
tooltip: 'Align left',
|
||||
icon: TextAlignLeftIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (block) {
|
||||
ctx.std.host.doc.updateBlock(block.model, {
|
||||
textAlign: TextAlign.Left,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.2.align-center',
|
||||
tooltip: 'Align center',
|
||||
icon: TextAlignCenterIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (block) {
|
||||
ctx.std.host.doc.updateBlock(block.model, {
|
||||
textAlign: TextAlign.Center,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.3.align-right',
|
||||
tooltip: 'Align right',
|
||||
icon: TextAlignRightIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (block) {
|
||||
ctx.std.host.doc.updateBlock(block.model, {
|
||||
textAlign: TextAlign.Right,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
|
||||
@@ -112,6 +112,15 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const alignItemsStyleMap = styleMap({
|
||||
alignItems:
|
||||
this.model.props.textAlign$.value === 'left'
|
||||
? 'flex-start'
|
||||
: this.model.props.textAlign$.value === 'right'
|
||||
? 'flex-end'
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="affine-image-container" style=${containerStyleMap}>
|
||||
${when(
|
||||
@@ -122,7 +131,11 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
.loading=${this.loading}
|
||||
.mode=${'page'}
|
||||
></affine-image-fallback-card>`,
|
||||
() => html`<affine-page-image .block=${this}></affine-page-image>`
|
||||
() =>
|
||||
html`<affine-page-image
|
||||
.block=${this}
|
||||
style="${alignItemsStyleMap}"
|
||||
></affine-page-image>`
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -78,8 +78,7 @@ export class ImageResizeManager {
|
||||
const rootComponent = getClosestRootBlockComponent(this._activeComponent);
|
||||
if (
|
||||
rootComponent &&
|
||||
rootComponent.service.std.get(DocModeProvider).getEditorMode() ===
|
||||
'edgeless'
|
||||
rootComponent.std.get(DocModeProvider).getEditorMode() === 'edgeless'
|
||||
) {
|
||||
const viewport = rootComponent.std.get(GfxControllerIdentifier).viewport;
|
||||
this._zoom = viewport.zoom;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ImageBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
FileSizeLimitService,
|
||||
FileSizeLimitProvider,
|
||||
NativeClipboardProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
@@ -362,7 +362,7 @@ export function shouldResizeImage(node: Node, target: EventTarget | null) {
|
||||
function hasExceeded(
|
||||
std: BlockStdScope,
|
||||
files: File[],
|
||||
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
|
||||
maxFileSize = std.get(FileSizeLimitProvider).maxFileSize
|
||||
) {
|
||||
const exceeded = files.some(file => file.size > maxFileSize);
|
||||
|
||||
@@ -481,10 +481,12 @@ export async function addImages(
|
||||
|
||||
// If maxWidth is provided, limit the width of the image to maxWidth
|
||||
// Otherwise, use the original width
|
||||
const width = maxWidth ? Math.min(props.width, maxWidth) : props.width;
|
||||
const height = maxWidth
|
||||
? (props.height / props.width) * width
|
||||
: props.height;
|
||||
if (maxWidth) {
|
||||
const p = props.height / props.width;
|
||||
props.width = Math.min(props.width, maxWidth);
|
||||
props.height = props.width * p;
|
||||
}
|
||||
const { width, height } = props;
|
||||
|
||||
const xywh = calcBoundByOrigin(
|
||||
center,
|
||||
|
||||
@@ -144,6 +144,10 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
||||
|
||||
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
|
||||
|
||||
const textAlignStyle = styleMap({
|
||||
textAlign: this.model.props.textAlign$?.value,
|
||||
});
|
||||
|
||||
const children = html`<div
|
||||
class="affine-block-children-container"
|
||||
style=${styleMap({
|
||||
@@ -155,7 +159,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
||||
</div>`;
|
||||
|
||||
return html`
|
||||
<div class=${'affine-list-block-container'}>
|
||||
<div class=${'affine-list-block-container'} style="${textAlignStyle}">
|
||||
<div
|
||||
class=${classMap({
|
||||
'affine-list-rich-text-wrapper': true,
|
||||
|
||||
@@ -3,14 +3,13 @@ import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { FootnoteDefinition, Root } from 'mdast';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import type { Root } from 'mdast';
|
||||
|
||||
const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
|
||||
const isFootnoteDefinitionNode = (
|
||||
node: MarkdownAST
|
||||
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
|
||||
|
||||
const createFootnoteDefinition = (
|
||||
identifier: string,
|
||||
@@ -67,10 +66,35 @@ const createNoteBlockMarkdownAdapterMatcher = (
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the footnoteDefinition node from the noteAst
|
||||
noteAst.children = noteAst.children.filter(
|
||||
child => !isFootnoteDefinitionNode(child)
|
||||
);
|
||||
const { provider } = context;
|
||||
let enableCitation = false;
|
||||
try {
|
||||
const featureFlagService = provider?.get(FeatureFlagService);
|
||||
enableCitation = !!featureFlagService?.getFlag('enable_citation');
|
||||
} catch {
|
||||
enableCitation = false;
|
||||
}
|
||||
if (enableCitation) {
|
||||
// if there are footnoteDefinition nodes, add a heading node to the noteAst before the first footnoteDefinition node
|
||||
const footnoteDefinitionIndex = noteAst.children.findIndex(child =>
|
||||
isFootnoteDefinitionNode(child)
|
||||
);
|
||||
if (footnoteDefinitionIndex !== -1) {
|
||||
noteAst.children.splice(footnoteDefinitionIndex, 0, {
|
||||
type: 'heading',
|
||||
depth: 6,
|
||||
data: {
|
||||
collapsed: true,
|
||||
},
|
||||
children: [{ type: 'text', value: 'Sources' }],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Remove the footnoteDefinition node from the noteAst
|
||||
noteAst.children = noteAst.children.filter(
|
||||
child => !isFootnoteDefinitionNode(child)
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
|
||||
@@ -4,9 +4,15 @@ import {
|
||||
textFormatConfigs,
|
||||
} from '@blocksuite/affine-inline-preset';
|
||||
import {
|
||||
type TextAlignConfig,
|
||||
textAlignConfigs,
|
||||
type TextConversionConfig,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type SlashMenuActionItem,
|
||||
@@ -56,6 +62,10 @@ const noteSlashMenuConfig: SlashMenuConfig = {
|
||||
createConversionItem(config, `1_List@${index++}`)
|
||||
),
|
||||
|
||||
...textAlignConfigs.map((config, index) =>
|
||||
createAlignItem(config, `2_Align@${index++}`)
|
||||
),
|
||||
|
||||
...textFormatConfigs
|
||||
.filter(i => !['Code', 'Link'].includes(i.name))
|
||||
.map((config, index) =>
|
||||
@@ -85,6 +95,31 @@ function createConversionItem(
|
||||
};
|
||||
}
|
||||
|
||||
function createAlignItem(
|
||||
config: TextAlignConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
): SlashMenuActionItem {
|
||||
const { textAlign, name, icon } = config;
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
icon,
|
||||
action: ({ std }) => {
|
||||
std.command
|
||||
.chain()
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(getSelectedModelsCommand, { types: ['text'] })
|
||||
.pipe((ctx, next) => {
|
||||
ctx.selectedModels.forEach(model => {
|
||||
ctx.std.host.doc.updateBlock(model, { textAlign });
|
||||
});
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTextFormatItem(
|
||||
config: TextFormatConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
|
||||
@@ -5,13 +5,17 @@ import {
|
||||
NoteBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
textAlignConfigs,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
focusBlockEnd,
|
||||
focusBlockStart,
|
||||
getBlockSelectionsCommand,
|
||||
getNextBlockCommand,
|
||||
getPrevBlockCommand,
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
@@ -157,6 +161,48 @@ class NoteKeymap {
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindTextAlignHotKey = () => {
|
||||
return textAlignConfigs.reduce(
|
||||
(acc, item) => {
|
||||
const keymap = item.hotkey!.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getTextSelectionCommand),
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
|
||||
.pipe((ctx, next) => {
|
||||
ctx.selectedModels.forEach(model => {
|
||||
ctx.std.host.doc.updateBlock(model, {
|
||||
textAlign: item.textAlign,
|
||||
});
|
||||
});
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...keymap,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private _focusBlock: BlockComponent | null = null;
|
||||
|
||||
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
||||
@@ -568,6 +614,7 @@ class NoteKeymap {
|
||||
...this._bindMoveBlockHotKey(),
|
||||
...this._bindQuickActionHotKey(),
|
||||
...this._bindTextConversionHotKey(),
|
||||
...this._bindTextAlignHotKey(),
|
||||
Tab: ctx => {
|
||||
const [success] = this.std.command.exec(indentBlocks);
|
||||
|
||||
|
||||
@@ -9,6 +9,15 @@ import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { Heading } from 'mdast';
|
||||
|
||||
/**
|
||||
* Extend the HeadingData type to include the collapsed property
|
||||
*/
|
||||
declare module 'mdast' {
|
||||
interface HeadingData {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const PARAGRAPH_MDAST_TYPE = new Set(['paragraph', 'heading', 'blockquote']);
|
||||
|
||||
const isParagraphMDASTType = (node: MarkdownAST) =>
|
||||
@@ -46,6 +55,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||
break;
|
||||
}
|
||||
case 'heading': {
|
||||
const isCollapsed = !!o.node.data?.collapsed;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
@@ -54,6 +64,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: `h${o.node.depth}`,
|
||||
collapsed: isCollapsed,
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(o.node),
|
||||
|
||||
@@ -235,6 +235,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
`;
|
||||
}
|
||||
|
||||
const textAlignStyle = styleMap({
|
||||
textAlign: this.model.props.textAlign$?.value,
|
||||
});
|
||||
|
||||
const children = html`<div
|
||||
class="affine-block-children-container"
|
||||
style=${styleMap({
|
||||
@@ -256,6 +260,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
</style>
|
||||
<div
|
||||
class="affine-paragraph-block-container"
|
||||
style="${textAlignStyle}"
|
||||
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
AttachmentAdapter,
|
||||
ClipboardAdapter,
|
||||
copyMiddleware,
|
||||
defaultImageProxyMiddleware,
|
||||
HtmlAdapter,
|
||||
ImageAdapter,
|
||||
MixTextAdapter,
|
||||
NotionTextAdapter,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
@@ -15,67 +9,7 @@ import {
|
||||
getSelectedModelsCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import {
|
||||
ClipboardAdapterConfigExtension,
|
||||
LifeCycleWatcher,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: ClipboardAdapter.MIME,
|
||||
adapter: ClipboardAdapter,
|
||||
priority: 100,
|
||||
});
|
||||
|
||||
const NotionClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: 'text/_notion-text-production',
|
||||
adapter: NotionTextAdapter,
|
||||
priority: 95,
|
||||
});
|
||||
|
||||
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: 'text/html',
|
||||
adapter: HtmlAdapter,
|
||||
priority: 90,
|
||||
});
|
||||
|
||||
const imageClipboardConfigs = [
|
||||
'image/apng',
|
||||
'image/avif',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
].map(mimeType => {
|
||||
return ClipboardAdapterConfigExtension({
|
||||
mimeType,
|
||||
adapter: ImageAdapter,
|
||||
priority: 80,
|
||||
});
|
||||
});
|
||||
|
||||
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: 'text/plain',
|
||||
adapter: MixTextAdapter,
|
||||
priority: 70,
|
||||
});
|
||||
|
||||
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: '*/*',
|
||||
adapter: AttachmentAdapter,
|
||||
priority: 60,
|
||||
});
|
||||
|
||||
export const clipboardConfigs: ExtensionType[] = [
|
||||
SnapshotClipboardConfig,
|
||||
NotionClipboardConfig,
|
||||
HtmlClipboardConfig,
|
||||
...imageClipboardConfigs,
|
||||
PlainTextClipboardConfig,
|
||||
AttachmentClipboardConfig,
|
||||
];
|
||||
import { LifeCycleWatcher, type UIEventHandler } from '@blocksuite/std';
|
||||
|
||||
/**
|
||||
* ReadOnlyClipboard is a class that provides a read-only clipboard for the root block.
|
||||
|
||||
@@ -4,16 +4,12 @@ import { BlockFlavourIdentifier, FlavourExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { RootBlockAdapterExtensions } from '../adapters/extension';
|
||||
import { clipboardConfigs } from '../clipboard';
|
||||
import { builtinToolbarConfig } from '../configs/toolbar';
|
||||
import { fallbackKeymap } from '../keyboard/keymap';
|
||||
import { viewportOverlayWidget } from './widgets';
|
||||
|
||||
export const CommonSpecs: ExtensionType[] = [
|
||||
FlavourExtension('affine:page'),
|
||||
...RootBlockAdapterExtensions,
|
||||
...clipboardConfigs,
|
||||
viewportOverlayWidget,
|
||||
fallbackKeymap,
|
||||
|
||||
ToolbarModuleExtension({
|
||||
@@ -21,5 +17,3 @@ export const CommonSpecs: ExtensionType[] = [
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
];
|
||||
|
||||
export * from './widgets';
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { WidgetViewExtension } from '@blocksuite/std';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { AFFINE_VIEWPORT_OVERLAY_WIDGET } from '../widgets/viewport-overlay/viewport-overlay.js';
|
||||
|
||||
export const viewportOverlayWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}`
|
||||
);
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
isFormatSupported,
|
||||
textFormatConfigs,
|
||||
} from '@blocksuite/affine-inline-preset';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
||||
import type { TextAlign } from '@blocksuite/affine-model';
|
||||
import {
|
||||
textAlignConfigs,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
copySelectedModelsCommand,
|
||||
deleteSelectedModelsCommand,
|
||||
@@ -39,6 +43,7 @@ import type {
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { ActionPlacement } from '@blocksuite/affine-shared/services';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
||||
import {
|
||||
CopyIcon,
|
||||
@@ -119,6 +124,72 @@ const conversionsActionGroup = {
|
||||
},
|
||||
} as const satisfies ToolbarActionGenerator;
|
||||
|
||||
const alignActionGroup = {
|
||||
id: 'b.align',
|
||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||
generate({ chain }) {
|
||||
const [ok, { selectedModels = [] }] = chain
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getTextSelectionCommand),
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
|
||||
.run();
|
||||
if (!ok) return null;
|
||||
|
||||
const alignment =
|
||||
textAlignConfigs.find(
|
||||
({ textAlign }) =>
|
||||
textAlign ===
|
||||
getMostCommonValue(
|
||||
selectedModels.map(
|
||||
({ props }) => props as { textAlign?: TextAlign }
|
||||
),
|
||||
'textAlign'
|
||||
)
|
||||
) ?? textAlignConfigs[0];
|
||||
const update = (textAlign: TextAlign) => {
|
||||
chain
|
||||
.pipe((ctx, next) => {
|
||||
selectedModels.forEach(model => {
|
||||
ctx.std.host.doc.updateBlock(model, { textAlign });
|
||||
});
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
};
|
||||
|
||||
return {
|
||||
content: html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button aria-label="Align" .tooltip="${'Align'}">
|
||||
${alignment.icon} ${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${repeat(
|
||||
textAlignConfigs,
|
||||
item => item.name,
|
||||
({ textAlign, name, icon }) => html`
|
||||
<editor-menu-action
|
||||
aria-label=${name}
|
||||
?data-selected=${alignment.textAlign === textAlign}
|
||||
@click=${() => update(textAlign)}
|
||||
>
|
||||
${icon}<span class="label">${name}</span>
|
||||
</editor-menu-action>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
} as const satisfies ToolbarActionGenerator;
|
||||
|
||||
const inlineTextActionGroup = {
|
||||
id: 'b.inline-text',
|
||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||
@@ -269,6 +340,7 @@ const turnIntoLinkedDoc = {
|
||||
export const builtinToolbarConfig = {
|
||||
actions: [
|
||||
conversionsActionGroup,
|
||||
alignActionGroup,
|
||||
inlineTextActionGroup,
|
||||
highlightActionGroup,
|
||||
turnIntoDatabase,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
RootBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import { BlockService, type BlockStdScope } from '@blocksuite/std';
|
||||
import type {
|
||||
GfxController,
|
||||
GfxModel,
|
||||
@@ -30,10 +30,12 @@ import {
|
||||
import { effect } from '@preact/signals-core';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
|
||||
import { RootService } from '../root-service.js';
|
||||
import { getCursorMode } from './utils/query.js';
|
||||
|
||||
export class EdgelessRootService extends RootService implements SurfaceContext {
|
||||
export class EdgelessRootService
|
||||
extends BlockService
|
||||
implements SurfaceContext
|
||||
{
|
||||
static override readonly flavour = RootBlockSchema.model.flavour;
|
||||
|
||||
private readonly _surface: SurfaceBlockModel;
|
||||
|
||||
@@ -9,19 +9,12 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { CommonSpecs } from '../common-specs/index.js';
|
||||
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
|
||||
import { EdgelessClipboardController } from './clipboard/clipboard.js';
|
||||
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
|
||||
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
|
||||
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
|
||||
import { quickTools } from './components/toolbar/tools.js';
|
||||
import { EdgelessRootService } from './edgeless-root-service.js';
|
||||
|
||||
export const edgelessZoomToolbarWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}`
|
||||
);
|
||||
export const edgelessDraggingAreaWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
EDGELESS_DRAGGING_AREA_WIDGET,
|
||||
@@ -51,13 +44,11 @@ const EdgelessCommonExtension: ExtensionType[] = [
|
||||
CommonSpecs,
|
||||
EdgelessRootService,
|
||||
ViewportElementExtension('.affine-edgeless-viewport'),
|
||||
...quickTools,
|
||||
].flat();
|
||||
|
||||
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||
...EdgelessCommonExtension,
|
||||
BlockViewExtension('affine:page', literal`affine-edgeless-root`),
|
||||
edgelessZoomToolbarWidget,
|
||||
edgelessDraggingAreaWidget,
|
||||
noteSlicerWidget,
|
||||
edgelessSelectedRectWidget,
|
||||
|
||||
@@ -12,35 +12,16 @@ import {
|
||||
EDGELESS_SELECTED_RECT_WIDGET,
|
||||
EdgelessSelectedRectWidget,
|
||||
} from './edgeless/components/rects/edgeless-selected-rect.js';
|
||||
import { EdgelessSlideMenu } from './edgeless/components/toolbar/common/slide-menu.js';
|
||||
import { ToolbarArrowUpIcon } from './edgeless/components/toolbar/common/toolbar-arrow-up-icon.js';
|
||||
import { EdgelessLinkToolButton } from './edgeless/components/toolbar/link/link-tool-button.js';
|
||||
import {
|
||||
EdgelessRootBlockComponent,
|
||||
EdgelessRootPreviewBlockComponent,
|
||||
PageRootBlockComponent,
|
||||
PreviewRootBlockComponent,
|
||||
} from './index.js';
|
||||
import {
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
AffineEdgelessZoomToolbarWidget,
|
||||
} from './widgets/edgeless-zoom-toolbar/index.js';
|
||||
import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js';
|
||||
import { EdgelessZoomToolbar } from './widgets/edgeless-zoom-toolbar/zoom-toolbar.js';
|
||||
import {
|
||||
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
|
||||
AffinePageDraggingAreaWidget,
|
||||
} from './widgets/page-dragging-area/page-dragging-area.js';
|
||||
import {
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
AffineViewportOverlayWidget,
|
||||
} from './widgets/viewport-overlay/viewport-overlay.js';
|
||||
|
||||
export function effects() {
|
||||
// Register components by category
|
||||
registerRootComponents();
|
||||
registerWidgets();
|
||||
registerEdgelessToolbarComponents();
|
||||
registerMiscComponents();
|
||||
}
|
||||
|
||||
@@ -54,37 +35,7 @@ function registerRootComponents() {
|
||||
);
|
||||
}
|
||||
|
||||
function registerWidgets() {
|
||||
customElements.define(
|
||||
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
|
||||
AffinePageDraggingAreaWidget
|
||||
);
|
||||
customElements.define(
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
AffineViewportOverlayWidget
|
||||
);
|
||||
customElements.define(
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
AffineEdgelessZoomToolbarWidget
|
||||
);
|
||||
}
|
||||
|
||||
function registerEdgelessToolbarComponents() {
|
||||
// Tool buttons
|
||||
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
|
||||
|
||||
// Menus
|
||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||
|
||||
// Toolbar components
|
||||
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
|
||||
}
|
||||
|
||||
function registerMiscComponents() {
|
||||
// Toolbar and UI components
|
||||
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
|
||||
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
|
||||
|
||||
// Auto-complete components
|
||||
customElements.define(
|
||||
'edgeless-auto-complete-panel',
|
||||
@@ -115,13 +66,6 @@ declare global {
|
||||
'note-slicer': NoteSlicer;
|
||||
'edgeless-dragging-area-rect': EdgelessDraggingAreaRectWidget;
|
||||
'edgeless-selected-rect': EdgelessSelectedRectWidget;
|
||||
'edgeless-slide-menu': EdgelessSlideMenu;
|
||||
'toolbar-arrow-up-icon': ToolbarArrowUpIcon;
|
||||
'edgeless-link-tool-button': EdgelessLinkToolButton;
|
||||
'affine-page-root': PageRootBlockComponent;
|
||||
'zoom-bar-toggle-button': ZoomBarToggleButton;
|
||||
'edgeless-zoom-toolbar': EdgelessZoomToolbar;
|
||||
|
||||
[AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET]: AffineEdgelessZoomToolbarWidget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,10 @@ export * from './edgeless/edgeless-builtin-spec.js';
|
||||
export * from './edgeless/edgeless-root-spec.js';
|
||||
export * from './edgeless/index.js';
|
||||
export * from './page/page-root-block.js';
|
||||
export { PageRootService } from './page/page-root-service.js';
|
||||
export * from './page/page-root-spec.js';
|
||||
export * from './preview/preview-root-block.js';
|
||||
export { RootService } from './root-service.js';
|
||||
export * from './types.js';
|
||||
export * from './utils/index.js';
|
||||
export * from './widgets/index.js';
|
||||
|
||||
declare type _GLOBAL_ =
|
||||
| typeof PointerEffect
|
||||
|
||||
@@ -28,7 +28,6 @@ import { query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
|
||||
import type { PageRootService } from './page-root-service.js';
|
||||
|
||||
const DOC_BLOCK_CHILD_PADDING = 24;
|
||||
const DOC_BOTTOM_PADDING = 32;
|
||||
@@ -49,10 +48,7 @@ function testClickOnBlankArea(
|
||||
return state.raw.clientX < blankLeft || state.raw.clientX > blankRight;
|
||||
}
|
||||
|
||||
export class PageRootBlockComponent extends BlockComponent<
|
||||
RootBlockModel,
|
||||
PageRootService
|
||||
> {
|
||||
export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
|
||||
static override styles = css`
|
||||
editor-host:has(> affine-page-root, * > affine-page-root) {
|
||||
display: block;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { RootBlockSchema } from '@blocksuite/affine-model';
|
||||
|
||||
import { RootService } from '../root-service.js';
|
||||
|
||||
export class PageRootService extends RootService {
|
||||
static override readonly flavour = RootBlockSchema.model.flavour;
|
||||
}
|
||||
@@ -1,23 +1,13 @@
|
||||
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
|
||||
import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/std';
|
||||
import { BlockViewExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { PageClipboard } from '../clipboard/page-clipboard.js';
|
||||
import { CommonSpecs } from '../common-specs/index.js';
|
||||
import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from '../widgets/page-dragging-area/page-dragging-area.js';
|
||||
import { PageRootService } from './page-root-service.js';
|
||||
|
||||
export const pageDraggingAreaWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_PAGE_DRAGGING_AREA_WIDGET)}`
|
||||
);
|
||||
|
||||
const PageCommonExtension: ExtensionType[] = [
|
||||
CommonSpecs,
|
||||
PageRootService,
|
||||
pageDraggingAreaWidget,
|
||||
ViewportElementExtension('.affine-page-viewport'),
|
||||
].flat();
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { RootBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockService } from '@blocksuite/std';
|
||||
|
||||
export abstract class RootService extends BlockService {
|
||||
static override readonly flavour = RootBlockSchema.model.flavour;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js';
|
||||
export { AffinePageDraggingAreaWidget } from './page-dragging-area/page-dragging-area.js';
|
||||
export * from './viewport-overlay/viewport-overlay.js';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FrameBlockComponent } from '@blocksuite/affine-block-frame';
|
||||
import { FrameBlockComponent } from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
getSurfaceBlock,
|
||||
@@ -24,12 +24,7 @@ import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import {
|
||||
Bound,
|
||||
deserializeXYWH,
|
||||
type SerializedXYWH,
|
||||
} from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import { Bound, type SerializedXYWH } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
BlockComponent,
|
||||
BlockSelection,
|
||||
@@ -127,6 +122,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
|
||||
private _referencedModel: GfxModel | null = null;
|
||||
|
||||
// since the xywh of edgeless element is not a signal, we need to use a signal to store the xywh
|
||||
private readonly _referenceXYWH$ = signal<SerializedXYWH | null>(null);
|
||||
|
||||
private get _shouldRender() {
|
||||
@@ -263,6 +259,8 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
}
|
||||
|
||||
private _initViewport() {
|
||||
this._referenceXYWH$.value = this.referenceModel?.xywh ?? null;
|
||||
|
||||
const refreshViewport = () => {
|
||||
if (!this._referenceXYWH$.value) return;
|
||||
const previewEditorHost = this.previewEditor;
|
||||
@@ -270,14 +268,12 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
const gfx = previewEditorHost.std.get(GfxControllerIdentifier);
|
||||
const viewport = gfx.viewport;
|
||||
|
||||
let bound = Bound.deserialize(this._referenceXYWH$.value);
|
||||
const w = Math.max(this.getBoundingClientRect().width, bound.w);
|
||||
const aspectRatio = bound.w / bound.h;
|
||||
const h = w / aspectRatio;
|
||||
|
||||
bound = Bound.fromCenter(bound.center, w, h);
|
||||
|
||||
viewport.setViewportByBound(bound);
|
||||
viewport.setViewportByBound(
|
||||
Bound.deserialize(this._referenceXYWH$.value),
|
||||
this.referenceModel instanceof FrameBlockModel
|
||||
? undefined
|
||||
: [20, 20, 20, 20]
|
||||
);
|
||||
};
|
||||
this.disposables.add(effect(refreshViewport));
|
||||
|
||||
@@ -304,28 +300,15 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
referenceXYWH$.value = referenceElement.xywh;
|
||||
|
||||
const { _disposable } = this;
|
||||
refreshViewport();
|
||||
_disposable.add(viewport.sizeUpdated.subscribe(refreshViewport));
|
||||
|
||||
if (referenceElement instanceof FrameBlockModel) {
|
||||
if (referenceElement instanceof GfxBlockElementModel) {
|
||||
_disposable.add(
|
||||
referenceElement.xywh$.subscribe(xywh => {
|
||||
referenceXYWH$.value = xywh;
|
||||
})
|
||||
);
|
||||
const subscription = this.std.view.viewUpdated.subscribe(
|
||||
({ id, type, method, view }) => {
|
||||
if (
|
||||
id === referenceElement.id &&
|
||||
type === 'block' &&
|
||||
method === 'add'
|
||||
) {
|
||||
assertType<FrameBlockComponent>(view);
|
||||
view.showBorder = false;
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
_disposable.add(subscription);
|
||||
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
|
||||
_disposable.add(
|
||||
surface.elementUpdated.subscribe(({ id, oldValues }) => {
|
||||
@@ -338,6 +321,21 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = this.std.view.viewUpdated.subscribe(
|
||||
({ id, type, method, view }) => {
|
||||
if (
|
||||
id === referenceElement.id &&
|
||||
type === 'block' &&
|
||||
method === 'add' &&
|
||||
view instanceof FrameBlockComponent
|
||||
) {
|
||||
view.showBorder = false;
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
_disposable.add(subscription);
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
@@ -371,15 +369,17 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
this._disposables.add(dispose);
|
||||
}
|
||||
|
||||
private _renderRefContent(referencedModel: GfxModel) {
|
||||
const [, , w, h] = deserializeXYWH(referencedModel.xywh);
|
||||
private _renderRefContent() {
|
||||
if (!this._referenceXYWH$.value) return nothing;
|
||||
const { w, h } = Bound.deserialize(this._referenceXYWH$.value);
|
||||
const aspectRatio = h !== 0 ? w / h : 1;
|
||||
const _previewSpec = this._previewSpec.concat(this._runtimePreviewExt);
|
||||
|
||||
return html`<div class="ref-content">
|
||||
<div
|
||||
class="ref-viewport"
|
||||
style=${styleMap({
|
||||
aspectRatio: `${w} / ${h}`,
|
||||
aspectRatio: `${aspectRatio}`,
|
||||
})}
|
||||
>
|
||||
${guard(this._previewDoc, () => {
|
||||
@@ -424,9 +424,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
|
||||
if (!this._shouldRender) return;
|
||||
|
||||
this._initReferencedModel();
|
||||
this._initHotkey();
|
||||
this._initViewport();
|
||||
this._initReferencedModel();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
@@ -445,7 +445,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
.referenceModel=${_referencedModel}
|
||||
.refFlavour=${model.props.refFlavour$.value}
|
||||
></surface-ref-placeholder>`
|
||||
: this._renderRefContent(_referencedModel);
|
||||
: this._renderRefContent();
|
||||
const edgelessTheme = this.std.get(ThemeProvider).edgeless$.value;
|
||||
|
||||
return html`
|
||||
@@ -471,7 +471,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
|
||||
const viewport = {
|
||||
xywh: this._referenceXYWH$.value,
|
||||
padding: [60, 20, 20, 20] as [number, number, number, number],
|
||||
padding: [20, 20, 20, 20] as [number, number, number, number],
|
||||
};
|
||||
|
||||
this.std.get(EditPropsStore).setStorage('viewport', viewport);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { type Color, ColorScheme } from '@blocksuite/affine-model';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { IBound } from '@blocksuite/global/gfx';
|
||||
import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type {
|
||||
GfxCompatibleInterface,
|
||||
GridManager,
|
||||
LayerManager,
|
||||
SurfaceBlockModel,
|
||||
@@ -43,6 +45,8 @@ export class CanvasRenderer {
|
||||
|
||||
private readonly _disposables = new DisposableGroup();
|
||||
|
||||
private readonly _turboEnabled: () => boolean;
|
||||
|
||||
private readonly _overlays = new Set<Overlay>();
|
||||
|
||||
private _refreshRafId: number | null = null;
|
||||
@@ -67,6 +71,8 @@ export class CanvasRenderer {
|
||||
removed: HTMLCanvasElement[];
|
||||
}>();
|
||||
|
||||
usePlaceholder = false;
|
||||
|
||||
viewport: Viewport;
|
||||
|
||||
get stackingCanvas() {
|
||||
@@ -83,6 +89,12 @@ export class CanvasRenderer {
|
||||
this.layerManager = options.layerManager;
|
||||
this.grid = options.gridManager;
|
||||
this.provider = options.provider ?? {};
|
||||
|
||||
this._turboEnabled = () => {
|
||||
const featureFlagService = options.std.get(FeatureFlagService);
|
||||
return featureFlagService.getFlag('enable_turbo_renderer');
|
||||
};
|
||||
|
||||
this._initViewport();
|
||||
|
||||
options.enableStackingCanvas = options.enableStackingCanvas ?? false;
|
||||
@@ -213,6 +225,19 @@ export class CanvasRenderer {
|
||||
}, this._container);
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
this.viewport.zooming$.subscribe(isZooming => {
|
||||
const shouldRenderPlaceholders = this._turboEnabled() && isZooming;
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.usePlaceholder = false;
|
||||
}
|
||||
|
||||
private _render() {
|
||||
@@ -279,23 +304,30 @@ export class CanvasRenderer {
|
||||
for (const element of elements) {
|
||||
const display = (element.display ?? true) && !element.hidden;
|
||||
if (display && intersects(getBoundWithRotation(element), bound)) {
|
||||
const renderFn = this.std.getOptional<ElementRenderer>(
|
||||
ElementRendererIdentifier(element.type)
|
||||
);
|
||||
if (
|
||||
this.usePlaceholder &&
|
||||
!(element as GfxCompatibleInterface).forceFullRender
|
||||
) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
|
||||
const drawX = element.x - bound.x;
|
||||
const drawY = element.y - bound.y;
|
||||
ctx.fillRect(drawX, drawY, element.w, element.h);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.save();
|
||||
const renderFn = this.std.getOptional<ElementRenderer>(
|
||||
ElementRendererIdentifier(element.type)
|
||||
);
|
||||
|
||||
if (!renderFn) {
|
||||
console.warn(`Cannot find renderer for ${element.type}`);
|
||||
continue;
|
||||
if (!renderFn) continue;
|
||||
|
||||
ctx.globalAlpha = element.opacity ?? 1;
|
||||
const dx = element.x - bound.x;
|
||||
const dy = element.y - bound.y;
|
||||
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.globalAlpha = element.opacity ?? 1;
|
||||
const dx = element.x - bound.x;
|
||||
const dy = element.y - bound.y;
|
||||
|
||||
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,14 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
|
||||
style=${styleMap({
|
||||
paddingLeft: `${virtualPadding}px`,
|
||||
paddingRight: `${virtualPadding}px`,
|
||||
marginLeft:
|
||||
this.model.props.textAlign$?.value === 'left'
|
||||
? undefined
|
||||
: 'auto',
|
||||
marginRight:
|
||||
this.model.props.textAlign$?.value === 'right'
|
||||
? undefined
|
||||
: 'auto',
|
||||
width: 'max-content',
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"./linked-doc-title": "./src/linked-doc-title/index.ts",
|
||||
"./view-dropdown-menu": "./src/view-dropdown-menu/index.ts",
|
||||
"./card-style-dropdown-menu": "./src/card-style-dropdown-menu/index.ts",
|
||||
"./citation": "./src/citation/index.ts",
|
||||
"./highlight-dropdown-menu": "./src/highlight-dropdown-menu/index.ts",
|
||||
"./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts",
|
||||
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts",
|
||||
|
||||
167
blocksuite/affine/components/src/citation/citation.ts
Normal file
167
blocksuite/affine/components/src/citation/citation.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
export class CitationCard extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
.citation-container {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
padding: 4px 8px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.citation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.citation-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
border-radius: 4px;
|
||||
|
||||
svg,
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
.citation-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
line-height: 22px;
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
font-size: var(--affine-font-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.citation-identifier {
|
||||
display: flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 36px;
|
||||
background: ${unsafeCSSVarV2('block/footnote/numberBg')};
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 220% */
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.citation-container:hover .citation-identifier,
|
||||
.citation-identifier.active {
|
||||
background: ${unsafeCSSVarV2('button/primary')};
|
||||
color: ${unsafeCSSVarV2('button/pureWhiteText')};
|
||||
}
|
||||
|
||||
.citation-content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
font-feature-settings:
|
||||
'liga' off,
|
||||
'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 166.667% */
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _IconTemplate = (icon: TemplateResult | string) => {
|
||||
if (typeof icon === 'string') {
|
||||
return html`<img src="${icon}" alt="favicon" />`;
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
override render() {
|
||||
const citationIdentifierClasses = classMap({
|
||||
'citation-identifier': true,
|
||||
active: this.active,
|
||||
});
|
||||
return html`
|
||||
<div
|
||||
class="citation-container"
|
||||
@click=${this.onClickCallback}
|
||||
@dblclick=${this.onDoubleClickCallback}
|
||||
>
|
||||
<div class="citation-header">
|
||||
${this.icon
|
||||
? html`<div class="citation-icon">
|
||||
${this._IconTemplate(this.icon)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="citation-title">${this.citationTitle}</div>
|
||||
<div class=${citationIdentifierClasses}>
|
||||
${this.citationIdentifier}
|
||||
</div>
|
||||
</div>
|
||||
${this.citationContent
|
||||
? html`<div class="citation-content">${this.citationContent}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor icon: TemplateResult | string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor citationTitle: string = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor citationContent: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor citationIdentifier: string = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onClickCallback: ((e: MouseEvent) => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onDoubleClickCallback: ((e: MouseEvent) => void) | undefined =
|
||||
undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor active: boolean = false;
|
||||
}
|
||||
7
blocksuite/affine/components/src/citation/index.ts
Normal file
7
blocksuite/affine/components/src/citation/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { CitationCard } from './citation';
|
||||
|
||||
export * from './citation';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-citation-card', CitationCard);
|
||||
}
|
||||
@@ -340,11 +340,6 @@ export const FontFamilyIcon = icons.FontIcon({
|
||||
height: '20',
|
||||
});
|
||||
|
||||
export const AttachmentIcon16 = icons.AttachmentIcon({
|
||||
width: '16',
|
||||
height: '16',
|
||||
});
|
||||
|
||||
export const TextBackgroundDuotoneIcon = html` <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ToggleDownIcon, ToggleRightIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
@@ -43,6 +44,12 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
|
||||
.with-drag-handle .affine-block-children-container .toggle-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
svg {
|
||||
color: ${unsafeCSSVarV2('icon/primary', '#77757D')};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
@@ -55,7 +62,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
|
||||
${ToggleDownIcon({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
style: 'color: #77757D',
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
@@ -70,7 +76,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
|
||||
${ToggleRightIcon({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
style: 'color: #77757D',
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection';
|
||||
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
|
||||
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
|
||||
import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu';
|
||||
import { effects as componentCitationEffects } from '@blocksuite/affine-components/citation';
|
||||
import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
|
||||
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
||||
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
||||
@@ -46,6 +47,7 @@ export function effects() {
|
||||
componentLinkPreviewEffects();
|
||||
componentLinkedDocTitleEffects();
|
||||
componentCardStyleDropdownMenuEffects();
|
||||
componentCitationEffects();
|
||||
componentHighlightDropdownMenuEffects();
|
||||
componentViewDropdownMenuEffects();
|
||||
componentTooltipContentWithShortcutEffects();
|
||||
|
||||
@@ -15,7 +15,6 @@ import { HighlightSelectionExtension } from '@blocksuite/affine-shared/selection
|
||||
import {
|
||||
BlockMetaService,
|
||||
FeatureFlagService,
|
||||
FileSizeLimitService,
|
||||
LinkPreviewerService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
@@ -51,7 +50,6 @@ export class FoundationStoreExtension extends StoreExtensionProvider {
|
||||
BlockMetaService,
|
||||
// TODO(@mirone): maybe merge these services into a file setting service
|
||||
LinkPreviewerService,
|
||||
FileSizeLimitService,
|
||||
ImageProxyService,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@ import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import {
|
||||
AttachmentAdapter,
|
||||
ClipboardAdapter,
|
||||
HtmlAdapter,
|
||||
ImageAdapter,
|
||||
MixTextAdapter,
|
||||
NotionTextAdapter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
AutoClearSelectionService,
|
||||
DefaultOpenDocExtension,
|
||||
@@ -11,15 +19,73 @@ import {
|
||||
DocModeService,
|
||||
EditPropsStore,
|
||||
EmbedOptionService,
|
||||
FileSizeLimitService,
|
||||
FontLoaderService,
|
||||
PageViewportServiceExtension,
|
||||
ThemeService,
|
||||
ToolbarRegistryExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { ClipboardAdapterConfigExtension } from '@blocksuite/std';
|
||||
import { InteractivityManager, ToolController } from '@blocksuite/std/gfx';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { effects } from './effects';
|
||||
|
||||
const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: ClipboardAdapter.MIME,
|
||||
adapter: ClipboardAdapter,
|
||||
priority: 100,
|
||||
});
|
||||
|
||||
const NotionClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: 'text/_notion-text-production',
|
||||
adapter: NotionTextAdapter,
|
||||
priority: 95,
|
||||
});
|
||||
|
||||
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: 'text/html',
|
||||
adapter: HtmlAdapter,
|
||||
priority: 90,
|
||||
});
|
||||
|
||||
const imageClipboardConfigs = [
|
||||
'image/apng',
|
||||
'image/avif',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
].map(mimeType => {
|
||||
return ClipboardAdapterConfigExtension({
|
||||
mimeType,
|
||||
adapter: ImageAdapter,
|
||||
priority: 80,
|
||||
});
|
||||
});
|
||||
|
||||
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: 'text/plain',
|
||||
adapter: MixTextAdapter,
|
||||
priority: 70,
|
||||
});
|
||||
|
||||
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({
|
||||
mimeType: '*/*',
|
||||
adapter: AttachmentAdapter,
|
||||
priority: 60,
|
||||
});
|
||||
|
||||
export const clipboardConfigs: ExtensionType[] = [
|
||||
SnapshotClipboardConfig,
|
||||
NotionClipboardConfig,
|
||||
HtmlClipboardConfig,
|
||||
...imageClipboardConfigs,
|
||||
PlainTextClipboardConfig,
|
||||
AttachmentClipboardConfig,
|
||||
];
|
||||
|
||||
export class FoundationViewExtension extends ViewExtensionProvider {
|
||||
override name = 'foundation';
|
||||
|
||||
@@ -44,7 +110,9 @@ export class FoundationViewExtension extends ViewExtensionProvider {
|
||||
FileDropExtension,
|
||||
ToolbarRegistryExtension,
|
||||
AutoClearSelectionService,
|
||||
FileSizeLimitService,
|
||||
]);
|
||||
context.register(clipboardConfigs);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
context.register([InteractivityManager, ToolController]);
|
||||
}
|
||||
|
||||
@@ -249,13 +249,20 @@ function renderLabel(
|
||||
const [, , w, h] = labelXYWH!;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
|
||||
ctx.setTransform(matrix);
|
||||
|
||||
if (renderer.usePlaceholder) {
|
||||
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
return; // Skip actual label rendering
|
||||
}
|
||||
|
||||
const deltas = wrapTextDeltas(text!, font, w);
|
||||
const lines = deltaInsertsToChunks(deltas);
|
||||
const lineHeight = getLineHeight(fontFamily, fontSize, fontWeight);
|
||||
const textHeight = (lines.length - 1) * lineHeight * 0.5;
|
||||
|
||||
ctx.setTransform(matrix);
|
||||
|
||||
ctx.font = font;
|
||||
ctx.textAlign = textAlign;
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
50
blocksuite/affine/gfx/link/package.json
Normal file
50
blocksuite/affine/gfx/link/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-gfx-link",
|
||||
"description": "Gfx link for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-bookmark": "workspace:*",
|
||||
"@blocksuite/affine-block-embed": "workspace:*",
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-gfx-pointer": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.21.0"
|
||||
}
|
||||
5
blocksuite/affine/gfx/link/src/effects.ts
Normal file
5
blocksuite/affine/gfx/link/src/effects.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EdgelessLinkToolButton } from './toolbar/link-tool-button';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
|
||||
}
|
||||
1
blocksuite/affine/gfx/link/src/index.ts
Normal file
1
blocksuite/affine/gfx/link/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { QuickToolExtension } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { buildLinkDenseMenu } from './link/link-dense-menu.js';
|
||||
import { buildLinkDenseMenu } from './toolbar/link-dense-menu';
|
||||
|
||||
const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
|
||||
export const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
|
||||
return {
|
||||
content: html`<edgeless-link-tool-button
|
||||
.edgeless=${block}
|
||||
@@ -11,5 +11,3 @@ const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
|
||||
menu: buildLinkDenseMenu(block, gfx),
|
||||
};
|
||||
});
|
||||
|
||||
export const quickTools = [linkQuickTool];
|
||||
@@ -2,10 +2,13 @@ import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmar
|
||||
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
|
||||
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
|
||||
import { LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import type * as PointerEffect from '@blocksuite/affine-gfx-pointer';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { QuickToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
declare type _GLOBAL_ = typeof PointerEffect;
|
||||
|
||||
export class EdgelessLinkToolButton extends QuickToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
.link-icon,
|
||||
23
blocksuite/affine/gfx/link/src/view.ts
Normal file
23
blocksuite/affine/gfx/link/src/view.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { linkQuickTool } from './link-tool';
|
||||
|
||||
export class LinkViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-link-gfx';
|
||||
|
||||
override effect() {
|
||||
super.effect();
|
||||
effects();
|
||||
}
|
||||
|
||||
override setup(context: ViewExtensionContext) {
|
||||
super.setup(context);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
context.register(linkQuickTool);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
blocksuite/affine/gfx/link/tsconfig.json
Normal file
24
blocksuite/affine/gfx/link/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../blocks/bookmark" },
|
||||
{ "path": "../../blocks/embed" },
|
||||
{ "path": "../../blocks/surface" },
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../pointer" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../widgets/edgeless-toolbar" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
]
|
||||
}
|
||||
@@ -47,7 +47,8 @@ export function getViewportLayoutTree(
|
||||
// Recursive function to build the tree structure
|
||||
const buildLayoutTreeNode = (
|
||||
model: BlockModel,
|
||||
ancestorViewportState?: string | null
|
||||
ancestorViewportState?: string | null,
|
||||
root = false
|
||||
): BlockLayoutTreeNode | null => {
|
||||
const baseLayout: BlockLayout = {
|
||||
blockId: model.id,
|
||||
@@ -92,6 +93,29 @@ export function getViewportLayoutTree(
|
||||
layoutMinY = Math.min(layoutMinY, calculatedRect.y);
|
||||
layoutMaxX = Math.max(layoutMaxX, calculatedRect.x + calculatedRect.w);
|
||||
layoutMaxY = Math.max(layoutMaxY, calculatedRect.y + calculatedRect.h);
|
||||
} else if (component && !root) {
|
||||
const clientRect = component.getBoundingClientRect();
|
||||
const [modelX, modelY] = viewport.toModelCoordFromClientCoord([
|
||||
clientRect.x,
|
||||
clientRect.y,
|
||||
]);
|
||||
|
||||
const rect = {
|
||||
x: modelX,
|
||||
y: modelY,
|
||||
w: clientRect.width / zoom / viewport.viewScale,
|
||||
h: clientRect.height / zoom / viewport.viewScale,
|
||||
};
|
||||
|
||||
layout = {
|
||||
...baseLayout,
|
||||
rect,
|
||||
};
|
||||
|
||||
layoutMinX = Math.min(layoutMinX, rect.x);
|
||||
layoutMinY = Math.min(layoutMinY, rect.y);
|
||||
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
|
||||
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
|
||||
} else {
|
||||
layoutMinX = Math.min(layoutMinX, baseLayout.rect.x);
|
||||
layoutMinY = Math.min(layoutMinY, baseLayout.rect.y);
|
||||
@@ -116,7 +140,7 @@ export function getViewportLayoutTree(
|
||||
};
|
||||
|
||||
const roots: BlockLayoutTreeNode[] = [];
|
||||
const rootNode = buildLayoutTreeNode(rootModel);
|
||||
const rootNode = buildLayoutTreeNode(rootModel, null, true);
|
||||
if (rootNode) {
|
||||
roots.push(rootNode);
|
||||
}
|
||||
@@ -155,7 +179,6 @@ export function debugLog(message: string, state: RenderingState) {
|
||||
}
|
||||
|
||||
export function paintPlaceholder(
|
||||
host: EditorHost,
|
||||
canvas: HTMLCanvasElement,
|
||||
layout: ViewportLayoutTree | null,
|
||||
viewport: Viewport
|
||||
@@ -175,27 +198,19 @@ export function paintPlaceholder(
|
||||
'rgba(160, 160, 160, 0.7)',
|
||||
];
|
||||
|
||||
const layoutHandlers = host.std.provider.getAll(
|
||||
BlockLayoutHandlersIdentifier
|
||||
);
|
||||
const handlersArray = Array.from(layoutHandlers.values());
|
||||
|
||||
const paintNode = (node: BlockLayoutTreeNode, depth: number = 0) => {
|
||||
const { layout: nodeLayout, type } = node;
|
||||
const handler = handlersArray.find(h => h.blockType === type);
|
||||
if (handler) {
|
||||
ctx.fillStyle = colors[depth % colors.length];
|
||||
const rect = nodeLayout.rect;
|
||||
const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
|
||||
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
|
||||
const width = rect.w * viewport.zoom * dpr;
|
||||
const height = rect.h * viewport.zoom * dpr;
|
||||
const { layout: nodeLayout } = node;
|
||||
ctx.fillStyle = colors[depth % colors.length];
|
||||
const rect = nodeLayout.rect;
|
||||
const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
|
||||
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
|
||||
const width = rect.w * viewport.zoom * dpr;
|
||||
const height = rect.h * viewport.zoom * dpr;
|
||||
|
||||
ctx.fillRect(x, y, width, height);
|
||||
if (width > 10 && height > 5) {
|
||||
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
}
|
||||
ctx.fillRect(x, y, width, height);
|
||||
if (width > 10 && height > 5) {
|
||||
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
}
|
||||
|
||||
if (node.children.length > 0) {
|
||||
|
||||
@@ -422,12 +422,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
}
|
||||
|
||||
private paintPlaceholder() {
|
||||
paintPlaceholder(
|
||||
this.std.host,
|
||||
this.canvas,
|
||||
this.layoutCache,
|
||||
this.viewport
|
||||
);
|
||||
paintPlaceholder(this.canvas, this.layoutCache, this.viewport);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
text-overflow: ellipsis;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
transition: background 0.3s ease-in-out;
|
||||
transform: translateY(-0.2em);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,10 +138,6 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
};
|
||||
|
||||
private readonly _handleDocReference = (docId: string) => {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.std
|
||||
.getOptional(PeekViewProvider)
|
||||
?.peek({
|
||||
|
||||
@@ -55,6 +55,8 @@ export type AttachmentBlockProps = {
|
||||
embed: boolean | BackwardCompatibleUndefined;
|
||||
|
||||
style?: (typeof AttachmentBlockStyles)[number];
|
||||
|
||||
footnoteIdentifier: string | null;
|
||||
} & Omit<GfxCommonBlockProps, 'scale'> &
|
||||
BlockMeta;
|
||||
|
||||
@@ -74,6 +76,7 @@ export const defaultAttachmentProps: AttachmentBlockProps = {
|
||||
'meta:updatedAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
'meta:updatedBy': undefined,
|
||||
footnoteIdentifier: null,
|
||||
};
|
||||
|
||||
export const AttachmentBlockSchema = defineBlockSchema({
|
||||
|
||||
@@ -20,12 +20,14 @@ export const BookmarkStyles: EmbedCardStyle[] = [
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'citation',
|
||||
] as const;
|
||||
|
||||
export type BookmarkBlockProps = {
|
||||
style: (typeof BookmarkStyles)[number];
|
||||
url: string;
|
||||
caption: string | null;
|
||||
footnoteIdentifier: string | null;
|
||||
} & LinkPreviewData &
|
||||
Omit<GfxCommonBlockProps, 'scale'> &
|
||||
BlockMeta;
|
||||
@@ -48,6 +50,8 @@ const defaultBookmarkProps: BookmarkBlockProps = {
|
||||
'meta:updatedAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
'meta:updatedBy': undefined,
|
||||
|
||||
footnoteIdentifier: null,
|
||||
};
|
||||
|
||||
export const BookmarkBlockSchema = defineBlockSchema({
|
||||
|
||||
@@ -10,11 +10,13 @@ export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
|
||||
'list',
|
||||
'cube',
|
||||
'horizontalThin',
|
||||
'citation',
|
||||
];
|
||||
|
||||
export type EmbedLinkedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
caption: string | null;
|
||||
footnoteIdentifier: string | null;
|
||||
} & ReferenceInfo;
|
||||
|
||||
export class EmbedLinkedDocModel extends defineEmbedModel<EmbedLinkedDocBlockProps>(
|
||||
|
||||
@@ -14,6 +14,8 @@ const defaultEmbedLinkedDocBlockProps: EmbedLinkedDocBlockProps = {
|
||||
// title & description aliases
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
|
||||
footnoteIdentifier: null,
|
||||
};
|
||||
|
||||
export const EmbedLinkedDocBlockSchema = createEmbedBlockSchema({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types.js';
|
||||
import { ImageBlockTransformer } from './image-transformer.js';
|
||||
|
||||
@@ -19,6 +20,7 @@ export type ImageBlockProps = {
|
||||
height?: number;
|
||||
rotate: number;
|
||||
size?: number;
|
||||
textAlign?: TextAlign;
|
||||
} & Omit<GfxCommonBlockProps, 'scale'> &
|
||||
BlockMeta;
|
||||
|
||||
@@ -32,6 +34,7 @@ const defaultImageProps: ImageBlockProps = {
|
||||
lockedBySelf: false,
|
||||
rotate: 0,
|
||||
size: -1,
|
||||
textAlign: undefined,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
'meta:updatedAt': undefined,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
// `toggle` type has been deprecated, do not use it
|
||||
@@ -13,6 +14,7 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
|
||||
export type ListProps = {
|
||||
type: ListType;
|
||||
text: Text;
|
||||
textAlign?: TextAlign;
|
||||
checked: boolean;
|
||||
collapsed: boolean;
|
||||
order: number | null;
|
||||
@@ -24,6 +26,7 @@ export const ListBlockSchema = defineBlockSchema({
|
||||
({
|
||||
type: 'bulleted',
|
||||
text: internal.Text(),
|
||||
textAlign: undefined,
|
||||
checked: false,
|
||||
collapsed: false,
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type Text,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
export type ParagraphType =
|
||||
@@ -19,6 +20,7 @@ export type ParagraphType =
|
||||
|
||||
export type ParagraphProps = {
|
||||
type: ParagraphType;
|
||||
textAlign?: TextAlign;
|
||||
text: Text;
|
||||
collapsed: boolean;
|
||||
} & BlockMeta;
|
||||
@@ -28,6 +30,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
|
||||
props: (internal): ParagraphProps => ({
|
||||
type: 'text',
|
||||
text: internal.Text(),
|
||||
textAlign: undefined,
|
||||
collapsed: false,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
export type TableCell = {
|
||||
@@ -29,6 +30,7 @@ export interface TableBlockProps extends BlockMeta {
|
||||
columns: Record<string, TableColumn>;
|
||||
// key = `${rowId}:${columnId}`
|
||||
cells: Record<string, TableCell>;
|
||||
textAlign?: TextAlign;
|
||||
}
|
||||
|
||||
export interface TableCellSerialized {
|
||||
@@ -51,6 +53,7 @@ export const TableBlockSchema = defineBlockSchema({
|
||||
rows: {},
|
||||
columns: {},
|
||||
cells: {},
|
||||
textAlign: undefined,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
'meta:updatedAt': undefined,
|
||||
|
||||
@@ -106,6 +106,11 @@ export type ConnectorElementProps = BaseElementProps & {
|
||||
export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorElementProps> {
|
||||
updatingPath = false;
|
||||
|
||||
/**
|
||||
* Connectors should always render, even during zoom.
|
||||
*/
|
||||
forceFullRender = true;
|
||||
|
||||
override get connectable() {
|
||||
return false as const;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ export type EmbedCardStyle =
|
||||
| 'figma'
|
||||
| 'html'
|
||||
| 'syncedDoc'
|
||||
| 'pdf';
|
||||
| 'pdf'
|
||||
| 'citation';
|
||||
|
||||
export type LinkPreviewData = {
|
||||
description: string | null;
|
||||
|
||||
35
blocksuite/affine/rich-text/src/align.ts
Normal file
35
blocksuite/affine/rich-text/src/align.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TextAlign } from '@blocksuite/affine-model';
|
||||
import {
|
||||
TextAlignCenterIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export interface TextAlignConfig {
|
||||
textAlign: TextAlign;
|
||||
name: string;
|
||||
hotkey: string[] | null;
|
||||
icon: TemplateResult<1>;
|
||||
}
|
||||
|
||||
export const textAlignConfigs: TextAlignConfig[] = [
|
||||
{
|
||||
textAlign: TextAlign.Left,
|
||||
name: 'Align left',
|
||||
hotkey: [`Mod-Shift-L`],
|
||||
icon: TextAlignLeftIcon(),
|
||||
},
|
||||
{
|
||||
textAlign: TextAlign.Center,
|
||||
name: 'Align center',
|
||||
hotkey: [`Mod-Shift-E`],
|
||||
icon: TextAlignCenterIcon(),
|
||||
},
|
||||
{
|
||||
textAlign: TextAlign.Right,
|
||||
name: 'Align right',
|
||||
hotkey: [`Mod-Shift-R`],
|
||||
icon: TextAlignRightIcon(),
|
||||
},
|
||||
];
|
||||
@@ -1,3 +1,4 @@
|
||||
export { type TextAlignConfig, textAlignConfigs } from './align';
|
||||
export { type TextConversionConfig, textConversionConfigs } from './conversion';
|
||||
export {
|
||||
asyncGetRichText,
|
||||
|
||||
@@ -22,10 +22,12 @@ export {
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
getFootnoteDefinitionText,
|
||||
IN_PARAGRAPH_NODE_CONTEXT_KEY,
|
||||
InlineDeltaToMarkdownAdapterExtension,
|
||||
type InlineDeltaToMarkdownAdapterMatcher,
|
||||
InlineDeltaToMarkdownAdapterMatcherIdentifier,
|
||||
isFootnoteDefinitionNode,
|
||||
isMarkdownAST,
|
||||
type Markdown,
|
||||
MarkdownAdapter,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Root, RootContentMap } from 'mdast';
|
||||
import type { FootnoteDefinition, Root, RootContentMap } from 'mdast';
|
||||
|
||||
export type Markdown = string;
|
||||
|
||||
@@ -16,5 +16,17 @@ export const isMarkdownAST = (node: unknown): node is MarkdownAST =>
|
||||
'type' in (node as object) &&
|
||||
(node as MarkdownAST).type !== undefined;
|
||||
|
||||
export const isFootnoteDefinitionNode = (
|
||||
node: MarkdownAST
|
||||
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
|
||||
|
||||
export const getFootnoteDefinitionText = (node: FootnoteDefinition) => {
|
||||
const childNode = node.children[0];
|
||||
if (childNode.type !== 'paragraph') return '';
|
||||
const paragraph = childNode.children[0];
|
||||
if (paragraph.type !== 'text') return '';
|
||||
return paragraph.value;
|
||||
};
|
||||
|
||||
export const FOOTNOTE_DEFINITION_PREFIX = 'footnoteDefinition:';
|
||||
export const IN_PARAGRAPH_NODE_CONTEXT_KEY = 'mdast:paragraph';
|
||||
|
||||
@@ -31,6 +31,7 @@ export const EMBED_CARD_WIDTH: Record<EmbedCardStyle, number> = {
|
||||
html: 752,
|
||||
syncedDoc: 800,
|
||||
pdf: 537 + 24 + 2,
|
||||
citation: 752,
|
||||
};
|
||||
|
||||
export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
|
||||
@@ -45,6 +46,7 @@ export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
|
||||
html: 544,
|
||||
syncedDoc: 455,
|
||||
pdf: 759 + 46 + 24 + 2,
|
||||
citation: 52,
|
||||
};
|
||||
|
||||
export const EMBED_BLOCK_FLAVOUR_LIST = [
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface BlockSuiteFlags {
|
||||
enable_callout: boolean;
|
||||
enable_edgeless_scribbled_style: boolean;
|
||||
enable_embed_doc_with_alias: boolean;
|
||||
enable_turbo_renderer: boolean;
|
||||
enable_citation: boolean;
|
||||
}
|
||||
|
||||
export class FeatureFlagService extends StoreExtension {
|
||||
@@ -42,6 +44,8 @@ export class FeatureFlagService extends StoreExtension {
|
||||
enable_callout: false,
|
||||
enable_edgeless_scribbled_style: false,
|
||||
enable_embed_doc_with_alias: false,
|
||||
enable_turbo_renderer: false,
|
||||
enable_citation: false,
|
||||
});
|
||||
|
||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { StoreExtension } from '@blocksuite/store';
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import { Extension } from '@blocksuite/store';
|
||||
|
||||
// bytes.parse('2GB')
|
||||
const maxFileSize = 2 * 1024 * 1024 * 1024;
|
||||
|
||||
export class FileSizeLimitService extends StoreExtension {
|
||||
static override key = 'file-size-limit';
|
||||
|
||||
maxFileSize = maxFileSize;
|
||||
export interface IFileSizeLimitService {
|
||||
maxFileSize: number;
|
||||
onOverFileSize?: () => void;
|
||||
}
|
||||
|
||||
export const FileSizeLimitProvider = createIdentifier<IFileSizeLimitService>(
|
||||
'FileSizeLimitService'
|
||||
);
|
||||
|
||||
export class FileSizeLimitService
|
||||
extends Extension
|
||||
implements IFileSizeLimitService
|
||||
{
|
||||
// 2GB
|
||||
maxFileSize = 2 * 1024 * 1024 * 1024;
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(FileSizeLimitProvider, FileSizeLimitService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
EDGELESS_TOOLBAR_WIDGET,
|
||||
EdgelessToolbarWidget,
|
||||
} from './edgeless-toolbar';
|
||||
import { EdgelessSlideMenu } from './menu/slide-menu';
|
||||
import { ToolbarArrowUpIcon } from './menu/toolbar-arrow-up-icon';
|
||||
import { EdgelessFontFamilyPanel } from './panel/font-family-panel';
|
||||
import { EdgelessFontWeightAndStylePanel } from './panel/font-weight-and-style-panel';
|
||||
|
||||
@@ -16,6 +18,8 @@ export function effects() {
|
||||
EdgelessFontWeightAndStylePanel
|
||||
);
|
||||
customElements.define('edgeless-font-family-panel', EdgelessFontFamilyPanel);
|
||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -25,5 +29,7 @@ declare global {
|
||||
'edgeless-toolbar-widget': EdgelessToolbarWidget;
|
||||
'edgeless-font-weight-and-style-panel': EdgelessFontWeightAndStylePanel;
|
||||
'edgeless-font-family-panel': EdgelessFontFamilyPanel;
|
||||
'edgeless-slide-menu': EdgelessSlideMenu;
|
||||
'toolbar-arrow-up-icon': ToolbarArrowUpIcon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
type EdgelessToolbarSlots,
|
||||
edgelessToolbarSlotsContext,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { consume } from '@lit/context';
|
||||
@@ -9,6 +5,11 @@ import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import {
|
||||
type EdgelessToolbarSlots,
|
||||
edgelessToolbarSlotsContext,
|
||||
} from '../index';
|
||||
|
||||
export class EdgelessSlideMenu extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user