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/block-zero-width": "./src/components/block-zero-width.ts",
|
||||||
"./components/caption": "./src/components/caption.ts",
|
"./components/caption": "./src/components/caption.ts",
|
||||||
"./components/card-style-dropdown-menu": "./src/components/card-style-dropdown-menu.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/color-picker": "./src/components/color-picker.ts",
|
||||||
"./components/context-menu": "./src/components/context-menu.ts",
|
"./components/context-menu": "./src/components/context-menu.ts",
|
||||||
"./components/date-picker": "./src/components/date-picker.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 { BookmarkBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-bookmark';
|
||||||
import { CodeBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-code';
|
import { CodeBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-code';
|
||||||
import { DatabaseBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-database';
|
import { DatabaseBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-database';
|
||||||
@@ -38,4 +39,5 @@ export const defaultBlockMarkdownAdapterMatchers = [
|
|||||||
DividerBlockMarkdownAdapterExtension,
|
DividerBlockMarkdownAdapterExtension,
|
||||||
ImageBlockMarkdownAdapterExtension,
|
ImageBlockMarkdownAdapterExtension,
|
||||||
LatexBlockMarkdownAdapterExtension,
|
LatexBlockMarkdownAdapterExtension,
|
||||||
|
AttachmentBlockMarkdownAdapterExtension,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection';
|
|||||||
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
|
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
|
||||||
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
|
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
|
||||||
import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu';
|
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 componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
|
||||||
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
||||||
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
||||||
@@ -154,6 +155,7 @@ export function effects() {
|
|||||||
componentEmbedCardModalEffects();
|
componentEmbedCardModalEffects();
|
||||||
componentLinkPreviewEffects();
|
componentLinkPreviewEffects();
|
||||||
componentLinkedDocTitleEffects();
|
componentLinkedDocTitleEffects();
|
||||||
|
componentCitationEffects();
|
||||||
componentCardStyleDropdownMenuEffects();
|
componentCardStyleDropdownMenuEffects();
|
||||||
componentHighlightDropdownMenuEffects();
|
componentHighlightDropdownMenuEffects();
|
||||||
componentViewDropdownMenuEffects();
|
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;
|
return this.std.store.get(FileSizeLimitService).maxFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isCitation() {
|
||||||
|
return !!this.model.props.footnoteIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
convertTo = () => {
|
convertTo = () => {
|
||||||
return this.std
|
return this.std
|
||||||
.get(AttachmentEmbedProvider)
|
.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.withoutTransact(() => {
|
||||||
this.doc.updateBlock(this.model, {
|
this.doc.updateBlock(this.model, {
|
||||||
style: AttachmentBlockStyles[1],
|
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() {
|
override renderBlock() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@@ -332,12 +348,17 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
|||||||
style=${this.containerStyleMap}
|
style=${this.containerStyleMap}
|
||||||
>
|
>
|
||||||
${when(
|
${when(
|
||||||
this.embedView,
|
this.isCitation,
|
||||||
|
() => this._renderCitation(),
|
||||||
() =>
|
() =>
|
||||||
html`<div class="affine-attachment-embed-container">
|
when(
|
||||||
${this.embedView}
|
this.embedView,
|
||||||
</div>`,
|
() =>
|
||||||
this.renderCard
|
html`<div class="affine-attachment-embed-container">
|
||||||
|
${this.embedView}
|
||||||
|
</div>`,
|
||||||
|
this.renderCard
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
|||||||
import type { ExtensionType } from '@blocksuite/store';
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
import { literal } from 'lit/static-html.js';
|
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 { AttachmentDropOption } from './attachment-service.js';
|
||||||
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
@@ -25,7 +25,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
|
|||||||
AttachmentDropOption,
|
AttachmentDropOption,
|
||||||
AttachmentEmbedConfigExtension(),
|
AttachmentEmbedConfigExtension(),
|
||||||
AttachmentEmbedService,
|
AttachmentEmbedService,
|
||||||
AttachmentBlockNotionHtmlAdapterExtension,
|
AttachmentBlockAdapterExtensions,
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -139,6 +139,12 @@ const downloadAction = {
|
|||||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||||
block?.download();
|
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;
|
} as const satisfies ToolbarAction;
|
||||||
|
|
||||||
const captionAction = {
|
const captionAction = {
|
||||||
@@ -331,7 +337,6 @@ const builtinSurfaceToolbarConfig = {
|
|||||||
id: 'e.caption',
|
id: 'e.caption',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
||||||
} as const satisfies ToolbarModuleConfig;
|
} as const satisfies ToolbarModuleConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './adapters/notion-html';
|
export * from './adapters';
|
||||||
export * from './attachment-block';
|
export * from './attachment-block';
|
||||||
export * from './attachment-service';
|
export * from './attachment-service';
|
||||||
export * from './attachment-spec';
|
export * from './attachment-spec';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from '@blocksuite/affine-ext-loader';
|
} from '@blocksuite/affine-ext-loader';
|
||||||
import { AttachmentBlockSchemaExtension } from '@blocksuite/affine-model';
|
import { AttachmentBlockSchemaExtension } from '@blocksuite/affine-model';
|
||||||
|
|
||||||
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html';
|
import { AttachmentBlockAdapterExtensions } from './adapters/extension';
|
||||||
|
|
||||||
export class AttachmentStoreExtension extends StoreExtensionProvider {
|
export class AttachmentStoreExtension extends StoreExtensionProvider {
|
||||||
override name = 'affine-attachment-block';
|
override name = 'affine-attachment-block';
|
||||||
@@ -12,6 +12,6 @@ export class AttachmentStoreExtension extends StoreExtensionProvider {
|
|||||||
override setup(context: StoreExtensionContext) {
|
override setup(context: StoreExtensionContext) {
|
||||||
super.setup(context);
|
super.setup(context);
|
||||||
context.register(AttachmentBlockSchemaExtension);
|
context.register(AttachmentBlockSchemaExtension);
|
||||||
context.register(AttachmentBlockNotionHtmlAdapterExtension);
|
context.register(AttachmentBlockAdapterExtensions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,102 @@
|
|||||||
import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed';
|
import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed';
|
||||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
import {
|
||||||
import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
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 =
|
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 =
|
export const BookmarkBlockMarkdownAdapterExtension =
|
||||||
BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher);
|
BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
} from '@blocksuite/affine-components/caption';
|
} from '@blocksuite/affine-components/caption';
|
||||||
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
|
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
|
||||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||||
|
import { BlockSelection } from '@blocksuite/std';
|
||||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
@@ -27,6 +28,14 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
|||||||
|
|
||||||
protected containerStyleMap!: ReturnType<typeof styleMap>;
|
protected containerStyleMap!: ReturnType<typeof styleMap>;
|
||||||
|
|
||||||
|
selectBlock = () => {
|
||||||
|
const selectionManager = this.std.selection;
|
||||||
|
const blockSelection = selectionManager.create(BlockSelection, {
|
||||||
|
blockId: this.blockId,
|
||||||
|
});
|
||||||
|
selectionManager.setGroup('note', [blockSelection]);
|
||||||
|
};
|
||||||
|
|
||||||
open = () => {
|
open = () => {
|
||||||
let link = this.model.props.url;
|
let link = this.model.props.url;
|
||||||
if (!link.match(/^[a-zA-Z]+:\/\//)) {
|
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() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -58,6 +98,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
|||||||
this.contentEditable = 'false';
|
this.contentEditable = 'false';
|
||||||
|
|
||||||
if (!this.model.props.description && !this.model.props.title) {
|
if (!this.model.props.description && !this.model.props.title) {
|
||||||
|
if (this.doc.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +128,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
|||||||
})}
|
})}
|
||||||
style=${this.containerStyleMap}
|
style=${this.containerStyleMap}
|
||||||
>
|
>
|
||||||
<bookmark-card
|
${this.isCitation ? this._renderCitationView() : this._renderCardView()}
|
||||||
.bookmark=${this}
|
|
||||||
.loading=${this.loading}
|
|
||||||
.error=${this.error}
|
|
||||||
></bookmark-card>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
|||||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||||
import { OpenInNewIcon } from '@blocksuite/icons/lit';
|
import { OpenInNewIcon } from '@blocksuite/icons/lit';
|
||||||
import {
|
import { isGfxBlockComponent, ShadowlessElement } from '@blocksuite/std';
|
||||||
BlockSelection,
|
|
||||||
isGfxBlockComponent,
|
|
||||||
ShadowlessElement,
|
|
||||||
} from '@blocksuite/std';
|
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -27,7 +23,7 @@ export class BookmarkCard extends SignalWatcher(
|
|||||||
const model = this.bookmark.model;
|
const model = this.bookmark.model;
|
||||||
|
|
||||||
if (model.parent?.flavour !== 'affine:surface') {
|
if (model.parent?.flavour !== 'affine:surface') {
|
||||||
this._selectBlock();
|
this.bookmark.selectBlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,14 +32,6 @@ export class BookmarkCard extends SignalWatcher(
|
|||||||
this.bookmark.open();
|
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 {
|
override connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,91 @@
|
|||||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
import {
|
||||||
|
EmbedLinkedDocBlockSchema,
|
||||||
|
FootNoteReferenceParamsSchema,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
AdapterTextUtils,
|
AdapterTextUtils,
|
||||||
BlockMarkdownAdapterExtension,
|
BlockMarkdownAdapterExtension,
|
||||||
type BlockMarkdownAdapterMatcher,
|
type BlockMarkdownAdapterMatcher,
|
||||||
|
FOOTNOTE_DEFINITION_PREFIX,
|
||||||
|
getFootnoteDefinitionText,
|
||||||
|
isFootnoteDefinitionNode,
|
||||||
|
type MarkdownAST,
|
||||||
} from '@blocksuite/affine-shared/adapters';
|
} 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 =
|
export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||||
{
|
{
|
||||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||||
toMatch: () => false,
|
toMatch: o => isLinkedDocFootnoteDefinitionNode(o.node),
|
||||||
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
|
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: {
|
fromBlockSnapshot: {
|
||||||
enter: (o, context) => {
|
enter: (o, context) => {
|
||||||
const { configs, walkerContext } = context;
|
const { configs, walkerContext } = context;
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
|
|||||||
doc.deleteBlock(this.model);
|
doc.deleteBlock(this.model);
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override _handleClick(evt: MouseEvent): void {
|
protected override _handleClick = (evt: MouseEvent): void => {
|
||||||
if (isNewTabTrigger(evt)) {
|
if (isNewTabTrigger(evt)) {
|
||||||
this.open({ openMode: 'open-in-new-tab', event: evt });
|
this.open({ openMode: 'open-in-new-tab', event: evt });
|
||||||
} else if (isNewViewTrigger(evt)) {
|
} else if (isNewViewTrigger(evt)) {
|
||||||
this.open({ openMode: 'open-in-new-view', event: evt });
|
this.open({ openMode: 'open-in-new-view', event: evt });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
|||||||
static override styles = styles;
|
static override styles = styles;
|
||||||
|
|
||||||
private readonly _load = async () => {
|
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 {
|
const {
|
||||||
loading = true,
|
loading = true,
|
||||||
isError = false,
|
isError = false,
|
||||||
@@ -243,6 +248,17 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
|||||||
return doc?.getStore({ id: this.model.props.pageId });
|
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) {
|
private _handleDoubleClick(event: MouseEvent) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const openDocService = this.std.get(OpenDocExtensionIdentifier);
|
const openDocService = this.std.get(OpenDocExtensionIdentifier);
|
||||||
@@ -264,105 +280,42 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
|||||||
return !!linkedDoc && this.isNoteContentEmpty && this.isBannerEmpty;
|
return !!linkedDoc && this.isNoteContentEmpty && this.isBannerEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _handleClick(event: MouseEvent) {
|
protected _handleClick = (event: MouseEvent) => {
|
||||||
if (isNewTabTrigger(event)) {
|
if (isNewTabTrigger(event)) {
|
||||||
this.open({ openMode: 'open-in-new-tab', event });
|
this.open({ openMode: 'open-in-new-tab', event });
|
||||||
} else if (isNewViewTrigger(event)) {
|
} else if (isNewViewTrigger(event)) {
|
||||||
this.open({ openMode: 'open-in-new-view', event });
|
this.open({ openMode: 'open-in-new-view', event });
|
||||||
}
|
}
|
||||||
this._selectBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
if (this.readonly) {
|
||||||
super.connectedCallback();
|
return;
|
||||||
|
|
||||||
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._selectBlock();
|
||||||
|
};
|
||||||
|
|
||||||
this.disposables.add(
|
private readonly _renderCitationView = () => {
|
||||||
this.model.propsUpdated.subscribe(({ key }) => {
|
const { footnoteIdentifier } = this.model.props;
|
||||||
if (key === 'style') {
|
return html`<div
|
||||||
this._cardStyle = this.model.props.style;
|
draggable="${this.blockDraggable ? 'true' : 'false'}"
|
||||||
}
|
class=${classMap({
|
||||||
if (key === 'pageId' || key === 'style') {
|
'embed-block-container': true,
|
||||||
this._load().catch(e => {
|
...this.selectedStyle$?.value,
|
||||||
console.error(e);
|
})}
|
||||||
this.isError = true;
|
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(): {
|
private readonly _renderEmbedView = () => {
|
||||||
loading?: boolean;
|
|
||||||
isError?: boolean;
|
|
||||||
isNoteContentEmpty?: boolean;
|
|
||||||
isBannerEmpty?: boolean;
|
|
||||||
} {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
override renderBlock() {
|
|
||||||
const linkedDoc = this.linkedDoc;
|
const linkedDoc = this.linkedDoc;
|
||||||
const isDeleted = !linkedDoc;
|
const isDeleted = !linkedDoc;
|
||||||
const isLoading = this._loading;
|
const isLoading = this._loading;
|
||||||
@@ -502,9 +455,107 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
|||||||
</div>
|
</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() {
|
override updated() {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// update card style when linked doc deleted
|
// update card style when linked doc deleted
|
||||||
const linkedDoc = this.linkedDoc;
|
const linkedDoc = this.linkedDoc;
|
||||||
const { xywh, style } = this.model.props;
|
const { xywh, style } = this.model.props;
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import {
|
|||||||
BlockMarkdownAdapterExtension,
|
BlockMarkdownAdapterExtension,
|
||||||
type BlockMarkdownAdapterMatcher,
|
type BlockMarkdownAdapterMatcher,
|
||||||
FOOTNOTE_DEFINITION_PREFIX,
|
FOOTNOTE_DEFINITION_PREFIX,
|
||||||
|
isFootnoteDefinitionNode,
|
||||||
type MarkdownAST,
|
type MarkdownAST,
|
||||||
} from '@blocksuite/affine-shared/adapters';
|
} 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 isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
|
||||||
const isFootnoteDefinitionNode = (
|
|
||||||
node: MarkdownAST
|
|
||||||
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
|
|
||||||
|
|
||||||
const createFootnoteDefinition = (
|
const createFootnoteDefinition = (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
@@ -67,10 +66,35 @@ const createNoteBlockMarkdownAdapterMatcher = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove the footnoteDefinition node from the noteAst
|
const { provider } = context;
|
||||||
noteAst.children = noteAst.children.filter(
|
let enableCitation = false;
|
||||||
child => !isFootnoteDefinitionNode(child)
|
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: {
|
fromBlockSnapshot: {
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import type { DeltaInsert } from '@blocksuite/store';
|
|||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
import type { Heading } from 'mdast';
|
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 PARAGRAPH_MDAST_TYPE = new Set(['paragraph', 'heading', 'blockquote']);
|
||||||
|
|
||||||
const isParagraphMDASTType = (node: MarkdownAST) =>
|
const isParagraphMDASTType = (node: MarkdownAST) =>
|
||||||
@@ -46,6 +55,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'heading': {
|
case 'heading': {
|
||||||
|
const isCollapsed = !!o.node.data?.collapsed;
|
||||||
walkerContext
|
walkerContext
|
||||||
.openNode(
|
.openNode(
|
||||||
{
|
{
|
||||||
@@ -54,6 +64,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
|||||||
flavour: 'affine:paragraph',
|
flavour: 'affine:paragraph',
|
||||||
props: {
|
props: {
|
||||||
type: `h${o.node.depth}`,
|
type: `h${o.node.depth}`,
|
||||||
|
collapsed: isCollapsed,
|
||||||
text: {
|
text: {
|
||||||
'$blocksuite:internal:text$': true,
|
'$blocksuite:internal:text$': true,
|
||||||
delta: deltaConverter.astToDelta(o.node),
|
delta: deltaConverter.astToDelta(o.node),
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"./linked-doc-title": "./src/linked-doc-title/index.ts",
|
"./linked-doc-title": "./src/linked-doc-title/index.ts",
|
||||||
"./view-dropdown-menu": "./src/view-dropdown-menu/index.ts",
|
"./view-dropdown-menu": "./src/view-dropdown-menu/index.ts",
|
||||||
"./card-style-dropdown-menu": "./src/card-style-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",
|
"./highlight-dropdown-menu": "./src/highlight-dropdown-menu/index.ts",
|
||||||
"./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts",
|
"./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts",
|
||||||
"./size-dropdown-menu": "./src/size-dropdown-menu/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 { WithDisposable } from '@blocksuite/global/lit';
|
||||||
import { ToggleDownIcon, ToggleRightIcon } from '@blocksuite/icons/lit';
|
import { ToggleDownIcon, ToggleRightIcon } from '@blocksuite/icons/lit';
|
||||||
import { ShadowlessElement } from '@blocksuite/std';
|
import { ShadowlessElement } from '@blocksuite/std';
|
||||||
@@ -43,6 +44,12 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
|
|||||||
.with-drag-handle .affine-block-children-container .toggle-icon {
|
.with-drag-handle .affine-block-children-container .toggle-icon {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
svg {
|
||||||
|
color: ${unsafeCSSVarV2('icon/primary', '#77757D')};
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
@@ -55,7 +62,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
|
|||||||
${ToggleDownIcon({
|
${ToggleDownIcon({
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
style: 'color: #77757D',
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -70,7 +76,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
|
|||||||
${ToggleRightIcon({
|
${ToggleRightIcon({
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
style: 'color: #77757D',
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection';
|
|||||||
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
|
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
|
||||||
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
|
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
|
||||||
import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu';
|
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 componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
|
||||||
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
||||||
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
||||||
@@ -46,6 +47,7 @@ export function effects() {
|
|||||||
componentLinkPreviewEffects();
|
componentLinkPreviewEffects();
|
||||||
componentLinkedDocTitleEffects();
|
componentLinkedDocTitleEffects();
|
||||||
componentCardStyleDropdownMenuEffects();
|
componentCardStyleDropdownMenuEffects();
|
||||||
|
componentCitationEffects();
|
||||||
componentHighlightDropdownMenuEffects();
|
componentHighlightDropdownMenuEffects();
|
||||||
componentViewDropdownMenuEffects();
|
componentViewDropdownMenuEffects();
|
||||||
componentTooltipContentWithShortcutEffects();
|
componentTooltipContentWithShortcutEffects();
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||||
transition: background 0.3s ease-in-out;
|
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) => {
|
private readonly _handleDocReference = (docId: string) => {
|
||||||
if (this.readonly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.std
|
this.std
|
||||||
.getOptional(PeekViewProvider)
|
.getOptional(PeekViewProvider)
|
||||||
?.peek({
|
?.peek({
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export type AttachmentBlockProps = {
|
|||||||
embed: boolean | BackwardCompatibleUndefined;
|
embed: boolean | BackwardCompatibleUndefined;
|
||||||
|
|
||||||
style?: (typeof AttachmentBlockStyles)[number];
|
style?: (typeof AttachmentBlockStyles)[number];
|
||||||
|
|
||||||
|
footnoteIdentifier: string | null;
|
||||||
} & Omit<GfxCommonBlockProps, 'scale'> &
|
} & Omit<GfxCommonBlockProps, 'scale'> &
|
||||||
BlockMeta;
|
BlockMeta;
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ export const defaultAttachmentProps: AttachmentBlockProps = {
|
|||||||
'meta:updatedAt': undefined,
|
'meta:updatedAt': undefined,
|
||||||
'meta:createdBy': undefined,
|
'meta:createdBy': undefined,
|
||||||
'meta:updatedBy': undefined,
|
'meta:updatedBy': undefined,
|
||||||
|
footnoteIdentifier: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AttachmentBlockSchema = defineBlockSchema({
|
export const AttachmentBlockSchema = defineBlockSchema({
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ export const BookmarkStyles: EmbedCardStyle[] = [
|
|||||||
'horizontal',
|
'horizontal',
|
||||||
'list',
|
'list',
|
||||||
'cube',
|
'cube',
|
||||||
|
'citation',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type BookmarkBlockProps = {
|
export type BookmarkBlockProps = {
|
||||||
style: (typeof BookmarkStyles)[number];
|
style: (typeof BookmarkStyles)[number];
|
||||||
url: string;
|
url: string;
|
||||||
caption: string | null;
|
caption: string | null;
|
||||||
|
footnoteIdentifier: string | null;
|
||||||
} & LinkPreviewData &
|
} & LinkPreviewData &
|
||||||
Omit<GfxCommonBlockProps, 'scale'> &
|
Omit<GfxCommonBlockProps, 'scale'> &
|
||||||
BlockMeta;
|
BlockMeta;
|
||||||
@@ -48,6 +50,8 @@ const defaultBookmarkProps: BookmarkBlockProps = {
|
|||||||
'meta:updatedAt': undefined,
|
'meta:updatedAt': undefined,
|
||||||
'meta:createdBy': undefined,
|
'meta:createdBy': undefined,
|
||||||
'meta:updatedBy': undefined,
|
'meta:updatedBy': undefined,
|
||||||
|
|
||||||
|
footnoteIdentifier: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BookmarkBlockSchema = defineBlockSchema({
|
export const BookmarkBlockSchema = defineBlockSchema({
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
|
|||||||
'list',
|
'list',
|
||||||
'cube',
|
'cube',
|
||||||
'horizontalThin',
|
'horizontalThin',
|
||||||
|
'citation',
|
||||||
];
|
];
|
||||||
|
|
||||||
export type EmbedLinkedDocBlockProps = {
|
export type EmbedLinkedDocBlockProps = {
|
||||||
style: EmbedCardStyle;
|
style: EmbedCardStyle;
|
||||||
caption: string | null;
|
caption: string | null;
|
||||||
|
footnoteIdentifier: string | null;
|
||||||
} & ReferenceInfo;
|
} & ReferenceInfo;
|
||||||
|
|
||||||
export class EmbedLinkedDocModel extends defineEmbedModel<EmbedLinkedDocBlockProps>(
|
export class EmbedLinkedDocModel extends defineEmbedModel<EmbedLinkedDocBlockProps>(
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const defaultEmbedLinkedDocBlockProps: EmbedLinkedDocBlockProps = {
|
|||||||
// title & description aliases
|
// title & description aliases
|
||||||
title: undefined,
|
title: undefined,
|
||||||
description: undefined,
|
description: undefined,
|
||||||
|
|
||||||
|
footnoteIdentifier: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedLinkedDocBlockSchema = createEmbedBlockSchema({
|
export const EmbedLinkedDocBlockSchema = createEmbedBlockSchema({
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export type EmbedCardStyle =
|
|||||||
| 'figma'
|
| 'figma'
|
||||||
| 'html'
|
| 'html'
|
||||||
| 'syncedDoc'
|
| 'syncedDoc'
|
||||||
| 'pdf';
|
| 'pdf'
|
||||||
|
| 'citation';
|
||||||
|
|
||||||
export type LinkPreviewData = {
|
export type LinkPreviewData = {
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ export {
|
|||||||
type BlockMarkdownAdapterMatcher,
|
type BlockMarkdownAdapterMatcher,
|
||||||
BlockMarkdownAdapterMatcherIdentifier,
|
BlockMarkdownAdapterMatcherIdentifier,
|
||||||
FOOTNOTE_DEFINITION_PREFIX,
|
FOOTNOTE_DEFINITION_PREFIX,
|
||||||
|
getFootnoteDefinitionText,
|
||||||
IN_PARAGRAPH_NODE_CONTEXT_KEY,
|
IN_PARAGRAPH_NODE_CONTEXT_KEY,
|
||||||
InlineDeltaToMarkdownAdapterExtension,
|
InlineDeltaToMarkdownAdapterExtension,
|
||||||
type InlineDeltaToMarkdownAdapterMatcher,
|
type InlineDeltaToMarkdownAdapterMatcher,
|
||||||
InlineDeltaToMarkdownAdapterMatcherIdentifier,
|
InlineDeltaToMarkdownAdapterMatcherIdentifier,
|
||||||
|
isFootnoteDefinitionNode,
|
||||||
isMarkdownAST,
|
isMarkdownAST,
|
||||||
type Markdown,
|
type Markdown,
|
||||||
MarkdownAdapter,
|
MarkdownAdapter,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Root, RootContentMap } from 'mdast';
|
import type { FootnoteDefinition, Root, RootContentMap } from 'mdast';
|
||||||
|
|
||||||
export type Markdown = string;
|
export type Markdown = string;
|
||||||
|
|
||||||
@@ -16,5 +16,17 @@ export const isMarkdownAST = (node: unknown): node is MarkdownAST =>
|
|||||||
'type' in (node as object) &&
|
'type' in (node as object) &&
|
||||||
(node as MarkdownAST).type !== undefined;
|
(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 FOOTNOTE_DEFINITION_PREFIX = 'footnoteDefinition:';
|
||||||
export const IN_PARAGRAPH_NODE_CONTEXT_KEY = 'mdast:paragraph';
|
export const IN_PARAGRAPH_NODE_CONTEXT_KEY = 'mdast:paragraph';
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const EMBED_CARD_WIDTH: Record<EmbedCardStyle, number> = {
|
|||||||
html: 752,
|
html: 752,
|
||||||
syncedDoc: 800,
|
syncedDoc: 800,
|
||||||
pdf: 537 + 24 + 2,
|
pdf: 537 + 24 + 2,
|
||||||
|
citation: 752,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
|
export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
|
||||||
@@ -45,6 +46,7 @@ export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
|
|||||||
html: 544,
|
html: 544,
|
||||||
syncedDoc: 455,
|
syncedDoc: 455,
|
||||||
pdf: 759 + 46 + 24 + 2,
|
pdf: 759 + 46 + 24 + 2,
|
||||||
|
citation: 52,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMBED_BLOCK_FLAVOUR_LIST = [
|
export const EMBED_BLOCK_FLAVOUR_LIST = [
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface BlockSuiteFlags {
|
|||||||
enable_edgeless_scribbled_style: boolean;
|
enable_edgeless_scribbled_style: boolean;
|
||||||
enable_embed_doc_with_alias: boolean;
|
enable_embed_doc_with_alias: boolean;
|
||||||
enable_turbo_renderer: boolean;
|
enable_turbo_renderer: boolean;
|
||||||
|
enable_citation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeatureFlagService extends StoreExtension {
|
export class FeatureFlagService extends StoreExtension {
|
||||||
@@ -44,6 +45,7 @@ export class FeatureFlagService extends StoreExtension {
|
|||||||
enable_edgeless_scribbled_style: false,
|
enable_edgeless_scribbled_style: false,
|
||||||
enable_embed_doc_with_alias: false,
|
enable_embed_doc_with_alias: false,
|
||||||
enable_turbo_renderer: false,
|
enable_turbo_renderer: false,
|
||||||
|
enable_citation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
|
||||||
|
|||||||
@@ -121,10 +121,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.affine-paragraph-block-container {
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-answer-text-editor {
|
.ai-answer-text-editor {
|
||||||
.affine-note-block-container {
|
.affine-note-block-container {
|
||||||
> .affine-block-children-container {
|
> .affine-block-children-container {
|
||||||
@@ -138,6 +134,23 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.affine-paragraph-block-container {
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.h6 {
|
||||||
|
padding-left: 16px;
|
||||||
|
color: ${unsafeCSSVarV2('text/link')};
|
||||||
|
font-size: var(--affine-font-base);
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
transform: translateX(0);
|
||||||
|
svg {
|
||||||
|
color: ${unsafeCSSVarV2('text/link')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-renderer-container {
|
.text-renderer-container {
|
||||||
@@ -216,6 +229,9 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
|||||||
'affine:list',
|
'affine:list',
|
||||||
'affine:divider',
|
'affine:divider',
|
||||||
'affine:latex',
|
'affine:latex',
|
||||||
|
'affine:bookmark',
|
||||||
|
'affine:attachment',
|
||||||
|
'affine:embed-linked-doc',
|
||||||
].map(flavour => ({ flavour, viewType: 'display' })),
|
].map(flavour => ({ flavour, viewType: 'display' })),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,16 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: isCanaryBuild,
|
configurable: isCanaryBuild,
|
||||||
defaultState: isCanaryBuild,
|
defaultState: isCanaryBuild,
|
||||||
},
|
},
|
||||||
|
enable_citation: {
|
||||||
|
category: 'blocksuite',
|
||||||
|
bsFlag: 'enable_citation',
|
||||||
|
displayName:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-citation.name',
|
||||||
|
description:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-citation.description',
|
||||||
|
configurable: isCanaryBuild,
|
||||||
|
defaultState: isCanaryBuild,
|
||||||
|
},
|
||||||
|
|
||||||
enable_emoji_folder_icon: {
|
enable_emoji_folder_icon: {
|
||||||
category: 'affine',
|
category: 'affine',
|
||||||
|
|||||||
@@ -5617,6 +5617,14 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Let your words stand out. This also include the callout in the transcription block.`
|
* `Let your words stand out. This also include the callout in the transcription block.`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.experimental-features.enable-callout.description"](): string;
|
["com.affine.settings.workspace.experimental-features.enable-callout.description"](): string;
|
||||||
|
/**
|
||||||
|
* `Citation`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.experimental-features.enable-citation.name"](): string;
|
||||||
|
/**
|
||||||
|
* `Enable citation feature.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.experimental-features.enable-citation.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Embed Iframe Block`
|
* `Embed Iframe Block`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1402,6 +1402,8 @@
|
|||||||
"com.affine.settings.workspace.experimental-features.enable-block-meta.description": "Once enabled, all blocks will have created time, updated time, created by and updated by.",
|
"com.affine.settings.workspace.experimental-features.enable-block-meta.description": "Once enabled, all blocks will have created time, updated time, created by and updated by.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-callout.name": "Callout",
|
"com.affine.settings.workspace.experimental-features.enable-callout.name": "Callout",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-callout.description": "Let your words stand out. This also include the callout in the transcription block.",
|
"com.affine.settings.workspace.experimental-features.enable-callout.description": "Let your words stand out. This also include the callout in the transcription block.",
|
||||||
|
"com.affine.settings.workspace.experimental-features.enable-citation.name": "Citation",
|
||||||
|
"com.affine.settings.workspace.experimental-features.enable-citation.description": "Enable citation feature.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-embed-iframe-block.name": "Embed Iframe Block",
|
"com.affine.settings.workspace.experimental-features.enable-embed-iframe-block.name": "Embed Iframe Block",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-embed-iframe-block.description": "Enables Embed Iframe Block.",
|
"com.affine.settings.workspace.experimental-features.enable-embed-iframe-block.description": "Enables Embed Iframe Block.",
|
||||||
"com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name": "Emoji Folder Icon",
|
"com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name": "Emoji Folder Icon",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"props": {
|
"props": {
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"caption": "",
|
"caption": "",
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"props": {
|
"props": {
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"props": {
|
"props": {
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"props": {
|
"props": {
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"props": {
|
"props": {
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"props": {
|
"props": {
|
||||||
"embed": false,
|
"embed": false,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"name": "test-card-1.png",
|
"name": "test-card-1.png",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"url": "http://localhost",
|
"url": "http://localhost",
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"title": null,
|
"title": null,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
|
|||||||
@@ -51,7 +51,8 @@
|
|||||||
"index": "a0",
|
"index": "a0",
|
||||||
"xywh": "[0,0,0,0]",
|
"xywh": "[0,0,0,0]",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"rotate": 0
|
"rotate": 0,
|
||||||
|
"footnoteIdentifier": null
|
||||||
},
|
},
|
||||||
"children": []
|
"children": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,8 @@
|
|||||||
"index": "a0",
|
"index": "a0",
|
||||||
"xywh": "[0,0,0,0]",
|
"xywh": "[0,0,0,0]",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"rotate": 0
|
"rotate": 0,
|
||||||
|
"footnoteIdentifier": null
|
||||||
},
|
},
|
||||||
"children": []
|
"children": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"url": "http://localhost",
|
"url": "http://localhost",
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"title": null,
|
"title": null,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
"url": "http://localhost",
|
"url": "http://localhost",
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"title": null,
|
"title": null,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"url": "http://localhost",
|
"url": "http://localhost",
|
||||||
"caption": null,
|
"caption": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"image": null,
|
"image": null,
|
||||||
"title": null,
|
"title": null,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"id": "11",
|
"id": "11",
|
||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"pageId": "6",
|
"pageId": "6",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"id": "11",
|
"id": "11",
|
||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"pageId": "6",
|
"pageId": "6",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"id": "11",
|
"id": "11",
|
||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"pageId": "6",
|
"pageId": "6",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"id": "12",
|
"id": "12",
|
||||||
"props": {
|
"props": {
|
||||||
"caption": null,
|
"caption": null,
|
||||||
|
"footnoteIdentifier": null,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"lockedBySelf": false,
|
"lockedBySelf": false,
|
||||||
"pageId": "5",
|
"pageId": "5",
|
||||||
|
|||||||
Reference in New Issue
Block a user