/* 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 readonly _darkModeChange = (e: MediaQueryListEvent) => { this._setThemeMode(!!e.matches); }; private readonly _handleDocsPanelClose = () => { this.leftSidePanel.toggle(this.docsPanel); }; private readonly _keydown = (e: KeyboardEvent) => { if (e.key === 'F1') { this._switchEditorMode(); } }; private readonly _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, ]), }, 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`
Test operations Print Add Note Export Markdown Export HTML Export PDF Export PNG Export Snapshot Import Snapshot Clear Site Data Toggle ${this._dark ? 'Light' : 'Dark'} Mode GitHub { this.doc.undo(); }} > { this.doc.redo(); }} > ${new URLSearchParams(location.search).get('room') ? html` { 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', } ); } }} >` : nothing}
${this._docMode === 'edgeless' ? html` mockEdgelessTheme.toggleTheme()} > ` : nothing} ${this._docMode === 'edgeless' ? html` { if (this.rootService instanceof EdgelessRootService) { this.rootService.gfx.tool.setTool('frameNavigator', { mode: 'fit', }); } }} > ` : nothing}
`; } @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; } }