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,
)
}