mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(editor): add experimental feature citation (#11984)
Closes: [BS-3122](https://linear.app/affine-design/issue/BS-3122/footnote-definition-adapter-适配) Closes: [BS-3123](https://linear.app/affine-design/issue/BS-3123/几个-block-card-view-适配-footnote-态) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new citation card component and web element for displaying citations. - Added support for citation-style rendering in attachment, bookmark, and linked document blocks. - Enabled citation parsing from footnote definitions in markdown for attachments, bookmarks, and linked docs. - Added a feature flag to enable or disable citation features. - Provided new toolbar logic to disable downloads for citation-style attachments. - **Improvements** - Updated block models and properties to support citation identifiers. - Added localization and settings for the citation experimental feature. - Enhanced markdown adapters to recognize and process citation footnotes. - Included new constants and styles for citation card display. - **Bug Fixes** - Ensured readonly state is respected in block interactions and rendering for citation blocks. - **Documentation** - Added exports and effects for new citation components and features. - **Tests** - Updated snapshots to include citation-related properties in block data. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -226,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();
|
||||
|
||||
@@ -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);
|
||||
@@ -53,6 +53,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
return this.std.store.get(FileSizeLimitService).maxFileSize;
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return !!this.model.props.footnoteIdentifier;
|
||||
}
|
||||
|
||||
convertTo = () => {
|
||||
return this.std
|
||||
.get(AttachmentEmbedProvider)
|
||||
@@ -147,7 +151,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
})
|
||||
);
|
||||
|
||||
if (!this.model.props.style) {
|
||||
if (!this.model.props.style && !this.doc.readonly) {
|
||||
this.doc.withoutTransact(() => {
|
||||
this.doc.updateBlock(this.model, {
|
||||
style: AttachmentBlockStyles[1],
|
||||
@@ -322,6 +326,18 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _renderCitation = () => {
|
||||
const { name, footnoteIdentifier } = this.model.props;
|
||||
const fileType = name.split('.').pop() ?? '';
|
||||
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
|
||||
@@ -332,12 +348,17 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
style=${this.containerStyleMap}
|
||||
>
|
||||
${when(
|
||||
this.embedView,
|
||||
this.isCitation,
|
||||
() => this._renderCitation(),
|
||||
() =>
|
||||
html`<div class="affine-attachment-embed-container">
|
||||
${this.embedView}
|
||||
</div>`,
|
||||
this.renderCard
|
||||
when(
|
||||
this.embedView,
|
||||
() =>
|
||||
html`<div class="affine-attachment-embed-container">
|
||||
${this.embedView}
|
||||
</div>`,
|
||||
this.renderCard
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -139,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 = {
|
||||
@@ -331,7 +337,6 @@ const builtinSurfaceToolbarConfig = {
|
||||
id: 'e.caption',
|
||||
},
|
||||
],
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -17,7 +17,8 @@ export type EmbedCardStyle =
|
||||
| 'figma'
|
||||
| 'html'
|
||||
| 'syncedDoc'
|
||||
| 'pdf';
|
||||
| 'pdf'
|
||||
| 'citation';
|
||||
|
||||
export type LinkPreviewData = {
|
||||
description: string | null;
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface BlockSuiteFlags {
|
||||
enable_edgeless_scribbled_style: boolean;
|
||||
enable_embed_doc_with_alias: boolean;
|
||||
enable_turbo_renderer: boolean;
|
||||
enable_citation: boolean;
|
||||
}
|
||||
|
||||
export class FeatureFlagService extends StoreExtension {
|
||||
@@ -44,6 +45,7 @@ export class FeatureFlagService extends StoreExtension {
|
||||
enable_edgeless_scribbled_style: false,
|
||||
enable_embed_doc_with_alias: false,
|
||||
enable_turbo_renderer: false,
|
||||
enable_citation: false,
|
||||
});
|
||||
|
||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||
|
||||
Reference in New Issue
Block a user