Skip to content
ts
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'

import { LitElement, html } 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 {
  createRenderRoot() {
    return this
  }

  @state()
  editor?: Editor<EditorExtension>

  @property({ type: Object, attribute: false })
  defaultContent?: NodeJSON

  private editorRef: Ref<HTMLDivElement> = createRef()

  protected firstUpdated(): void {
    if (!this.editor) {
      const extension = defineExtension()
      this.editor = createEditor({
        extension,
        defaultContent: this.defaultContent || defaultContent,
      })
    }

    this.editor.mount(this.editorRef.value)
  }

  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 shadow dark:border-zinc-700 flex flex-col bg-white dark:bg-neutral-900'>
        <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 [&_pre]:text-white [&_pre]:bg-zinc-800' ${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,
          ),
        },
      ],
    },
  ],
}
ts
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>
ts
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
        },
      }
    },
  })
}
ts
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 }
ts
export function getId() {
  return Math.random().toString(36).slice(2)
}
ts
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-white 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 left-2 top-3 h-0 select-none overflow-visible', contenteditable: 'false' },
    select,
  )
}