From df910d7013c2018895650352a805373d00d5e81f Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Fri, 17 Jan 2025 09:38:42 +0000 Subject: [PATCH] feat(editor): add affine inline footnote (#9745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [BS-2369](https://linear.app/affine-design/issue/BS-2369/新增-affinetextattribute-footnote) [BS-2370](https://linear.app/affine-design/issue/BS-2370/支持-footnote-自定义渲染行内内容) [BS-2372](https://linear.app/affine-design/issue/BS-2372/提供-footnoteconfigextension) [BS-2375](https://linear.app/affine-design/issue/BS-2375/footnote-自定义渲染-popup) ### Add new AffineTextAttribute: footnote ``` /** * FootNote is used to reference a doc, attachment or url. */ export interface AffineTextAttributes { ... footnote?: { label: string; // label of the footnote reference: { type: 'doc' | 'attachment' | 'url'; // type of reference docId?: string; // the id of the reference doc url?: string; // the url of the reference network resource blobId?: string; // the id of the reference attachment fileName?: string; // the name of the reference attachment fileType?: string; // the type of the reference attachment } } | null } ``` ### FootNoteNodeConfigProvider Extension #### FootNoteNodeConfig Type Definition ``` type FootNoteNodeRenderer = ( footnote: FootNote, std: BlockStdScope ) => TemplateResult<1>; type FootNotePopupRenderer = ( footnote: FootNote, std: BlockStdScope, abortController: AbortController ) => TemplateResult<1>; export interface FootNoteNodeConfig { customNodeRenderer?: FootNoteNodeRenderer; customPopupRenderer?: FootNotePopupRenderer; interactive?: boolean; hidePopup?: boolean; } ``` #### FootNoteNodeConfigProvider Class ``` export class FootNoteNodeConfigProvider { private _customNodeRenderer?: FootNoteNodeRenderer; private _customPopupRenderer?: FootNotePopupRenderer; private _hidePopup: boolean; private _interactive: boolean; get customNodeRenderer() { return this._customNodeRenderer; } get customPopupRenderer() { return this._customPopupRenderer; } get doc() { return this.std.store; } get hidePopup() { return this._hidePopup; } get interactive() { return this._interactive; } constructor( config: FootNoteNodeConfig, readonly std: BlockStdScope ) { this._customNodeRenderer = config.customNodeRenderer; this._customPopupRenderer = config.customPopupRenderer; this._hidePopup = config.hidePopup ?? false; this._interactive = config.interactive ?? true; } setCustomNodeRenderer(renderer: FootNoteNodeRenderer) { this._customNodeRenderer = renderer; } setCustomPopupRenderer(renderer: FootNotePopupRenderer) { this._customPopupRenderer = renderer; } setHidePopup(hidePopup: boolean) { this._hidePopup = hidePopup; } setInteractive(interactive: boolean) { this._interactive = interactive; } } ``` #### FootNoteNodeConfigProvider Extension ``` export const FootNoteNodeConfigIdentifier = createIdentifier('AffineFootNoteNodeConfig'); export function FootNoteNodeConfigExtension( config: FootNoteNodeConfig ): ExtensionType { return { setup: di => { di.addImpl( FootNoteNodeConfigIdentifier, provider => new FootNoteNodeConfigProvider(config, provider.get(StdIdentifier)) ); }, }; } ``` The footnote node can be extended by this extension. ### FootnoteInlineSpec ``` export const FootNoteInlineSpecExtension = InlineSpecExtension( 'footnote', provider => { const std = provider.get(StdIdentifier); const config = provider.getOptional(FootNoteNodeConfigIdentifier) ?? undefined; return { name: 'footnote', schema: FootNoteSchema.optional().nullable().catch(undefined), match: delta => { return !!delta.attributes?.footnote; }, renderer: ({ delta }) => { return html``; }, embed: true, }; } ); ``` --- .../src/rich-text/all-extensions.ts | 2 + .../components/src/rich-text/effects.ts | 14 +- .../inline/presets/affine-inline-specs.ts | 28 ++- .../nodes/footnote-node/footnote-config.ts | 93 ++++++++++ .../nodes/footnote-node/footnote-node.ts | 167 ++++++++++++++++++ .../footnote-node/footnote-popup-chip.ts | 89 ++++++++++ .../nodes/footnote-node/footnote-popup.ts | 126 +++++++++++++ .../rich-text/inline/presets/nodes/index.ts | 2 + blocksuite/affine/model/src/consts/doc.ts | 33 ++++ blocksuite/affine/shared/src/types/index.ts | 7 +- 10 files changed, 558 insertions(+), 3 deletions(-) create mode 100644 blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-config.ts create mode 100644 blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts create mode 100644 blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts create mode 100644 blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts diff --git a/blocksuite/affine/components/src/rich-text/all-extensions.ts b/blocksuite/affine/components/src/rich-text/all-extensions.ts index deff7d00e..a0213db48 100644 --- a/blocksuite/affine/components/src/rich-text/all-extensions.ts +++ b/blocksuite/affine/components/src/rich-text/all-extensions.ts @@ -6,6 +6,7 @@ import { BoldInlineSpecExtension, CodeInlineSpecExtension, ColorInlineSpecExtension, + FootNoteInlineSpecExtension, InlineAdapterExtensions, InlineSpecExtensions, ItalicInlineSpecExtension, @@ -31,6 +32,7 @@ export const DefaultInlineManagerExtension = InlineManagerExtension({ LatexInlineSpecExtension.identifier, ReferenceInlineSpecExtension.identifier, LinkInlineSpecExtension.identifier, + FootNoteInlineSpecExtension.identifier, ], }); diff --git a/blocksuite/affine/components/src/rich-text/effects.ts b/blocksuite/affine/components/src/rich-text/effects.ts index 64a44e761..0f2a7aeef 100644 --- a/blocksuite/affine/components/src/rich-text/effects.ts +++ b/blocksuite/affine/components/src/rich-text/effects.ts @@ -16,8 +16,14 @@ import type { toggleTextStyleCommand, toggleUnderline, } from './format/text-style.js'; -import { AffineLink, AffineReference } from './inline/index.js'; +import { + AffineFootnoteNode, + AffineLink, + AffineReference, +} from './inline/index.js'; import { AffineText } from './inline/presets/nodes/affine-text.js'; +import { FootNotePopup } from './inline/presets/nodes/footnote-node/footnote-popup.js'; +import { FootNotePopupChip } from './inline/presets/nodes/footnote-node/footnote-popup-chip.js'; import { LatexEditorMenu } from './inline/presets/nodes/latex-node/latex-editor-menu.js'; import { LatexEditorUnit } from './inline/presets/nodes/latex-node/latex-editor-unit.js'; import { AffineLatexNode } from './inline/presets/nodes/latex-node/latex-node.js'; @@ -37,12 +43,18 @@ export function effects() { customElements.define('reference-popup', ReferencePopup); customElements.define('reference-alias-popup', ReferenceAliasPopup); customElements.define('affine-reference', AffineReference); + customElements.define('affine-footnote-node', AffineFootnoteNode); + customElements.define('footnote-popup', FootNotePopup); + customElements.define('footnote-popup-chip', FootNotePopupChip); } declare global { interface HTMLElementTagNameMap { 'affine-latex-node': AffineLatexNode; 'affine-reference': AffineReference; + 'affine-footnote-node': AffineFootnoteNode; + 'footnote-popup': FootNotePopup; + 'footnote-popup-chip': FootNotePopupChip; 'affine-link': AffineLink; 'affine-text': AffineText; 'rich-text': RichText; diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/affine-inline-specs.ts b/blocksuite/affine/components/src/rich-text/inline/presets/affine-inline-specs.ts index 04518ea78..fe957b063 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/affine-inline-specs.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/affine-inline-specs.ts @@ -1,4 +1,4 @@ -import { ReferenceInfoSchema } from '@blocksuite/affine-model'; +import { FootNoteSchema, ReferenceInfoSchema } from '@blocksuite/affine-model'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { StdIdentifier } from '@blocksuite/block-std'; import type { InlineEditor, InlineRootElement } from '@blocksuite/inline'; @@ -6,6 +6,7 @@ import { html } from 'lit'; import { z } from 'zod'; import { InlineSpecExtension } from '../../extension/index.js'; +import { FootNoteNodeConfigIdentifier } from './nodes/footnote-node/footnote-config.js'; import { ReferenceNodeConfigIdentifier, ReferenceNodeConfigProvider, @@ -178,6 +179,30 @@ export const LatexEditorUnitSpecExtension = InlineSpecExtension({ }, }); +export const FootNoteInlineSpecExtension = InlineSpecExtension( + 'footnote', + provider => { + const std = provider.get(StdIdentifier); + const config = + provider.getOptional(FootNoteNodeConfigIdentifier) ?? undefined; + return { + name: 'footnote', + schema: FootNoteSchema.optional().nullable().catch(undefined), + match: delta => { + return !!delta.attributes?.footnote; + }, + renderer: ({ delta }) => { + return html``; + }, + embed: true, + }; + } +); + export const InlineSpecExtensions = [ BoldInlineSpecExtension, ItalicInlineSpecExtension, @@ -190,4 +215,5 @@ export const InlineSpecExtensions = [ ReferenceInlineSpecExtension, LinkInlineSpecExtension, LatexEditorUnitSpecExtension, + FootNoteInlineSpecExtension, ]; diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-config.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-config.ts new file mode 100644 index 000000000..2bc4c7fd1 --- /dev/null +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-config.ts @@ -0,0 +1,93 @@ +import type { FootNote } from '@blocksuite/affine-model'; +import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std'; +import { createIdentifier } from '@blocksuite/global/di'; +import type { ExtensionType } from '@blocksuite/store'; +import type { TemplateResult } from 'lit'; + +type FootNoteNodeRenderer = ( + footnote: FootNote, + std: BlockStdScope +) => TemplateResult<1>; + +type FootNotePopupRenderer = ( + footnote: FootNote, + std: BlockStdScope, + abortController: AbortController +) => TemplateResult<1>; + +export interface FootNoteNodeConfig { + customNodeRenderer?: FootNoteNodeRenderer; + customPopupRenderer?: FootNotePopupRenderer; + interactive?: boolean; + hidePopup?: boolean; +} + +export class FootNoteNodeConfigProvider { + private _customNodeRenderer?: FootNoteNodeRenderer; + private _customPopupRenderer?: FootNotePopupRenderer; + private _hidePopup: boolean; + private _interactive: boolean; + + get customNodeRenderer() { + return this._customNodeRenderer; + } + + get customPopupRenderer() { + return this._customPopupRenderer; + } + + get doc() { + return this.std.store; + } + + get hidePopup() { + return this._hidePopup; + } + + get interactive() { + return this._interactive; + } + + constructor( + config: FootNoteNodeConfig, + readonly std: BlockStdScope + ) { + this._customNodeRenderer = config.customNodeRenderer; + this._customPopupRenderer = config.customPopupRenderer; + this._hidePopup = config.hidePopup ?? false; + this._interactive = config.interactive ?? true; + } + + setCustomNodeRenderer(renderer: FootNoteNodeRenderer) { + this._customNodeRenderer = renderer; + } + + setCustomPopupRenderer(renderer: FootNotePopupRenderer) { + this._customPopupRenderer = renderer; + } + + setHidePopup(hidePopup: boolean) { + this._hidePopup = hidePopup; + } + + setInteractive(interactive: boolean) { + this._interactive = interactive; + } +} + +export const FootNoteNodeConfigIdentifier = + createIdentifier('AffineFootNoteNodeConfig'); + +export function FootNoteNodeConfigExtension( + config: FootNoteNodeConfig +): ExtensionType { + return { + setup: di => { + di.addImpl( + FootNoteNodeConfigIdentifier, + provider => + new FootNoteNodeConfigProvider(config, provider.get(StdIdentifier)) + ); + }, + }; +} diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts new file mode 100644 index 000000000..865acd4cb --- /dev/null +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts @@ -0,0 +1,167 @@ +import type { FootNote } from '@blocksuite/affine-model'; +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import { + BlockSelection, + type BlockStdScope, + ShadowlessElement, + TextSelection, +} from '@blocksuite/block-std'; +import { WithDisposable } from '@blocksuite/global/utils'; +import { + type DeltaInsert, + INLINE_ROOT_ATTR, + type InlineRootElement, + ZERO_WIDTH_NON_JOINER, + ZERO_WIDTH_SPACE, +} from '@blocksuite/inline'; +import { baseTheme } from '@toeverything/theme'; +import { css, html, nothing, unsafeCSS } from 'lit'; +import { property } from 'lit/decorators.js'; +import { ref } from 'lit-html/directives/ref.js'; + +import { HoverController } from '../../../../../hover/controller'; +import type { FootNoteNodeConfigProvider } from './footnote-config'; + +export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) { + static override styles = css` + .footnote-node { + padding: 0 2px; + user-select: none; + cursor: pointer; + } + + .footnote-content-default { + display: inline-block; + background: ${unsafeCSSVarV2('button/primary')}; + color: ${unsafeCSSVarV2('button/pureWhiteText')}; + width: 14px; + height: 14px; + line-height: 14px; + font-size: 10px; + font-weight: 400; + border-radius: 50%; + text-align: center; + text-overflow: ellipsis; + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + } + `; + + get customNodeRenderer() { + return this.config?.customNodeRenderer; + } + + get customPopupRenderer() { + return this.config?.customPopupRenderer; + } + + get interactive() { + return this.config?.interactive; + } + + get hidePopup() { + return this.config?.hidePopup; + } + + get inlineEditor() { + const inlineRoot = this.closest>( + `[${INLINE_ROOT_ATTR}]` + ); + return inlineRoot?.inlineEditor; + } + + get selfInlineRange() { + const selfInlineRange = this.inlineEditor?.getInlineRangeFromElement(this); + return selfInlineRange; + } + + private readonly _FootNoteDefaultContent = (footnote: FootNote) => { + return html`${footnote.label}`; + }; + + private readonly _FootNotePopup = ( + footnote: FootNote, + abortController: AbortController + ) => { + return this.customPopupRenderer + ? this.customPopupRenderer(footnote, this.std, abortController) + : html``; + }; + + private readonly _whenHover: HoverController = new HoverController( + this, + ({ abortController }) => { + const footnote = this.delta.attributes?.footnote; + if (!footnote) return null; + + if ( + this.config?.hidePopup || + !this.selfInlineRange || + !this.inlineEditor + ) { + return null; + } + + const selection = this.std?.selection; + if (!selection) { + return null; + } + const textSelection = selection.find(TextSelection); + if (!!textSelection && !textSelection.isCollapsed()) { + return null; + } + + const blockSelections = selection.filter(BlockSelection); + if (blockSelections.length) { + return null; + } + + return { + template: this._FootNotePopup(footnote, abortController), + container: this, + computePosition: { + referenceElement: this, + placement: 'top', + autoUpdate: true, + }, + }; + }, + { enterDelay: 500 } + ); + + override render() { + const attributes = this.delta.attributes; + const footnote = attributes?.footnote; + if (!footnote) { + return nothing; + } + + const node = this.customNodeRenderer + ? this.customNodeRenderer(footnote, this.std) + : this._FootNoteDefaultContent(footnote); + + return html`${node}`; + } + + @property({ attribute: false }) + accessor config: FootNoteNodeConfigProvider | undefined = undefined; + + @property({ type: Object }) + accessor delta: DeltaInsert = { + insert: ZERO_WIDTH_SPACE, + attributes: {}, + }; + + @property({ attribute: false }) + accessor std!: BlockStdScope; +} diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts new file mode 100644 index 000000000..66193f7c2 --- /dev/null +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts @@ -0,0 +1,89 @@ +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +export class FootNotePopupChip extends LitElement { + static override styles = css` + .popup-chip-container { + display: flex; + border-radius: 4px; + max-width: 173px; + height: 24px; + padding: 2px 4px; + align-items: center; + gap: 4px; + box-sizing: border-box; + cursor: default; + } + + .prefix-icon, + .suffix-icon { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: ${unsafeCSSVarV2('icon/primary')}; + border-radius: 4px; + + svg { + width: 16px; + height: 16px; + } + } + + .suffix-icon:hover { + background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; + cursor: pointer; + } + + .popup-chip-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; + height: 20px; + line-height: 20px; + color: ${unsafeCSSVarV2('text/primary')}; + font-size: 12px; + font-weight: 400; + } + `; + + override render() { + return html` + + `; + } + + @property({ attribute: false }) + accessor prefixIcon: TemplateResult | undefined = undefined; + + @property({ attribute: false }) + accessor label: string = ''; + + @property({ attribute: false }) + accessor suffixIcon: TemplateResult | undefined = undefined; + + @property({ attribute: false }) + accessor onClick: (() => void) | undefined = undefined; + + @property({ attribute: false }) + accessor onPrefixClick: (() => void) | undefined = undefined; + + @property({ attribute: false }) + accessor onSuffixClick: (() => void) | undefined = undefined; +} diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts new file mode 100644 index 000000000..e929b6753 --- /dev/null +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts @@ -0,0 +1,126 @@ +import type { FootNote } from '@blocksuite/affine-model'; +import { DocDisplayMetaProvider } from '@blocksuite/affine-shared/services'; +import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import type { BlockStdScope } from '@blocksuite/block-std'; +import { WithDisposable } from '@blocksuite/global/utils'; +import { DualLinkIcon, LinkIcon } from '@blocksuite/icons/lit'; +import { css, html, LitElement, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +import { getAttachmentFileIcons } from '../../../../../icons'; +import { RefNodeSlotsProvider } from '../../../../extension/ref-node-slots'; + +export class FootNotePopup extends WithDisposable(LitElement) { + static override styles = css` + .footnote-popup-container { + border-radius: 4px; + box-shadow: ${unsafeCSSVar('overlayPanelShadow')}; + border-radius: 4px; + background-color: ${unsafeCSSVarV2('layer/background/primary')}; + border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; + } + `; + + private readonly _prefixIcon = () => { + const referenceType = this.footnote.reference.type; + if (referenceType === 'doc') { + const docId = this.footnote.reference.docId; + if (!docId) { + return undefined; + } + return this.std.get(DocDisplayMetaProvider).icon(docId).value; + } else if (referenceType === 'attachment') { + const fileType = this.footnote.reference.fileType; + if (!fileType) { + return undefined; + } + return getAttachmentFileIcons(fileType); + } + return undefined; + }; + + private readonly _suffixIcon = (): TemplateResult | undefined => { + const referenceType = this.footnote.reference.type; + if (referenceType === 'doc') { + return DualLinkIcon({ width: '16px', height: '16px' }); + } else if (referenceType === 'url') { + return LinkIcon({ width: '16px', height: '16px' }); + } + return undefined; + }; + + private readonly _popupLabel = () => { + const referenceType = this.footnote.reference.type; + let label = ''; + const { docId, fileName, url } = this.footnote.reference; + switch (referenceType) { + case 'doc': + if (!docId) { + return label; + } + label = this.std.get(DocDisplayMetaProvider).title(docId).value; + break; + case 'attachment': + if (!fileName) { + return label; + } + label = fileName; + break; + case 'url': + if (!url) { + return label; + } + // TODO(@chen): get url title from url, need to implement after LinkPreviewer refactored as an extension + label = url; + break; + } + return label; + }; + + /** + * When clicking the chip, we will navigate to the reference doc or open the url + */ + private readonly _onChipClick = () => { + const referenceType = this.footnote.reference.type; + const { docId, url } = this.footnote.reference; + switch (referenceType) { + case 'doc': + if (!docId) { + break; + } + this.std + .getOptional(RefNodeSlotsProvider) + ?.docLinkClicked.emit({ pageId: docId }); + break; + case 'url': + if (!url) { + break; + } + window.open(url, '_blank'); + break; + } + this.abortController.abort(); + }; + + override render() { + return html` +
+ +
+ `; + } + + @property({ attribute: false }) + accessor footnote!: FootNote; + + @property({ attribute: false }) + accessor std!: BlockStdScope; + + @property({ attribute: false }) + accessor abortController!: AbortController; +} diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/index.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/index.ts index ece36cc15..fc1e91136 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/index.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/index.ts @@ -1,3 +1,5 @@ +export * from './footnote-node/footnote-config.js'; +export { AffineFootnoteNode } from './footnote-node/footnote-node.js'; export { AffineLink, toggleLinkPopup } from './link-node/index.js'; export * from './reference-node/reference-config.js'; export { AffineReference } from './reference-node/reference-node.js'; diff --git a/blocksuite/affine/model/src/consts/doc.ts b/blocksuite/affine/model/src/consts/doc.ts index 22a5c11d8..84b214421 100644 --- a/blocksuite/affine/model/src/consts/doc.ts +++ b/blocksuite/affine/model/src/consts/doc.ts @@ -4,6 +4,10 @@ export type DocMode = 'edgeless' | 'page'; export const DocModes = ['edgeless', 'page'] as const; +export type FootNoteReferenceType = 'doc' | 'attachment' | 'url'; + +export const FootNoteReferenceTypes = ['doc', 'attachment', 'url'] as const; + /** * Custom title and description information. * @@ -42,3 +46,32 @@ export const ReferenceInfoSchema = z .merge(AliasInfoSchema); export type ReferenceInfo = z.infer; + +/** + * FootNoteReferenceParamsSchema is used to define the parameters for a footnote reference. + * It supports the following types: + * 1. docId: string - the id of the doc + * 2. blobId: string - the id of the attachment + * 3. url: string - the url of the reference + * 4. fileName: string - the name of the attachment + * 5. fileType: string - the type of the attachment + */ +export const FootNoteReferenceParamsSchema = z.object({ + type: z.enum(FootNoteReferenceTypes), + docId: z.string().optional(), + blobId: z.string().optional(), + fileName: z.string().optional(), + fileType: z.string().optional(), + url: z.string().optional(), +}); + +export type FootNoteReferenceParams = z.infer< + typeof FootNoteReferenceParamsSchema +>; + +export const FootNoteSchema = z.object({ + label: z.string(), + reference: FootNoteReferenceParamsSchema, +}); + +export type FootNote = z.infer; diff --git a/blocksuite/affine/shared/src/types/index.ts b/blocksuite/affine/shared/src/types/index.ts index aa33d3d81..3f6976562 100644 --- a/blocksuite/affine/shared/src/types/index.ts +++ b/blocksuite/affine/shared/src/types/index.ts @@ -1,4 +1,8 @@ -import type { EmbedCardStyle, ReferenceInfo } from '@blocksuite/affine-model'; +import type { + EmbedCardStyle, + FootNote, + ReferenceInfo, +} from '@blocksuite/affine-model'; import type { BlockComponent } from '@blocksuite/block-std'; import type { BlockModel } from '@blocksuite/store'; @@ -70,4 +74,5 @@ export interface AffineTextAttributes { background?: string | null; color?: string | null; latex?: string | null; + footnote?: FootNote | null; }