Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help with custom blocks in v0.17 #8788

Closed
KehindeWilliams opened this issue Nov 22, 2024 · 2 comments
Closed

Help with custom blocks in v0.17 #8788

KehindeWilliams opened this issue Nov 22, 2024 · 2 comments

Comments

@KehindeWilliams
Copy link

KehindeWilliams commented Nov 22, 2024

Hi there,

We've been using Blocksuite v0.12 in our product to display markdown content, and we've also created custom blocks to extend its features.

We've encountered some issues as we're migrating to the latest version(v0.17).
Although blocksuite.io provides documentation for defining custom and embed blocks, the instructions don't seem to work with v0.17, and I urgently need assistance.

Below is an example of how we implement this in v0.12. How do we adapt this for v0.17? If you could provide a sample code or guidance, it would be greatly appreciated.

import { createRoot } from 'react-dom/client';
import { html } from 'lit';
import { literal } from 'lit/static-html.js';
import { useEffect, useRef, useState } from 'react';
import { EmbedContainer } from '../EmbedContainer';
import { EmbedBlockComponent, defineEmbedModel } from '@blocksuite/blocks';
import { BlockModel } from '@blocksuite/store';

interface ChemistryComponentProps {
    model: EmbedChemistryModel,
    moleCule: string;
    element: EmbedBlockElement;
    setMoleCule: (val: string) => void;
}

declare global {
    interface Window {
        ketcher: any;
    }
}

const ChemistryComponent: React.FC<ChemistryComponentProps> = ({
    model, moleCule, setMoleCule, element
}) => {
    const [ketcher, setKetcher] = useState(null);
    const calculatorRef = useRef<HTMLIFrameElement>();
    const [loading, setLoading] = useState(true);

    const onDelete = () => {
        model.doc.deleteBlock(model);
    }

    const onExport = async () => {
        if (!ketcher) return;
        const blob = await ketcher.generateImage(await ketcher.getKet(), { outputFormat: 'png', backgroundColor: '255, 255, 255' });
        return { blob, name: `chemistry-${model.id}.png` }
    }

    useEffect(() => {
        if (ketcher && moleCule) {
            ketcher.setMolecule(moleCule);
        }
        const timer = setInterval(async () => {
            if (ketcher) {
                const data = await ketcher.getMolfile();
                setMoleCule(data);
            } else {
                setKetcher(calculatorRef.current.contentWindow.ketcher);
            }
        }, 1000);
        return () => {
            clearInterval(timer);
        }
    }, [ketcher])

    return (
        <EmbedContainer element={element} loading={loading} onDelete={onDelete} onExport={onExport} aspect='aspect-video'>
            <iframe src="/ketcher/index.html"
                ref={calculatorRef}
                className='w-full h-full'
                onLoad={() => {
                    setLoading(false);
                }} />
        </EmbedContainer>
    )
};


export class EmbedChemistryModel extends defineEmbedModel<{
    blockType: boolean;
    moleCule: string;
}>(BlockModel) { }

class EmbedChemistryBlock extends EmbedBlockComponent<EmbedChemistryModel> {
    static properties = {
        reactRoot: {
            type: Object
        }
    }

    accessor reactRoot: any = null;

    firstUpdated() {
        this.reactRoot = createRoot(this.querySelector('#chemistry-root'));
        this.reactRoot.render(
            <ChemistryComponent element={this} model={this.model}
                moleCule={this.model.moleCule}
                setMoleCule={val => { this.model.moleCule = val; }} />);
    }

    override disconnectedCallback() {
        this.reactRoot.unmount();
        super.disconnectedCallback();
    }
    override render() {
        return html`<div id="chemistry-root"></div>`
    }
}

customElements.define('affine-chemistry-block', EmbedChemistryBlock);

declare global {
    interface HTMLElementTagNameMap {
        'affine-chemistry-block': EmbedChemistryBlock;
    }
}

export const EmbedChemistryBlockSpec = createEmbedBlock({
    schema: {
        name: 'chemistry',
        version: 1,
        toModel: () => new EmbedChemistryModel(),
        props: () => ({
            moleCule: '',
        }),
    },
    view: {
        component: literal`affine-chemistry-block`,
    },
});

I appreciate your support and look forward to hearing from you.

Thanks,
Kehinde

@L-Sun
Copy link
Contributor

L-Sun commented Nov 22, 2024

There are some utility functions for creating a embed block. Here, we provide a simple example to show how to use them based your code.

import {
  BlockViewExtension,
  type ExtensionType,
  FlavourExtension,
} from '@blocksuite/affine/block-std';
import {
  createEmbedBlockSchema,
  defineEmbedModel,
  EmbedBlockComponent,
  toEdgelessEmbedBlock,
} from '@blocksuite/affine/blocks';
import { BlockModel } from '@blocksuite/affine/store';
import { literal } from 'lit/static-html.js';

type EmbedChemistryBlockProps = {
  moleCule: string;
};

export const EmbedChemistryBlockSchema = createEmbedBlockSchema({
  name: 'chemistry',
  version: 1,
  toModel: () => new EmbedChemistryModel(),
  props: (): EmbedChemistryBlockProps => ({ moleCule: '' }),
});

class EmbedChemistryModel extends defineEmbedModel<EmbedChemistryBlockProps>(
  BlockModel
) {}

class EmbedChemistryComponent extends EmbedBlockComponent<EmbedChemistryModel> {
  // Render logic here
}

// this is used to render the block in edgeless mode
export class EmbedEdgelessBlockComponent extends toEdgelessEmbedBlock(
  EmbedChemistryComponent
) {}

// create spec for the block
export const EmbedChemistryBlockSpec: ExtensionType[] = [
  FlavourExtension('embed-chemistry'),
  BlockViewExtension('embed-chemistry', model => {
    return model.parent?.flavour === 'affine:surface'
      ? literal`embed-chemistry-block`
      : literal`embed-edgeless-chemistry-block`;
  }),
];

// call this side-effect function, which will define web components
export function chemistryBlockEffects() {
  customElements.define('embed-chemistry-block', EmbedChemistryComponent);
  customElements.define(
    'embed-edgeless-chemistry-block',
    EmbedEdgelessBlockComponent
  );
}


declare global {
  interface HTMLElementTagNameMap {
    'embed-chemistry-block': EmbedChemistryComponent;
    'embed-edgeless-chemistry-block': EmbedEdgelessBlockComponent;
  }

  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace BlockSuite {
    interface EdgelessBlockModelMap {
      'embed-chemistry': EmbedChemistryModel;
    }
    interface BlockModels {
      'embed-chemistry': EmbedChemistryModel;
    }
  }
}



function main() {
  // In your editor setup stage
  // ...

  schema.register([EmbedChemistryBlockSchema]);
  editor.pageSpecs.push(...EmbedChemistryBlockSpec);
  editor.edgelessSpecs.push(...EmbedChemistryBlockSpec);

  // ...
}

@KehindeWilliams
Copy link
Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

No branches or pull requests

2 participants