import '@shoelace-style/shoelace'; import { ShadowlessElement } from '@blocksuite/block-std'; import { type AttributeRenderer, type BaseTextAttributes, baseTextAttributes, createInlineKeyDownHandler, InlineEditor, KEYBOARD_ALLOW_DEFAULT, ZERO_WIDTH_NON_JOINER, } from '@blocksuite/inline'; import { effects } from '@blocksuite/inline/effects'; import { effect } from '@preact/signals-core'; import { css, html, nothing } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import * as Y from 'yjs'; import { z } from 'zod'; import { markdownMatches } from './markdown.js'; effects(); function inlineTextStyles( props: BaseTextAttributes ): ReturnType { let textDecorations = ''; if (props.underline) { textDecorations += 'underline'; } if (props.strike) { textDecorations += ' line-through'; } let inlineCodeStyle = {}; if (props.code) { inlineCodeStyle = { 'font-family': '"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace', 'line-height': 'normal', background: 'rgba(135,131,120,0.15)', color: '#EB5757', 'border-radius': '3px', 'font-size': '85%', padding: '0.2em 0.4em', }; } return styleMap({ 'font-weight': props.bold ? 'bold' : 'normal', 'font-style': props.italic ? 'italic' : 'normal', 'text-decoration': textDecorations.length > 0 ? textDecorations : 'none', ...inlineCodeStyle, }); } const attributeRenderer: AttributeRenderer = ({ delta, selected }) => { // @ts-expect-error ignore if (delta.attributes?.embed) { return html`@flrande`; } const style = delta.attributes ? inlineTextStyles(delta.attributes) : styleMap({}); return html``; }; function toggleStyle( inlineEditor: InlineEditor, attrs: NonNullable ): void { const inlineRange = inlineEditor.getInlineRange(); if (!inlineRange) { return; } const root = inlineEditor.rootElement; if (!root) { return; } const deltas = inlineEditor.getDeltasByInlineRange(inlineRange); let oldAttributes: NonNullable = {}; for (const [delta] of deltas) { const attributes = delta.attributes; if (!attributes) { continue; } oldAttributes = { ...attributes }; } const newAttributes = Object.fromEntries( Object.entries(attrs).map(([k, v]) => { if ( typeof v === 'boolean' && v === (oldAttributes as Record)[k] ) { return [k, null]; } else { return [k, v]; } }) ); inlineEditor.formatText(inlineRange, newAttributes, { mode: 'merge', }); root.blur(); inlineEditor.setInlineRange(inlineRange); } @customElement('test-rich-text') export class TestRichText extends ShadowlessElement { override firstUpdated() { this.contentEditable = 'true'; this.style.outline = 'none'; this.inlineEditor.mount(this._container, this); const keydownHandler = createInlineKeyDownHandler(this.inlineEditor, { inputRule: { key: ' ', handler: context => { const { inlineEditor, prefixText, inlineRange } = context; for (const match of markdownMatches) { const matchedText = prefixText.match(match.pattern); if (matchedText) { return match.action({ inlineEditor, prefixText, inlineRange, pattern: match.pattern, undoManager: this.undoManager, }); } } return KEYBOARD_ALLOW_DEFAULT; }, }, }); this.addEventListener('keydown', keydownHandler); this.inlineEditor.slots.textChange.on(() => { const el = this.querySelector('.y-text'); if (el) { const text = this.inlineEditor.yText.toDelta(); const span = document.createElement('span'); span.innerHTML = JSON.stringify(text); el.replaceChildren(span); } }); effect(() => { const inlineRange = this.inlineEditor.inlineRange$.value; const el = this.querySelector('.v-range'); if (el && inlineRange) { const span = document.createElement('span'); span.innerHTML = JSON.stringify(inlineRange); el.replaceChildren(span); } }); } override render() { return html`
`; } @query('.rich-text-container') private accessor _container!: HTMLDivElement; @property({ attribute: false }) accessor inlineEditor!: InlineEditor; @property({ attribute: false }) accessor undoManager!: Y.UndoManager; } const TEXT_ID = 'inline-editor'; const yDocA = new Y.Doc(); const yDocB = new Y.Doc(); yDocA.on('update', update => { Y.applyUpdate(yDocB, update); }); yDocB.on('update', update => { Y.applyUpdate(yDocA, update); }); @customElement('custom-toolbar') export class CustomToolbar extends ShadowlessElement { static override styles = css` .custom-toolbar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); } `; override firstUpdated() { const boldButton = this.querySelector('.bold'); const italicButton = this.querySelector('.italic'); const underlineButton = this.querySelector('.underline'); const strikeButton = this.querySelector('.strike'); const code = this.querySelector('.code'); const embed = this.querySelector('.embed'); const resetButton = this.querySelector('.reset'); const undoButton = this.querySelector('.undo'); const redoButton = this.querySelector('.redo'); if ( !boldButton || !italicButton || !underlineButton || !strikeButton || !code || !embed || !resetButton || !undoButton || !redoButton ) { throw new Error('Cannot find button'); } const undoManager = new Y.UndoManager(this.inlineEditor.yText, { trackedOrigins: new Set([this.inlineEditor.yText.doc?.clientID]), }); addEventListener('keydown', e => { if ( e instanceof KeyboardEvent && (e.ctrlKey || e.metaKey) && e.key === 'z' ) { e.preventDefault(); if (e.shiftKey) { undoManager.redo(); } else { undoManager.undo(); } } }); undoButton.addEventListener('click', () => { undoManager.undo(); }); redoButton.addEventListener('click', () => { undoManager.redo(); }); boldButton.addEventListener('click', () => { undoManager.stopCapturing(); toggleStyle(this.inlineEditor, { bold: true }); }); italicButton.addEventListener('click', () => { undoManager.stopCapturing(); toggleStyle(this.inlineEditor, { italic: true }); }); underlineButton.addEventListener('click', () => { undoManager.stopCapturing(); toggleStyle(this.inlineEditor, { underline: true }); }); strikeButton.addEventListener('click', () => { undoManager.stopCapturing(); toggleStyle(this.inlineEditor, { strike: true }); }); code.addEventListener('click', () => { undoManager.stopCapturing(); toggleStyle(this.inlineEditor, { code: true }); }); embed.addEventListener('click', () => { undoManager.stopCapturing(); // @ts-expect-error ignore toggleStyle(this.inlineEditor, { embed: true }); }); resetButton.addEventListener('click', () => { undoManager.stopCapturing(); const rangeStatic = this.inlineEditor.getInlineRange(); if (!rangeStatic) { return; } this.inlineEditor.resetText(rangeStatic); }); } override render() { return html`
bold italic underline strike code embed reset undo redo
`; } @property({ attribute: false }) accessor inlineEditor!: InlineEditor; @property({ attribute: false }) accessor undoManager!: Y.UndoManager; } @customElement('test-page') export class TestPage extends ShadowlessElement { static override styles = css` .container { display: grid; height: 100vh; width: 100vw; justify-content: center; align-items: center; } .editors { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); padding: 20px; background-color: #202124; border-radius: 10px; color: #fff; grid-gap: 20px; } .editors > div { height: 600px; max-width: 400px; display: grid; grid-template-rows: 150px minmax(0, 1fr); grid-template-columns: minmax(0, 1fr); overflow-y: scroll; } `; private _editorA: InlineEditor | null = null; private _editorB: InlineEditor | null = null; private _undoManagerA: Y.UndoManager | null = null; private _undoManagerB: Y.UndoManager | null = null; override firstUpdated() { const textA = yDocA.getText(TEXT_ID); this._editorA = new InlineEditor< BaseTextAttributes & { embed?: true; } >(textA, { isEmbed: delta => !!delta.attributes?.embed, }); this._editorA.setAttributeSchema( baseTextAttributes.extend({ embed: z.literal(true).optional().catch(undefined), }) ); this._editorA.setAttributeRenderer(attributeRenderer); this._undoManagerA = new Y.UndoManager(textA, { trackedOrigins: new Set([textA.doc?.clientID]), }); const textB = yDocB.getText(TEXT_ID); this._editorB = new InlineEditor(textB); this._undoManagerB = new Y.UndoManager(textB, { trackedOrigins: new Set([textB.doc?.clientID]), }); this.requestUpdate(); } override render() { if (!this._editorA) { return nothing; } return html`
`; } }