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 shadow dark:border-zinc-700 flex flex-col bg-white dark:bg-gray-950'>
<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 shadow dark:border-zinc-700 flex flex-col bg-white dark:bg-gray-950'
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()