Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions vanilla-slash-menu/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.next
.svelte-kit
15 changes: 15 additions & 0 deletions vanilla-slash-menu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# vanilla-slash-menu

A [ProseKit](https://prosekit.dev) example.

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/prosekit/examples/tree/master/vanilla-slash-menu)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/prosekit/examples/tree/master/vanilla-slash-menu)

Run the example locally with:

```bash
npx degit prosekit/examples/vanilla-slash-menu vanilla-slash-menu
cd vanilla-slash-menu
npm install
npm run dev
```
11 changes: 11 additions & 0 deletions vanilla-slash-menu/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProseKit + Vanilla TypeScript</title>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions vanilla-slash-menu/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "example-vanilla-slash-menu",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"preview": "vite preview"
},
"dependencies": {
"prosekit": "^0.17.1"
},
"devDependencies": {
"@egoist/tailwindcss-icons": "^1.9.0",
"@iconify-json/lucide": "^1.2.84",
"@tailwindcss/vite": "^4.1.18",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"vite": "7.3.1"
}
}
13 changes: 13 additions & 0 deletions vanilla-slash-menu/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import 'tailwindcss';
@import 'tw-animate-css';

@plugin "@egoist/tailwindcss-icons";

body {
height: 100svh;
display: grid;
max-width: 900px;
padding: 16px;
margin-left: auto;
margin-right: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'

import { createEditor } from 'prosekit/core'

import { renderSlashMenu } from '../../ui/slash-menu'

import { defineExtension } from './extension'

export function setupVanillaEditor() {
const extension = defineExtension()
const editor = createEditor({ extension })

return {
render: () => {
const port = document.createElement('div')
port.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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white'

const scrolling = document.createElement('div')
scrolling.className = 'relative w-full flex-1 box-border overflow-y-auto'
port.append(scrolling)

const content = document.createElement('div')
content.className =
'ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500'
scrolling.append(content)

scrolling.append(renderSlashMenu(editor))

editor.mount(content)

return port
},
destroy: () => {
editor.unmount()
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { definePlaceholder } from 'prosekit/extensions/placeholder'

export function defineExtension() {
return union(
defineBasicExtension(),
definePlaceholder({ placeholder: 'Press / for commands...' }),
)
}

export type EditorExtension = ReturnType<typeof defineExtension>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { setupVanillaEditor } from './editor'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { renderSlashMenu } from './slash-menu'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'prosekit/web/autocomplete'

import type { AutocompleteEmptyElement } from 'prosekit/web/autocomplete'

export function renderSlashMenuEmpty() {
const empty = document.createElement(
'prosekit-autocomplete-empty',
) as AutocompleteEmptyElement
empty.className =
'relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800'

const span = document.createElement('span')
span.textContent = 'No results'
empty.append(span)

return empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'prosekit/web/autocomplete'

import type {
AutocompleteItemElement,
AutocompleteItemEvents,
} from 'prosekit/web/autocomplete'

export function renderSlashMenuItem(options: {
label: string
kbd?: string
onSelect: (event: AutocompleteItemEvents['select']) => void
}) {
const item = document.createElement(
'prosekit-autocomplete-item',
) as AutocompleteItemElement
item.className =
'relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-focused:bg-gray-100 dark:data-focused:bg-gray-800'
item.addEventListener('select', (event) =>
options.onSelect(event as AutocompleteItemEvents['select']),
)

const span = document.createElement('span')
span.textContent = options.label
item.append(span)

if (options.kbd) {
const kbd = document.createElement('kbd')
kbd.className = 'text-xs font-mono text-gray-400 dark:text-gray-500'
kbd.textContent = options.kbd
item.append(kbd)
}

return item
}
118 changes: 118 additions & 0 deletions vanilla-slash-menu/src/components/editor/ui/slash-menu/slash-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'prosekit/web/autocomplete'

import type { BasicExtension } from 'prosekit/basic'
import type { Editor } from 'prosekit/core'
import { canUseRegexLookbehind } from 'prosekit/core'
import type {
AutocompleteListElement,
AutocompletePopoverElement,
} from 'prosekit/web/autocomplete'

import { renderSlashMenuEmpty } from './slash-menu-empty'
import { renderSlashMenuItem } from './slash-menu-item'

// Match inputs like "/", "/table", "/heading 1" etc. Do not match "/ heading".
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u

export function renderSlashMenu(editor: Editor<BasicExtension>) {
const popover = document.createElement(
'prosekit-autocomplete-popover',
) as AutocompletePopoverElement
popover.className =
'relative block max-h-100 min-w-60 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'
popover.editor = editor
popover.regex = regex

const list = document.createElement(
'prosekit-autocomplete-list',
) as AutocompleteListElement
list.editor = editor
popover.append(list)

list.append(
renderSlashMenuItem({
label: 'Text',
kbd: undefined,
onSelect: () => editor.commands.setParagraph(),
}),
)
list.append(
renderSlashMenuItem({
label: 'Heading 1',
kbd: '#',
onSelect: () => editor.commands.setHeading({ level: 1 }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Heading 2',
kbd: '##',
onSelect: () => editor.commands.setHeading({ level: 2 }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Heading 3',
kbd: '###',
onSelect: () => editor.commands.setHeading({ level: 3 }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Bullet list',
kbd: '-',
onSelect: () => editor.commands.wrapInList({ kind: 'bullet' }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Ordered list',
kbd: '1.',
onSelect: () => editor.commands.wrapInList({ kind: 'ordered' }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Task list',
kbd: '[]',
onSelect: () => editor.commands.wrapInList({ kind: 'task' }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Toggle list',
kbd: '>>',
onSelect: () => editor.commands.wrapInList({ kind: 'toggle' }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Quote',
kbd: '>',
onSelect: () => editor.commands.setBlockquote(),
}),
)
list.append(
renderSlashMenuItem({
label: 'Table',
onSelect: () => editor.commands.insertTable({ row: 3, col: 3 }),
}),
)
list.append(
renderSlashMenuItem({
label: 'Divider',
kbd: '---',
onSelect: () => editor.commands.insertHorizontalRule(),
}),
)
list.append(
renderSlashMenuItem({
label: 'Code',
kbd: '```',
onSelect: () => editor.commands.setCodeBlock(),
}),
)
list.append(renderSlashMenuEmpty())

return popover
}
5 changes: 5 additions & 0 deletions vanilla-slash-menu/src/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { setupVanillaEditor } from './components/editor/examples/slash-menu'

export function renderEditor() {
return setupVanillaEditor().render()
}
11 changes: 11 additions & 0 deletions vanilla-slash-menu/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import './app.css'
import { renderEditor } from './editor'

let container = document.querySelector('#app')
if (!container) {
container = document.createElement('div')
container.id = 'app'
document.body.appendChild(container)
}

container.replaceChildren(renderEditor())
26 changes: 26 additions & 0 deletions vanilla-slash-menu/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
6 changes: 6 additions & 0 deletions vanilla-slash-menu/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
plugins: [tailwindcss()],
})