Example: dom
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { html, LitElement, } from 'lit' import { customElement, property, state, } from 'lit/decorators.js' import { createRef, ref, type Ref, } from 'lit/directives/ref.js' import { createEditor, type Editor, type NodeJSON, } from 'prosekit/core' import { defineExtension, type EditorExtension, } from './extension' @customElement('example-lit-dom') export class MyEditor extends LitElement { override createRenderRoot() { return this } @state() editor?: Editor<EditorExtension> @property({ type: Object, attribute: false }) defaultContent?: NodeJSON private editorRef: Ref<HTMLDivElement> = createRef() protected override firstUpdated(): void { if (!this.editor) { const extension = defineExtension() this.editor = createEditor({ extension, defaultContent: this.defaultContent || defaultContent, }) } this.editor.mount(this.editorRef.value) } override render() { return html` <div class='box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white'> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500' ${ref(this.editorRef)}></div> </div> </div> ` } } const defaultContent: NodeJSON = { type: 'doc', content: [ { type: 'heading', attrs: { level: 1, }, content: [ { type: 'text', text: 'Image', }, ], }, { type: 'image', attrs: { src: 'https://placehold.co/120x80', }, }, { type: 'heading', attrs: { level: 1, }, content: [ { type: 'text', text: 'Code Block', }, ], }, { type: 'codeBlock', attrs: { language: 'python', }, content: [ { type: 'text', text: 'if __name__ == "__main__":\n print("hello world!")\n\n'.repeat( 20, ), }, ], }, ], }
import { defineBasicExtension } from 'prosekit/basic' import { union } from 'prosekit/core' import { defineCodeBlock, defineCodeBlockShiki, } from 'prosekit/extensions/code-block' import { defineCodeBlockView } from './code-block-view' export function defineExtension() { return union( defineBasicExtension(), defineCodeBlock(), defineCodeBlockShiki(), defineCodeBlockView(), ) } export type EditorExtension = ReturnType<typeof defineExtension>
import { defineNodeView, setNodeAttrs, } from 'prosekit/core' import { createElement } from './create-element' import { createLanguageSelector } from './language-selector' export function defineCodeBlockView() { return defineNodeView({ name: 'codeBlock', constructor: (node, view, getPos) => { const language = node.attrs.language as string const setLanguage = (language: string) => { const pos = getPos()! const attrs = { language } const command = setNodeAttrs({ type: 'codeBlock', attrs, pos }) command(view.state, view.dispatch) } const type = node.type const code = createElement('code', {}) const dom = createElement( 'div', { 'data-node-view-root': 'true', }, createLanguageSelector({ language, setLanguage }), createElement('pre', {}, code), ) return { dom: dom, contentDOM: code, update: (node) => { if (node.type !== type) { return false } code.textContent = node.textContent return true }, ignoreMutation: () => { return true }, } }, }) }
function createElement<K extends keyof HTMLElementTagNameMap>( tagName: K, attributes: Record<string, string>, ...children: (string | HTMLElement)[] ): HTMLElementTagNameMap[K] function createElement( tagName: string, attributes: Record<string, string>, ...children: (string | HTMLElement)[] ): HTMLElement { const element = document.createElement(tagName) const { class: className, ...rest } = attributes if (className) { for (const c of className.split(' ')) { element.classList.add(c) } } Object.entries(rest).forEach(([key, value]) => { element.setAttribute(key, value) }) children.forEach((child) => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)) } else { element.appendChild(child) } }) return element } export { createElement }
export function getId() { return Math.random().toString(36).slice(2) }
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block' import { createElement } from './create-element' export function createLanguageSelector({ language, setLanguage, }: { language?: string setLanguage: (language: string) => void }) { const select = createElement( 'select', { class: 'outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded border-none bg-transparent px-2 py-1 text-xs transition text-[var(--prosemirror-highlight)] opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 [div[data-node-view-root]:hover_&]:hover:opacity-80' }, createElement('option', { value: '' }, 'Plain Text'), ...shikiBundledLanguagesInfo.map((info) => { return createElement('option', { value: info.id }, info.name) }), ) select.value = language || '' select.addEventListener('change', (event) => { setLanguage((event.target as HTMLSelectElement).value) }) return createElement( 'div', { class: 'relative mx-2 top-3 h-0 select-none overflow-visible text-xs', contenteditable: 'false' }, select, ) }
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, union, } from 'prosekit/core' import { definePlaceholder } from 'prosekit/extensions/placeholder' import { AutocompleteItem, AutocompleteList, AutocompletePopover, } from 'prosekit/lit/autocomplete' export function defineExtension() { return union( defineBasicExtension(), definePlaceholder({ placeholder: 'Press / for commands...' }), ) } const editor = createEditor({ extension: defineExtension() }) function createPopover() { const popover = new AutocompletePopover() popover.editor = editor popover.regex = /\/(\w*)$/ popover.append(createList()) return popover } function createList() { const list = new AutocompleteList() list.editor = editor list.append( createItem('Insert Heading 1', () => handleHeadingInsert(1)), createItem('Insert Heading 2', () => handleHeadingInsert(2)), createItem('Turn into Heading 1', () => handleHeadingConvert(1)), createItem('Turn into Heading 2', () => handleHeadingConvert(2)), ) list.className = 'relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden' return list } /** * @param {string} text * @param {function} callback */ function createItem(text, callback) { const item = new AutocompleteItem() item.append(text) item.onSelect = callback item.className = 'relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800' return item } /** * @param {number} level */ function handleHeadingInsert(level) { editor.commands.insertHeading({ level }) } /** * @param {number} level */ function handleHeadingConvert(level) { editor.commands.setHeading({ level }) } function main() { const root = document.querySelector('.example-vanilla-dom') if (!root) { return } root.innerHTML = '' const viewport = root.appendChild(document.createElement('div')) viewport.className = 'box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white' const scrolling = viewport.appendChild(document.createElement('div')) scrolling.className = 'relative w-full flex-1 box-border overflow-y-scroll' const content = scrolling.appendChild(document.createElement('div')) content.className = 'ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500' editor.mount(content) scrolling.appendChild(createPopover()) } main()