feat(editor): add experimental feature adapter panel to AFFiNE canary (#12489)
Closes: [BS-2539](https://linear.app/affine-design/issue/BS-2539/为-affine-添加-ef,并且支持在-affine-预览对应的功能) > [!warning] > This feature is only available in the canary build and is intended for debugging purposes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced an "Adapter Panel" feature with a new sidebar tab for previewing document content in multiple formats (Markdown, PlainText, HTML, Snapshot), controllable via a feature flag. - Added a fully integrated adapter panel component with reactive UI elements for selecting adapters, toggling HTML preview modes, and updating content. - Provided a customizable adapter panel for both main app and playground environments, supporting content transformation pipelines and export previews. - Enabled seamless toggling and live updating of adapter panel content through intuitive menus and controls. - **Localization** - Added English translations and descriptive settings for the Adapter Panel feature. - **Chores** - Added new package and workspace dependencies along with TypeScript project references to support the Adapter Panel modules and components. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
177
blocksuite/affine/fragments/adapter-panel/src/adapter-panel.ts
Normal file
177
blocksuite/affine/fragments/adapter-panel/src/adapter-panel.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { Store, TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import {
|
||||
type HtmlAdapter,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
type MarkdownAdapter,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
type PlainTextAdapter,
|
||||
PlainTextAdapterFactoryIdentifier,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { provide } from '@lit/context';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from './config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL = 'affine-adapter-panel';
|
||||
|
||||
export class AdapterPanel extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.adapters-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private _createJob() {
|
||||
return this.store.getTransformer(this.transformerMiddlewares);
|
||||
}
|
||||
|
||||
private _getDocSnapshot() {
|
||||
const job = this._createJob();
|
||||
const result = job.docToSnapshot(this.store);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getHtmlContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const htmlAdapterFactory = this.store.get(HtmlAdapterFactoryIdentifier);
|
||||
const htmlAdapter = htmlAdapterFactory.get(job) as HtmlAdapter;
|
||||
const result = await htmlAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get html content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async _getMarkdownContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const markdownAdapterFactory = this.store.get(
|
||||
MarkdownAdapterFactoryIdentifier
|
||||
);
|
||||
const markdownAdapter = markdownAdapterFactory.get(
|
||||
job
|
||||
) as MarkdownAdapter;
|
||||
const result = await markdownAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get markdown content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async _getPlainTextContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const plainTextAdapterFactory = this.store.get(
|
||||
PlainTextAdapterFactoryIdentifier
|
||||
);
|
||||
const plainTextAdapter = plainTextAdapterFactory.get(
|
||||
job
|
||||
) as PlainTextAdapter;
|
||||
const result = await plainTextAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get plain text content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _updateActiveContent = async () => {
|
||||
const activeId = this.activeAdapter.id;
|
||||
switch (activeId) {
|
||||
case 'markdown':
|
||||
this._context.markdownContent$.value =
|
||||
(await this._getMarkdownContent()) || '';
|
||||
break;
|
||||
case 'html':
|
||||
this._context.htmlContent$.value = (await this._getHtmlContent()) || '';
|
||||
break;
|
||||
case 'plaintext':
|
||||
this._context.plainTextContent$.value =
|
||||
(await this._getPlainTextContent()) || '';
|
||||
break;
|
||||
case 'snapshot':
|
||||
this._context.docSnapshot$.value = this._getDocSnapshot() || null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._context = {
|
||||
activeAdapter$: signal(ADAPTERS[0]),
|
||||
isHtmlPreview$: signal(false),
|
||||
docSnapshot$: signal(null),
|
||||
htmlContent$: signal(''),
|
||||
markdownContent$: signal(''),
|
||||
plainTextContent$: signal(''),
|
||||
};
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has('store')) {
|
||||
this._updateActiveContent().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
if (this.activeAdapter) {
|
||||
this._updateActiveContent().catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapters-container">
|
||||
<affine-adapter-panel-header
|
||||
.updateActiveContent=${this._updateActiveContent}
|
||||
></affine-adapter-panel-header>
|
||||
<affine-adapter-panel-body></affine-adapter-panel-body>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor store!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor transformerMiddlewares: TransformerMiddleware[] = [];
|
||||
|
||||
@provide({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL]: AdapterPanel;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user