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 -->
178 lines
4.9 KiB
TypeScript
178 lines
4.9 KiB
TypeScript
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;
|
|
}
|
|
}
|