feat(editor): add affine inline footnote (#9745)

[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<FootNoteNodeConfigProvider>('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`<affine-footnote-node
          .delta=${delta}
          .std=${std}
          .config=${config}
        ></affine-footnote-node>`;
      },
      embed: true,
    };
  }
);
```
This commit is contained in:
donteatfriedrice
2025-01-17 09:38:42 +00:00
parent 7d1d167858
commit df910d7013
10 changed files with 558 additions and 3 deletions

View File

@@ -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<typeof ReferenceInfoSchema>;
/**
* 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<typeof FootNoteSchema>;