mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-23 01:07:12 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
285
blocksuite/playground/apps/_common/components/adapters-panel.ts
Normal file
285
blocksuite/playground/apps/_common/components/adapters-panel.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
|
||||
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddlewareBuilder,
|
||||
embedSyncedDocMiddleware,
|
||||
type HtmlAdapter,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
type MarkdownAdapter,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
type PlainTextAdapter,
|
||||
PlainTextAdapterFactoryIdentifier,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/blocks';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { type DocSnapshot, Job } from '@blocksuite/store';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import type SlTabPanel from '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('adapters-panel')
|
||||
export class AdaptersPanel extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
adapters-panel {
|
||||
width: 36vw;
|
||||
}
|
||||
.adapters-container {
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.adapter-container {
|
||||
padding: 0px 16px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
white-space: pre-wrap;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.update-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
}
|
||||
.update-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
.html-panel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.html-preview-container,
|
||||
.html-panel-content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.html-panel-footer {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
line-height: 20px;
|
||||
}
|
||||
span[active] {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
get doc() {
|
||||
return this.editor.doc;
|
||||
}
|
||||
|
||||
private _createJob() {
|
||||
return new Job({
|
||||
collection: this.doc.collection,
|
||||
middlewares: [
|
||||
docLinkBaseURLMiddlewareBuilder('https://example.com').get(),
|
||||
titleMiddleware,
|
||||
embedSyncedDocMiddleware('content'),
|
||||
defaultImageProxyMiddleware,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private _getDocSnapshot() {
|
||||
const job = this._createJob();
|
||||
const result = job.docToSnapshot(this.doc);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getHtmlContent() {
|
||||
const job = this._createJob();
|
||||
const htmlAdapterFactory = this.editor.std.provider.get(
|
||||
HtmlAdapterFactoryIdentifier
|
||||
);
|
||||
const htmlAdapter = htmlAdapterFactory.get(job) as HtmlAdapter;
|
||||
const result = await htmlAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _getMarkdownContent() {
|
||||
const job = this._createJob();
|
||||
const markdownAdapterFactory = this.editor.std.provider.get(
|
||||
MarkdownAdapterFactoryIdentifier
|
||||
);
|
||||
const markdownAdapter = markdownAdapterFactory.get(job) as MarkdownAdapter;
|
||||
const result = await markdownAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _getPlainTextContent() {
|
||||
const job = this._createJob();
|
||||
const plainTextAdapterFactory = this.editor.std.provider.get(
|
||||
PlainTextAdapterFactoryIdentifier
|
||||
);
|
||||
const plainTextAdapter = plainTextAdapterFactory.get(
|
||||
job
|
||||
) as PlainTextAdapter;
|
||||
const result = await plainTextAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _handleTabShow(name: string) {
|
||||
switch (name) {
|
||||
case 'markdown':
|
||||
this._markdownContent = (await this._getMarkdownContent()) || '';
|
||||
break;
|
||||
case 'html':
|
||||
this._htmlContent = (await this._getHtmlContent()) || '';
|
||||
break;
|
||||
case 'plaintext':
|
||||
this._plainTextContent = (await this._getPlainTextContent()) || '';
|
||||
break;
|
||||
case 'snapshot':
|
||||
this._docSnapshot = this._getDocSnapshot() || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHtmlPanel() {
|
||||
return html`
|
||||
${this._isHtmlPreview
|
||||
? html`<iframe
|
||||
class="html-preview-container"
|
||||
.srcdoc=${this._htmlContent}
|
||||
></iframe>`
|
||||
: html`<div class="html-panel-content">${this._htmlContent}</div>`}
|
||||
<div class="html-panel-footer">
|
||||
<span
|
||||
class="html-panel-footer-item"
|
||||
?active=${!this._isHtmlPreview}
|
||||
@click=${() => (this._isHtmlPreview = false)}
|
||||
>Source</span
|
||||
>
|
||||
<span
|
||||
class="html-panel-footer-item"
|
||||
?active=${this._isHtmlPreview}
|
||||
@click=${() => (this._isHtmlPreview = true)}
|
||||
>Preview</span
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _updateActiveTabContent() {
|
||||
if (!this._activeTab) return;
|
||||
const activeTabName = this._activeTab.name;
|
||||
await this._handleTabShow(activeTabName);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const doc = this.doc;
|
||||
if (doc) {
|
||||
this._updateActiveTabContent().catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const snapshotString = this._docSnapshot
|
||||
? JSON.stringify(this._docSnapshot, null, 4)
|
||||
: '';
|
||||
return html`
|
||||
<div class="adapters-container">
|
||||
<sl-tab-group
|
||||
activation="auto"
|
||||
@sl-tab-show=${(e: CustomEvent) => this._handleTabShow(e.detail.name)}
|
||||
>
|
||||
<sl-tab slot="nav" panel="markdown">Markdown</sl-tab>
|
||||
<sl-tab slot="nav" panel="plaintext">PlainText</sl-tab>
|
||||
<sl-tab slot="nav" panel="html">HTML</sl-tab>
|
||||
<sl-tab slot="nav" panel="snapshot">Snapshot</sl-tab>
|
||||
|
||||
<sl-tab-panel name="markdown">
|
||||
<div class="adapter-container">${this._markdownContent}</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="html">
|
||||
<div class="adapter-container html-panel">
|
||||
${this._renderHtmlPanel()}
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="plaintext">
|
||||
<div class="adapter-container">${this._plainTextContent}</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="snapshot">
|
||||
<div class="adapter-container">${snapshotString}</div>
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
<sl-tooltip content="Update Adapter Content" placement="left" hoist>
|
||||
<div class="update-button" @click="${this._updateActiveTabContent}">
|
||||
Update
|
||||
</div>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override willUpdate(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('editor')) {
|
||||
requestIdleCallback(() => {
|
||||
this._updateActiveTabContent().catch(console.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@query('sl-tab-panel[active]')
|
||||
private accessor _activeTab!: SlTabPanel;
|
||||
|
||||
@state()
|
||||
private accessor _docSnapshot: DocSnapshot | null = null;
|
||||
|
||||
@state()
|
||||
private accessor _htmlContent = '';
|
||||
|
||||
@state()
|
||||
private accessor _isHtmlPreview = false;
|
||||
|
||||
@state()
|
||||
private accessor _markdownContent = '';
|
||||
|
||||
@state()
|
||||
private accessor _plainTextContent = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: AffineEditorContainer;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'adapters-panel': AdaptersPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
import { getAttachmentFileIcons } from '@blocksuite/blocks';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowDownBigIcon,
|
||||
ArrowUpBigIcon,
|
||||
CloseIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { css, html, LitElement, type TemplateResult } from 'lit';
|
||||
import { customElement, query } from 'lit/decorators.js';
|
||||
|
||||
import type { DocInfo, MessageData, MessageDataType } from './pdf/types.js';
|
||||
import { MessageOp, RenderKind, State } from './pdf/types.js';
|
||||
|
||||
const DPI = window.devicePixelRatio;
|
||||
|
||||
type FileInfo = {
|
||||
name: string;
|
||||
size: string;
|
||||
isPDF: boolean;
|
||||
icon: TemplateResult;
|
||||
};
|
||||
|
||||
@customElement('attachment-viewer-panel')
|
||||
export class AttachmentViewerPanel extends SignalWatcher(
|
||||
WithDisposable(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
dialog {
|
||||
padding: 0;
|
||||
top: 50px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--affine-v2-dialog-background-primary);
|
||||
box-shadow: var(--affine-overlay-shadow);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 700px;
|
||||
height: 900px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
& > .close {
|
||||
user-select: none;
|
||||
outline: none;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
header,
|
||||
footer {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
h5 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin: 0;
|
||||
|
||||
.file-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
|
||||
.page {
|
||||
width: calc(100% - 40px);
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
#cursor = signal<number>(0);
|
||||
|
||||
#docInfo = signal<DocInfo | null>(null);
|
||||
|
||||
#fileInfo = signal<FileInfo | null>(null);
|
||||
|
||||
#state = signal<State>(State.Connecting);
|
||||
|
||||
#worker: Worker | null = null;
|
||||
|
||||
clear = () => {
|
||||
this.#dialog.close();
|
||||
|
||||
this.#state.value = State.IDLE;
|
||||
this.#worker?.terminate();
|
||||
this.#worker = null;
|
||||
|
||||
this.#fileInfo.value = null;
|
||||
this.#docInfo.value = null;
|
||||
this.#cursor.value = 0;
|
||||
|
||||
const canvas = this.#page;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
goto(at: number) {
|
||||
this.#cursor.value = at;
|
||||
this.post(MessageOp.Render, {
|
||||
index: at,
|
||||
scale: 1 * DPI,
|
||||
kind: RenderKind.Page,
|
||||
});
|
||||
}
|
||||
|
||||
open(model: AttachmentBlockModel) {
|
||||
this.#dialog.showModal();
|
||||
|
||||
const { name, size } = model;
|
||||
|
||||
const fileType = name.split('.').pop() ?? '';
|
||||
const icon = getAttachmentFileIcons(fileType);
|
||||
const isPDF = fileType === 'pdf';
|
||||
|
||||
this.#fileInfo.value = {
|
||||
name,
|
||||
icon,
|
||||
isPDF,
|
||||
size: humanFileSize(size),
|
||||
};
|
||||
|
||||
if (!isPDF) return;
|
||||
if (!model.sourceId) return;
|
||||
if (this.#worker) return;
|
||||
|
||||
const process = async ({ data }: MessageEvent<MessageData>) => {
|
||||
const { type } = data;
|
||||
|
||||
switch (type) {
|
||||
case MessageOp.Init: {
|
||||
console.debug('connecting');
|
||||
this.#state.value = State.Connecting;
|
||||
break;
|
||||
}
|
||||
|
||||
case MessageOp.Inited: {
|
||||
console.debug('connected');
|
||||
this.#state.value = State.Connected;
|
||||
|
||||
const blob = await model.doc.blobSync.get(model.sourceId!);
|
||||
|
||||
if (!blob) return;
|
||||
const buffer = await blob.arrayBuffer();
|
||||
|
||||
this.post(MessageOp.Open, buffer, [buffer]);
|
||||
break;
|
||||
}
|
||||
|
||||
case MessageOp.Opened: {
|
||||
const info = data[type];
|
||||
this.#cursor.value = 0;
|
||||
this.#docInfo.value = info;
|
||||
this.#state.value = State.Opened;
|
||||
this.post(MessageOp.Render, {
|
||||
index: 0,
|
||||
scale: 1 * DPI,
|
||||
kind: RenderKind.Page,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case MessageOp.Rendered: {
|
||||
const { index, kind, imageData } = data[type];
|
||||
|
||||
if (index !== this.#cursor.value) return;
|
||||
|
||||
const canvas = this.#page;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
console.debug('render page', index, kind);
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
|
||||
ctx.clearRect(0, 0, imageData.width, imageData.height);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.#worker = new Worker(new URL('./pdf/worker.ts', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
this.#worker.addEventListener('message', event => {
|
||||
process(event).catch(console.error);
|
||||
});
|
||||
}
|
||||
|
||||
post<T extends MessageOp>(
|
||||
type: T,
|
||||
data?: MessageDataType[T],
|
||||
transfers?: Transferable[]
|
||||
) {
|
||||
if (!this.#worker) return;
|
||||
|
||||
const message = { type, [type]: data };
|
||||
if (transfers?.length) {
|
||||
this.#worker?.postMessage(message, transfers);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#worker?.postMessage(message);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const fileInfo = this.#fileInfo.value;
|
||||
const isPDF = fileInfo?.isPDF ?? false;
|
||||
const docInfo = this.#docInfo.value;
|
||||
const cursor = this.#cursor.value;
|
||||
const total = docInfo ? docInfo.total : 0;
|
||||
const width = docInfo ? docInfo.width : 0;
|
||||
const height = docInfo ? docInfo.height : 0;
|
||||
const isEmpty = total === 0;
|
||||
const print = (n: number) => (isEmpty ? '-' : n);
|
||||
|
||||
return html`
|
||||
<dialog>
|
||||
<div class="dialog">
|
||||
<header>
|
||||
<h5>
|
||||
<span>${fileInfo?.name}</span>
|
||||
<span>${fileInfo?.size}</span>
|
||||
<span class="file-icon">${fileInfo?.icon}</span>
|
||||
</h5>
|
||||
</header>
|
||||
<main class="body">
|
||||
${isPDF
|
||||
? html`<canvas class="page"></canvas>`
|
||||
: html`<p class="error">This file format is not supported.</p>`}
|
||||
<div class="controls">
|
||||
<icon-button
|
||||
.disabled=${isEmpty || cursor === 0}
|
||||
@click=${() => this.goto(cursor - 1)}
|
||||
>${ArrowUpBigIcon()}</icon-button
|
||||
>
|
||||
<icon-button
|
||||
.disabled=${isEmpty || cursor + 1 === total}
|
||||
@click=${() => this.goto(cursor + 1)}
|
||||
>${ArrowDownBigIcon()}</icon-button
|
||||
>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<span>${print(width)}</span>
|
||||
x
|
||||
<span>${print(height)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>${print(cursor + 1)}</span>
|
||||
/
|
||||
<span>${print(total)}</span>
|
||||
</div>
|
||||
</footer>
|
||||
<icon-button class="close" @click=${this.clear}
|
||||
>${CloseIcon()}</icon-button
|
||||
>
|
||||
</div>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('dialog')
|
||||
accessor #dialog!: HTMLDialogElement;
|
||||
|
||||
@query('.page')
|
||||
accessor #page: HTMLCanvasElement | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'attachment-viewer-panel': AttachmentViewerPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||
import '@shoelace-style/shoelace/dist/components/alert/alert.js';
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js';
|
||||
import '@shoelace-style/shoelace/dist/components/button-group/button-group.js';
|
||||
import '@shoelace-style/shoelace/dist/components/color-picker/color-picker.js';
|
||||
import '@shoelace-style/shoelace/dist/components/divider/divider.js';
|
||||
import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js';
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
|
||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js';
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
||||
import '@shoelace-style/shoelace/dist/components/menu/menu.js';
|
||||
import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js';
|
||||
import '@shoelace-style/shoelace/dist/components/select/select.js';
|
||||
import '@shoelace-style/shoelace/dist/components/tab/tab.js';
|
||||
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js';
|
||||
import '@shoelace-style/shoelace/dist/components/tooltip/tooltip.js';
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import '@shoelace-style/shoelace/dist/themes/dark.css';
|
||||
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import {
|
||||
ColorScheme,
|
||||
type DocMode,
|
||||
DocModeProvider,
|
||||
EdgelessRootService,
|
||||
ExportManager,
|
||||
printToPdf,
|
||||
} from '@blocksuite/blocks';
|
||||
import { type SerializedXYWH, SignalWatcher } from '@blocksuite/global/utils';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { type DocCollection, Text } from '@blocksuite/store';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
import { notify } from '../../default/utils/notify.js';
|
||||
import { mockEdgelessTheme } from '../mock-services.js';
|
||||
import { generateRoomId } from '../sync/websocket/utils.js';
|
||||
import type { DocsPanel } from './docs-panel.js';
|
||||
import type { LeftSidePanel } from './left-side-panel.js';
|
||||
|
||||
const basePath =
|
||||
'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.11.2/dist/';
|
||||
setBasePath(basePath);
|
||||
|
||||
@customElement('collab-debug-menu')
|
||||
export class CollabDebugMenu extends SignalWatcher(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
:root {
|
||||
--sl-font-size-medium: var(--affine-font-xs);
|
||||
--sl-input-font-size-small: var(--affine-font-xs);
|
||||
}
|
||||
|
||||
.dg.ac {
|
||||
z-index: 1001 !important;
|
||||
}
|
||||
|
||||
.top-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
private _darkModeChange = (e: MediaQueryListEvent) => {
|
||||
this._setThemeMode(!!e.matches);
|
||||
};
|
||||
|
||||
private _handleDocsPanelClose = () => {
|
||||
this.leftSidePanel.toggle(this.docsPanel);
|
||||
};
|
||||
|
||||
private _keydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F1') {
|
||||
this._switchEditorMode();
|
||||
}
|
||||
};
|
||||
|
||||
private _startCollaboration = async () => {
|
||||
if (window.wsProvider) {
|
||||
notify('There is already a websocket provider exists', 'neutral').catch(
|
||||
console.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('room') || (await generateRoomId());
|
||||
|
||||
params.set('room', id);
|
||||
const url = new URL(location.href);
|
||||
url.search = params.toString();
|
||||
location.href = url.href;
|
||||
};
|
||||
|
||||
get doc() {
|
||||
return this.editor.doc;
|
||||
}
|
||||
|
||||
get editorMode() {
|
||||
return this.editor.mode;
|
||||
}
|
||||
|
||||
set editorMode(value: DocMode) {
|
||||
this.editor.mode = value;
|
||||
}
|
||||
|
||||
get rootService() {
|
||||
try {
|
||||
return this.editor.std.getService('affine:page');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _addNote() {
|
||||
const rootModel = this.doc.root;
|
||||
if (!rootModel) return;
|
||||
const rootId = rootModel.id;
|
||||
|
||||
this.doc.captureSync();
|
||||
|
||||
const count = rootModel.children.length;
|
||||
const xywh: SerializedXYWH = `[0,${count * 60},800,95]`;
|
||||
|
||||
const noteId = this.doc.addBlock('affine:note', { xywh }, rootId);
|
||||
this.doc.addBlock('affine:paragraph', {}, noteId);
|
||||
}
|
||||
|
||||
private async _clearSiteData() {
|
||||
await fetch('/Clear-Site-Data');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
private _exportHtml() {
|
||||
const htmlTransformer = this.rootService?.transformers.html;
|
||||
htmlTransformer?.exportDoc(this.doc).catch(console.error);
|
||||
}
|
||||
|
||||
private _exportMarkDown() {
|
||||
const markdownTransformer = this.rootService?.transformers.markdown;
|
||||
markdownTransformer?.exportDoc(this.doc).catch(console.error);
|
||||
}
|
||||
|
||||
private _exportPdf() {
|
||||
this.editor.std.get(ExportManager).exportPdf().catch(console.error);
|
||||
}
|
||||
|
||||
private _exportPng() {
|
||||
this.editor.std.get(ExportManager).exportPng().catch(console.error);
|
||||
}
|
||||
|
||||
private async _exportSnapshot() {
|
||||
if (!this.rootService) return;
|
||||
const zipTransformer = this.rootService.transformers.zip;
|
||||
await zipTransformer.exportDocs(
|
||||
this.collection,
|
||||
[...this.collection.docs.values()].map(collection => collection.getDoc())
|
||||
);
|
||||
}
|
||||
|
||||
private _importSnapshot() {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', '.zip');
|
||||
input.multiple = false;
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.item(0);
|
||||
if (!file) return;
|
||||
if (!this.rootService) return;
|
||||
try {
|
||||
const zipTransformer = this.rootService.transformers.zip;
|
||||
const docs = await zipTransformer.importDocs(this.collection, file);
|
||||
for (const doc of docs) {
|
||||
let noteBlockId;
|
||||
const noteBlocks = window.doc.getBlocksByFlavour('affine:note');
|
||||
if (noteBlocks.length) {
|
||||
noteBlockId = noteBlocks[0].id;
|
||||
} else {
|
||||
noteBlockId = this.doc.addBlock(
|
||||
'affine:note',
|
||||
{
|
||||
xywh: '[-200,-48,400,96]',
|
||||
},
|
||||
this.doc.root?.id
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
break;
|
||||
}
|
||||
|
||||
window.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
type: 'text',
|
||||
text: new Text([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: doc.id,
|
||||
},
|
||||
},
|
||||
} as DeltaInsert<AffineTextAttributes>,
|
||||
]),
|
||||
},
|
||||
noteBlockId
|
||||
);
|
||||
}
|
||||
this.requestUpdate();
|
||||
} catch (e) {
|
||||
console.error('Invalid snapshot.');
|
||||
console.error(e);
|
||||
} finally {
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
private _insertTransitionStyle(classKey: string, duration: number) {
|
||||
const $html = document.documentElement;
|
||||
const $style = document.createElement('style');
|
||||
const slCSSKeys = ['sl-transition-x-fast'];
|
||||
$style.innerHTML = `html.${classKey} * { transition: all ${duration}ms 0ms linear !important; } :root { ${slCSSKeys.map(
|
||||
key => `--${key}: ${duration}ms`
|
||||
)} }`;
|
||||
|
||||
$html.append($style);
|
||||
$html.classList.add(classKey);
|
||||
|
||||
setTimeout(() => {
|
||||
$style.remove();
|
||||
$html.classList.remove(classKey);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
private _print() {
|
||||
printToPdf().catch(console.error);
|
||||
}
|
||||
|
||||
private _setThemeMode(dark: boolean) {
|
||||
const html = document.querySelector('html');
|
||||
|
||||
this._dark = dark;
|
||||
localStorage.setItem('blocksuite:dark', dark ? 'true' : 'false');
|
||||
if (!html) return;
|
||||
html.dataset.theme = dark ? 'dark' : 'light';
|
||||
|
||||
this._insertTransitionStyle('color-transition', 0);
|
||||
|
||||
if (dark) {
|
||||
html.classList.add('dark');
|
||||
html.classList.add('sl-theme-dark');
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
html.classList.remove('sl-theme-dark');
|
||||
}
|
||||
|
||||
const theme = dark ? ColorScheme.Dark : ColorScheme.Light;
|
||||
mockEdgelessTheme.setTheme(theme);
|
||||
}
|
||||
|
||||
private _switchEditorMode() {
|
||||
if (!this.editor.host) return;
|
||||
const newMode = this._docMode === 'page' ? 'edgeless' : 'page';
|
||||
const docModeService = this.editor.host.std.get(DocModeProvider);
|
||||
if (docModeService) {
|
||||
docModeService.setPrimaryMode(newMode, this.editor.doc.id);
|
||||
}
|
||||
this._docMode = newMode;
|
||||
this.editor.mode = newMode;
|
||||
}
|
||||
|
||||
private _toggleDarkMode() {
|
||||
this._setThemeMode(!this._dark);
|
||||
}
|
||||
|
||||
private _toggleDocsPanel() {
|
||||
this.docsPanel.onClose = this._handleDocsPanelClose;
|
||||
this.leftSidePanel.toggle(this.docsPanel);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._docMode = this.editor.mode;
|
||||
this.editor.slots.docUpdated.on(({ newDocId }) => {
|
||||
const newDocMode = this.editor.std
|
||||
.get(DocModeProvider)
|
||||
.getPrimaryMode(newDocId);
|
||||
this._docMode = newDocMode;
|
||||
});
|
||||
|
||||
document.body.addEventListener('keydown', this._keydown);
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this._setThemeMode(this._dark && matchMedia.matches);
|
||||
matchMedia.addEventListener('change', this._darkModeChange);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
matchMedia.removeEventListener('change', this._darkModeChange);
|
||||
document.body.removeEventListener('keydown', this._keydown);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.doc.slots.historyUpdated.on(() => {
|
||||
this._canUndo = this.doc.canUndo;
|
||||
this._canRedo = this.doc.canRedo;
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<style>
|
||||
.collab-debug-menu {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
z-index: 1000; /* for debug visibility */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.collab-debug-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.default-toolbar {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 8px 8px 8px 16px;
|
||||
width: 100%;
|
||||
min-width: 390px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.default-toolbar sl-button.dots-menu::part(base) {
|
||||
color: var(--sl-color-neutral-700);
|
||||
}
|
||||
|
||||
.default-toolbar sl-button.dots-menu::part(label) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.default-toolbar > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.edgeless-toolbar {
|
||||
align-items: center;
|
||||
margin-right: 17px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.edgeless-toolbar sl-select,
|
||||
.edgeless-toolbar sl-color-picker,
|
||||
.edgeless-toolbar sl-button {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
<div class="collab-debug-menu default">
|
||||
<div class="default-toolbar">
|
||||
<div class="top-container">
|
||||
<sl-dropdown placement="bottom" hoist>
|
||||
<sl-button
|
||||
class="dots-menu"
|
||||
variant="text"
|
||||
size="small"
|
||||
slot="trigger"
|
||||
>
|
||||
<sl-icon
|
||||
style="font-size: 14px"
|
||||
name="three-dots-vertical"
|
||||
label="Menu"
|
||||
></sl-icon>
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>
|
||||
<sl-icon
|
||||
slot="prefix"
|
||||
name="terminal"
|
||||
label="Test operations"
|
||||
></sl-icon>
|
||||
<span>Test operations</span>
|
||||
<sl-menu slot="submenu">
|
||||
<sl-menu-item @click="${this._print}"> Print </sl-menu-item>
|
||||
<sl-menu-item @click=${this._addNote}>
|
||||
Add Note</sl-menu-item
|
||||
>
|
||||
<sl-menu-item @click=${this._exportMarkDown}>
|
||||
Export Markdown
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._exportHtml}>
|
||||
Export HTML
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._exportPdf}>
|
||||
Export PDF
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._exportPng}>
|
||||
Export PNG
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._exportSnapshot}>
|
||||
Export Snapshot
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._importSnapshot}>
|
||||
Import Snapshot
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._clearSiteData}>
|
||||
Clear Site Data
|
||||
<sl-icon slot="prefix" name="trash"></sl-icon>
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click=${this._toggleDarkMode}>
|
||||
Toggle ${this._dark ? 'Light' : 'Dark'} Mode
|
||||
<sl-icon
|
||||
slot="prefix"
|
||||
name=${this._dark ? 'moon' : 'brightness-high'}
|
||||
></sl-icon>
|
||||
</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/toeverything/blocksuite"
|
||||
>
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="github"></sl-icon>
|
||||
GitHub
|
||||
</sl-menu-item>
|
||||
</a>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
|
||||
<!-- undo/redo group -->
|
||||
<sl-button-group label="History">
|
||||
<!-- undo -->
|
||||
<sl-tooltip content="Undo" placement="bottom" hoist>
|
||||
<sl-button
|
||||
pill
|
||||
size="small"
|
||||
content="Undo"
|
||||
.disabled=${!this._canUndo}
|
||||
@click=${() => {
|
||||
this.doc.undo();
|
||||
}}
|
||||
>
|
||||
<sl-icon name="arrow-counterclockwise" label="Undo"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
<!-- redo -->
|
||||
<sl-tooltip content="Redo" placement="bottom" hoist>
|
||||
<sl-button
|
||||
pill
|
||||
size="small"
|
||||
content="Redo"
|
||||
.disabled=${!this._canRedo}
|
||||
@click=${() => {
|
||||
this.doc.redo();
|
||||
}}
|
||||
>
|
||||
<sl-icon name="arrow-clockwise" label="Redo"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
|
||||
<sl-tooltip content="Start Collaboration" placement="bottom" hoist>
|
||||
<sl-button @click=${this._startCollaboration} size="small" circle>
|
||||
<sl-icon name="people" label="Collaboration"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip content="Docs" placement="bottom" hoist>
|
||||
<sl-button
|
||||
@click=${this._toggleDocsPanel}
|
||||
size="small"
|
||||
circle
|
||||
data-docs-panel-toggle
|
||||
>
|
||||
<sl-icon name="filetype-doc" label="Doc"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
${new URLSearchParams(location.search).get('room')
|
||||
? html`<sl-input
|
||||
placeholder="Your name in room"
|
||||
clearable
|
||||
size="small"
|
||||
@blur=${(e: Event) => {
|
||||
if ((e.target as HTMLInputElement).value.length > 0) {
|
||||
this.collection.awarenessStore.awareness.setLocalStateField(
|
||||
'user',
|
||||
{
|
||||
name: (e.target as HTMLInputElement).value ?? '',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.collection.awarenessStore.awareness.setLocalStateField(
|
||||
'user',
|
||||
{
|
||||
name: 'Unknown',
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
></sl-input
|
||||
></sl-tooltip>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px">
|
||||
<!-- Edgeless Theme button -->
|
||||
${this._docMode === 'edgeless'
|
||||
? html`<sl-tooltip
|
||||
content="Edgeless Theme"
|
||||
placement="bottom"
|
||||
hoist
|
||||
>
|
||||
<sl-button
|
||||
size="small"
|
||||
circle
|
||||
@click=${() => mockEdgelessTheme.toggleTheme()}
|
||||
>
|
||||
<sl-icon
|
||||
name="${mockEdgelessTheme.theme$.value === 'dark'
|
||||
? 'moon'
|
||||
: 'brightness-high'}"
|
||||
label="Edgeless Theme"
|
||||
></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>`
|
||||
: nothing}
|
||||
<!-- Present button -->
|
||||
${this._docMode === 'edgeless'
|
||||
? html`<sl-tooltip content="Present" placement="bottom" hoist>
|
||||
<sl-button
|
||||
size="small"
|
||||
circle
|
||||
@click=${() => {
|
||||
if (this.rootService instanceof EdgelessRootService) {
|
||||
this.rootService.gfx.tool.setTool('frameNavigator', {
|
||||
mode: 'fit',
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<sl-icon name="easel" label="Present"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>`
|
||||
: nothing}
|
||||
<sl-button-group label="Mode" style="margin-right: 12px">
|
||||
<!-- switch to page -->
|
||||
<sl-tooltip content="Page" placement="bottom" hoist>
|
||||
<sl-button
|
||||
pill
|
||||
size="small"
|
||||
content="Page"
|
||||
.disabled=${this._docMode !== 'edgeless'}
|
||||
@click=${this._switchEditorMode}
|
||||
>
|
||||
<sl-icon name="filetype-doc" label="Page"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
<!-- switch to edgeless -->
|
||||
<sl-tooltip content="Edgeless" placement="bottom" hoist>
|
||||
<sl-button
|
||||
pill
|
||||
size="small"
|
||||
content="Edgeless"
|
||||
.disabled=${this._docMode !== 'page'}
|
||||
@click=${this._switchEditorMode}
|
||||
>
|
||||
<sl-icon name="palette" label="Edgeless"></sl-icon>
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _canRedo = false;
|
||||
|
||||
@state()
|
||||
private accessor _canUndo = false;
|
||||
|
||||
@state()
|
||||
private accessor _dark = localStorage.getItem('blocksuite:dark') === 'true';
|
||||
|
||||
@state()
|
||||
private accessor _docMode: DocMode = 'page';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor collection!: DocCollection;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor docsPanel!: DocsPanel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: AffineEditorContainer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor leftSidePanel!: LeftSidePanel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor readonly = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'collab-debug-menu': CollabDebugMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('custom-frame-panel')
|
||||
export class CustomFramePanel extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
.custom-frame-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
height: 100vh;
|
||||
width: 320px;
|
||||
box-sizing: border-box;
|
||||
padding-top: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
private _renderPanel() {
|
||||
return html`<affine-frame-panel
|
||||
.host=${this.editor.std.host}
|
||||
></affine-frame-panel>`;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const std = this.editor.std;
|
||||
if (std) {
|
||||
this.editor.updateComplete
|
||||
.then(() => this.requestUpdate())
|
||||
.catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${this._show
|
||||
? html`<div class="custom-frame-container">${this._renderPanel()}</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this._show = !this._show;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _show = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: AffineEditorContainer;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'custom-frame-panel': CustomFramePanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('custom-outline-panel')
|
||||
export class CustomOutlinePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.custom-outline-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 16px;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
height: 100vh;
|
||||
width: 320px;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
private _renderPanel() {
|
||||
return html`<affine-outline-panel
|
||||
.editor=${this.editor}
|
||||
.fitPadding=${[50, 360, 50, 50]}
|
||||
></affine-outline-panel>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${this._show
|
||||
? html`
|
||||
<div class="custom-outline-container">${this._renderPanel()}</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this._show = !this._show;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _show = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: AffineEditorContainer;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'custom-outline-panel': CustomOutlinePanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('custom-outline-viewer')
|
||||
export class CustomOutlineViewer extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.outline-viewer-container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 256px;
|
||||
right: 22px;
|
||||
max-height: calc(100vh - 256px - 76px); // top(256px) and bottom(76px)
|
||||
}
|
||||
`;
|
||||
|
||||
private _renderViewer() {
|
||||
return html`<affine-outline-viewer
|
||||
.editor=${this.editor}
|
||||
.toggleOutlinePanel=${this.toggleOutlinePanel}
|
||||
></affine-outline-viewer>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this._show || this.editor.mode === 'edgeless') return nothing;
|
||||
|
||||
return html`<div class="outline-viewer-container">
|
||||
${this._renderViewer()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this._show = !this._show;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _show = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: AffineEditorContainer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor toggleOutlinePanel: (() => void) | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'custom-outline-viewer': CustomOutlineViewer;
|
||||
}
|
||||
}
|
||||
137
blocksuite/playground/apps/_common/components/demo-script.ts
Normal file
137
blocksuite/playground/apps/_common/components/demo-script.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
export const demoScript = `import * as THREE from "three";
|
||||
import {OrbitControls} from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
|
||||
let scene = new THREE.Scene();
|
||||
let camera = new THREE.PerspectiveCamera(30, innerWidth / innerHeight, 1, 1000);
|
||||
camera.position.set(0, 10, 10).setLength(17);
|
||||
let renderer = new THREE.WebGLRenderer({antialias: true});
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
window.addEventListener("resize", event => {
|
||||
camera.aspect = innerWidth / innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
})
|
||||
|
||||
let controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
let gu = {
|
||||
time: {value: 0}
|
||||
}
|
||||
|
||||
let params = {
|
||||
instanceCount: {value: 10},
|
||||
instanceLength: {value: 1.75},
|
||||
instanceGap: {value: 0.5},
|
||||
profileFactor: {value: 1.5}
|
||||
}
|
||||
|
||||
let ig = new THREE.InstancedBufferGeometry().copy(new THREE.BoxGeometry(1, 1, 1, 100, 1, 1).translate(0.5, 0, 0));
|
||||
ig.instanceCount = params.instanceCount.value;
|
||||
|
||||
let m = new THREE.MeshBasicMaterial({
|
||||
vertexColors: true,
|
||||
onBeforeCompile: shader => {
|
||||
shader.uniforms.time = gu.time;
|
||||
shader.uniforms.instanceCount = params.instanceCount;
|
||||
shader.uniforms.instanceLength = params.instanceLength;
|
||||
shader.uniforms.instanceGap = params.instanceGap;
|
||||
shader.uniforms.profileFactor = params.profileFactor;
|
||||
shader.vertexShader = \`
|
||||
uniform float time;
|
||||
|
||||
uniform float instanceCount;
|
||||
uniform float instanceLength;
|
||||
uniform float instanceGap;
|
||||
|
||||
uniform float profileFactor;
|
||||
|
||||
varying float noGrid;
|
||||
|
||||
mat2 rot(float a){return mat2(cos(a), sin(a), -sin(a), cos(a));}
|
||||
|
||||
\${shader.vertexShader}
|
||||
\`.replace(
|
||||
\`#include <begin_vertex>\`,
|
||||
\`#include <begin_vertex>
|
||||
|
||||
float t = time * 0.1;
|
||||
|
||||
float iID = float(gl_InstanceID);
|
||||
|
||||
float instanceTotalLength = instanceLength + instanceGap;
|
||||
float instanceFactor = instanceLength / instanceTotalLength;
|
||||
|
||||
float circleLength = instanceTotalLength * instanceCount;
|
||||
float circleRadius = circleLength / PI2;
|
||||
|
||||
float partAngle = PI2 / instanceCount;
|
||||
float boxAngle = partAngle * instanceFactor;
|
||||
|
||||
float partTurn = PI / instanceCount;
|
||||
float boxTurn = partTurn * instanceFactor;
|
||||
|
||||
float startAngle = t + partAngle * iID;
|
||||
float startTurn = t * 0.5 + partTurn * iID;
|
||||
|
||||
float angleFactor = position.x;
|
||||
|
||||
float angle = startAngle + boxAngle * angleFactor;
|
||||
float turn = startTurn + boxTurn * angleFactor;
|
||||
|
||||
vec3 pos = vec3(0, position.y, position.z);
|
||||
pos.yz *= rot(turn);
|
||||
pos.yz *= profileFactor;
|
||||
pos.z += circleRadius;
|
||||
pos.xz *= rot(angle);
|
||||
|
||||
transformed = pos;
|
||||
float nZ = floor(abs(normal.z) + 0.1);
|
||||
float nX = floor(abs(normal.x) + 0.1);
|
||||
noGrid = 1. - nX;
|
||||
vColor = vec3(nZ == 1. ? 0.1 : nX == 1. ? 0. : 0.01);
|
||||
\`
|
||||
);
|
||||
//console.log(shader.vertexShader);
|
||||
shader.fragmentShader = \`
|
||||
varying float noGrid;
|
||||
|
||||
float lines(vec2 coord, float thickness){
|
||||
vec2 grid = abs(fract(coord - 0.5) - 0.5) / fwidth(coord) / thickness;
|
||||
float line = min(grid.x, grid.y);
|
||||
return 1.0 - min(line, 1.0);
|
||||
}
|
||||
\${shader.fragmentShader}
|
||||
\`.replace(
|
||||
\`#include <color_fragment>\`,
|
||||
\`#include <color_fragment>
|
||||
|
||||
float multiply = vColor.r > 0.05 ? 3. : 2.;
|
||||
float edges = lines(vUv, 3.);
|
||||
float grid = min(noGrid, lines(vUv * multiply, 1.));
|
||||
diffuseColor.rgb = mix(diffuseColor.rgb, vec3(1), max(edges, grid));
|
||||
\`
|
||||
)
|
||||
//console.log(shader.fragmentShader)
|
||||
}
|
||||
});
|
||||
m.defines = {"USE_UV": ""};
|
||||
|
||||
let o = new THREE.Mesh(ig, m);
|
||||
scene.add(o)
|
||||
o.rotation.z = -Math.PI * 0.25;
|
||||
|
||||
let clock = new THREE.Clock();
|
||||
let t = 0;
|
||||
|
||||
renderer.setAnimationLoop(()=>{
|
||||
let dt = clock.getDelta();
|
||||
t += dt;
|
||||
gu.time.value = t;
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
})
|
||||
`;
|
||||
181
blocksuite/playground/apps/_common/components/docs-panel.ts
Normal file
181
blocksuite/playground/apps/_common/components/docs-panel.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import {
|
||||
CloseIcon,
|
||||
createDefaultDoc,
|
||||
GenerateDocUrlProvider,
|
||||
} from '@blocksuite/blocks';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { BlockCollection, DocCollection } from '@blocksuite/store';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { removeModeFromStorage } from '../mock-services.js';
|
||||
|
||||
@customElement('docs-panel')
|
||||
export class DocsPanel extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
docs-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background-color: var(--affine-background-secondary-color);
|
||||
font-family: var(--affine-font-family);
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
.doc-item:hover .delete-doc-icon {
|
||||
display: flex;
|
||||
}
|
||||
.doc-item {
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
.delete-doc-icon {
|
||||
display: none;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.delete-doc-icon:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
.delete-doc-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--affine-secondary-color);
|
||||
fill: var(--affine-secondary-color);
|
||||
}
|
||||
.new-doc-button {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
.new-doc-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
createDoc = () => {
|
||||
createDocBlock(this.editor.doc.collection);
|
||||
};
|
||||
|
||||
gotoDoc = (doc: BlockCollection) => {
|
||||
const url = this.editor.std
|
||||
.getOptional(GenerateDocUrlProvider)
|
||||
?.generateDocUrl(doc.id);
|
||||
if (url) history.pushState({}, '', url);
|
||||
|
||||
this.editor.doc = doc.getDoc();
|
||||
this.editor.doc.load();
|
||||
this.editor.doc.resetHistory();
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private get collection() {
|
||||
return this.editor.doc.collection;
|
||||
}
|
||||
|
||||
private get docs() {
|
||||
return [...this.collection.docs.values()];
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!(event.target instanceof Node)) return;
|
||||
|
||||
const toggleButton = document.querySelector(
|
||||
'sl-button[data-docs-panel-toggle]'
|
||||
);
|
||||
if (toggleButton?.contains(event.target as Node)) return;
|
||||
|
||||
if (!this.contains(event.target)) {
|
||||
this.onClose?.();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
this.disposables.add(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
});
|
||||
|
||||
this.disposables.add(
|
||||
this.editor.doc.collection.slots.docUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
const { docs, collection } = this;
|
||||
return html`
|
||||
<div @click="${this.createDoc}" class="new-doc-button">New Doc</div>
|
||||
${repeat(
|
||||
docs,
|
||||
v => v.id,
|
||||
doc => {
|
||||
const style = styleMap({
|
||||
backgroundColor:
|
||||
this.editor.doc.id === doc.id
|
||||
? 'var(--affine-hover-color)'
|
||||
: undefined,
|
||||
padding: '4px 4px 4px 8px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
const click = () => {
|
||||
this.gotoDoc(doc);
|
||||
};
|
||||
const deleteDoc = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const isDeleteCurrent = doc.id === this.editor.doc.id;
|
||||
|
||||
collection.removeDoc(doc.id);
|
||||
removeModeFromStorage(doc.id);
|
||||
// When delete the current doc, we need to set the editor doc to the first remaining doc
|
||||
if (isDeleteCurrent) {
|
||||
this.editor.doc = this.docs[0].getDoc();
|
||||
}
|
||||
};
|
||||
return html`<div class="doc-item" @click="${click}" style="${style}">
|
||||
${doc.meta?.title || 'Untitled'}
|
||||
${docs.length > 1
|
||||
? html`<div @click="${deleteDoc}" class="delete-doc-icon">
|
||||
${CloseIcon}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: AffineEditorContainer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onClose!: () => void;
|
||||
}
|
||||
|
||||
function createDocBlock(collection: DocCollection) {
|
||||
const id = collection.idGenerator();
|
||||
createDefaultDoc(collection, { id });
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'docs-panel': DocsPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('left-side-panel')
|
||||
export class LeftSidePanel extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
left-side-panel {
|
||||
padding-top: 50px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
currentContent: HTMLElement | null = null;
|
||||
|
||||
hideContent() {
|
||||
if (this.currentContent) {
|
||||
this.style.display = 'none';
|
||||
this.currentContent.remove();
|
||||
this.currentContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
return html``;
|
||||
}
|
||||
|
||||
showContent(ele: HTMLElement) {
|
||||
if (this.currentContent) {
|
||||
this.currentContent.remove();
|
||||
}
|
||||
this.style.display = 'block';
|
||||
this.currentContent = ele;
|
||||
this.append(ele);
|
||||
}
|
||||
|
||||
toggle(ele: HTMLElement) {
|
||||
if (this.currentContent !== ele) {
|
||||
this.showContent(ele);
|
||||
} else {
|
||||
this.hideContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'left-side-panel': LeftSidePanel;
|
||||
}
|
||||
}
|
||||
64
blocksuite/playground/apps/_common/components/pdf/types.ts
Normal file
64
blocksuite/playground/apps/_common/components/pdf/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export enum State {
|
||||
IDLE = 0,
|
||||
Connecting,
|
||||
Connected,
|
||||
Opening,
|
||||
Opened,
|
||||
Failed,
|
||||
}
|
||||
|
||||
export type DocInfo = {
|
||||
total: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type ViewportInfo = {
|
||||
dpi: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export enum MessageState {
|
||||
Poll,
|
||||
Ready,
|
||||
}
|
||||
|
||||
export enum MessageOp {
|
||||
Init,
|
||||
Inited,
|
||||
Open,
|
||||
Opened,
|
||||
Render,
|
||||
Rendered,
|
||||
}
|
||||
|
||||
export enum RenderKind {
|
||||
Page,
|
||||
Thumbnail,
|
||||
}
|
||||
|
||||
export interface MessageDataMap {
|
||||
[MessageOp.Init]: undefined;
|
||||
[MessageOp.Inited]: undefined;
|
||||
[MessageOp.Open]: ArrayBuffer;
|
||||
[MessageOp.Opened]: DocInfo;
|
||||
[MessageOp.Render]: {
|
||||
index: number;
|
||||
kind: RenderKind;
|
||||
scale: number;
|
||||
};
|
||||
[MessageOp.Rendered]: {
|
||||
index: number;
|
||||
kind: RenderKind;
|
||||
imageData: ImageData;
|
||||
};
|
||||
}
|
||||
|
||||
export type MessageDataType<T = MessageDataMap> = {
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
export type MessageData<T = MessageOp, P = MessageDataType> = {
|
||||
type: T;
|
||||
} & P;
|
||||
124
blocksuite/playground/apps/_common/components/pdf/worker.ts
Normal file
124
blocksuite/playground/apps/_common/components/pdf/worker.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { Document } from '@toeverything/pdf-viewer';
|
||||
import {
|
||||
createPDFium,
|
||||
PageRenderingflags,
|
||||
Runtime,
|
||||
Viewer,
|
||||
} from '@toeverything/pdf-viewer';
|
||||
import wasmUrl from '@toeverything/pdfium/wasm?url';
|
||||
|
||||
import { type MessageData, type MessageDataType, MessageOp } from './types';
|
||||
|
||||
let inited = false;
|
||||
let viewer: Viewer | null = null;
|
||||
let doc: Document | undefined = undefined;
|
||||
|
||||
const docInfo = { total: 0, width: 1, height: 1 };
|
||||
const flags = PageRenderingflags.REVERSE_BYTE_ORDER | PageRenderingflags.ANNOT;
|
||||
|
||||
function post<T extends MessageOp>(type: T, data?: MessageDataType[T]) {
|
||||
const message = { type, [type]: data };
|
||||
self.postMessage(message);
|
||||
}
|
||||
|
||||
function renderToImageData(index: number, scale: number) {
|
||||
if (!viewer || !doc) return;
|
||||
|
||||
const page = doc.page(index);
|
||||
|
||||
if (!page) return;
|
||||
|
||||
const width = Math.ceil(docInfo.width * scale);
|
||||
const height = Math.ceil(docInfo.height * scale);
|
||||
|
||||
const bitmap = viewer.createBitmap(width, height, 0);
|
||||
bitmap.fill(0, 0, width, height);
|
||||
page.render(bitmap, 0, 0, width, height, 0, flags);
|
||||
|
||||
// @ts-expect-error FIXME: ts error
|
||||
const data = new Uint8ClampedArray(bitmap.toUint8Array());
|
||||
|
||||
bitmap.close();
|
||||
page.close();
|
||||
|
||||
return new ImageData(data, width, height);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
inited = true;
|
||||
|
||||
console.debug('pdf worker pending');
|
||||
self.postMessage({ type: MessageOp.Init });
|
||||
|
||||
const pdfium = await createPDFium({
|
||||
// @ts-expect-error allow
|
||||
locateFile: () => wasmUrl,
|
||||
});
|
||||
viewer = new Viewer(new Runtime(pdfium));
|
||||
|
||||
self.postMessage({ type: MessageOp.Inited });
|
||||
console.debug('pdf worker ready');
|
||||
}
|
||||
|
||||
async function process({ data }: MessageEvent<MessageData>) {
|
||||
if (!inited) {
|
||||
await start();
|
||||
}
|
||||
|
||||
if (!viewer) return;
|
||||
|
||||
const { type } = data;
|
||||
|
||||
switch (type) {
|
||||
case MessageOp.Open: {
|
||||
const buffer = data[type];
|
||||
if (!buffer) return;
|
||||
|
||||
doc = viewer.open(new Uint8Array(buffer));
|
||||
|
||||
if (!doc) return;
|
||||
|
||||
const page = doc.page(0);
|
||||
|
||||
if (!page) return;
|
||||
|
||||
Object.assign(docInfo, {
|
||||
total: doc.pageCount(),
|
||||
height: Math.ceil(page.height()),
|
||||
width: Math.ceil(page.width()),
|
||||
});
|
||||
page.close();
|
||||
post(MessageOp.Opened, docInfo);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MessageOp.Render: {
|
||||
if (!doc) return;
|
||||
|
||||
const { index, kind, scale } = data[type];
|
||||
|
||||
const { total } = docInfo;
|
||||
|
||||
if (index < 0 || index >= total) return;
|
||||
|
||||
queueMicrotask(() => {
|
||||
const imageData = renderToImageData(index, scale);
|
||||
if (!imageData) return;
|
||||
|
||||
post(MessageOp.Rendered, { index, kind, imageData });
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('message', (event: MessageEvent<MessageData>) => {
|
||||
process(event).catch(console.error);
|
||||
});
|
||||
|
||||
start().catch(error => {
|
||||
inited = false;
|
||||
console.log(error);
|
||||
});
|
||||
49
blocksuite/playground/apps/_common/components/side-panel.ts
Normal file
49
blocksuite/playground/apps/_common/components/side-panel.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('side-panel')
|
||||
export class SidePanel extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
side-panel {
|
||||
width: 395px;
|
||||
background-color: var(--affine-background-secondary-color);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
currentContent: HTMLElement | null = null;
|
||||
|
||||
hideContent() {
|
||||
if (this.currentContent) {
|
||||
this.style.display = 'none';
|
||||
this.currentContent.remove();
|
||||
this.currentContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
return html``;
|
||||
}
|
||||
|
||||
showContent(ele: HTMLElement) {
|
||||
if (this.currentContent) {
|
||||
this.currentContent.remove();
|
||||
}
|
||||
this.style.display = 'block';
|
||||
this.currentContent = ele;
|
||||
this.append(ele);
|
||||
}
|
||||
|
||||
toggle(ele: HTMLElement) {
|
||||
if (this.currentContent !== ele) {
|
||||
this.showContent(ele);
|
||||
} else {
|
||||
this.hideContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
1041
blocksuite/playground/apps/_common/components/starter-debug-menu.ts
Normal file
1041
blocksuite/playground/apps/_common/components/starter-debug-menu.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user