Text Align
Sets the horizontal alignment of the block nodes using the text-align CSS property.
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import { defineExtension } from './extension'
import Toolbar from './toolbar'
const defaultContent = '<h1 style="text-align:center;">Heading</h1>'
+ '<p style="text-align:left;">First paragraph</p>'
+ '<p style="text-align:center;">Second paragraph</p>'
+ '<p style="text-align:right;">Third paragraph</p>'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [])
return (
<ProseKit editor={editor}>
<div 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">
<Toolbar />
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} 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"></div>
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineTextAlign } from 'prosekit/extensions/text-align'
export function defineExtension() {
return union(
defineBasicExtension(),
defineTextAlign({ types: ['paragraph', 'heading'] }),
)
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import type { Editor } from 'prosekit/core'
import { useEditorDerivedValue } from 'prosekit/react'
import { Button } from '../../ui/button'
import type { EditorExtension } from './extension'
function isTextAlignActive(editor: Editor<EditorExtension>, value: string) {
return Object.values(editor.nodes).some((node) => {
// @ts-expect-error textAlign may not be available on every node
return node.isActive({ textAlign: value })
})
}
function getToolbarItems(editor: Editor<EditorExtension>) {
return {
left: {
isActive: isTextAlignActive(editor, 'left'),
canExec: editor.commands.setTextAlign.canExec('left'),
command: () => editor.commands.setTextAlign('left'),
},
center: {
isActive: isTextAlignActive(editor, 'center'),
canExec: editor.commands.setTextAlign.canExec('center'),
command: () => editor.commands.setTextAlign('center'),
},
right: {
isActive: isTextAlignActive(editor, 'right'),
canExec: editor.commands.setTextAlign.canExec('right'),
command: () => editor.commands.setTextAlign('right'),
},
justify: {
isActive: isTextAlignActive(editor, 'justify'),
canExec: editor.commands.setTextAlign.canExec('justify'),
command: () => editor.commands.setTextAlign('justify'),
},
}
}
export default function Toolbar() {
const items = useEditorDerivedValue(getToolbarItems)
return (
<div className="z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center">
<Button
pressed={items.left.isActive}
disabled={!items.left.canExec}
onClick={items.left.command}
>
Left
</Button>
<Button
pressed={items.center.isActive}
disabled={!items.center.canExec}
onClick={items.center.command}
>
Center
</Button>
<Button
pressed={items.right.isActive}
disabled={!items.right.canExec}
onClick={items.right.command}
>
Right
</Button>
<Button
pressed={items.justify.isActive}
disabled={!items.justify.canExec}
onClick={items.justify.command}
>
Justify
</Button>
</div>
)
}import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from 'prosekit/react/tooltip'
import type {
MouseEventHandler,
ReactNode,
} from 'react'
export default function Button(props: {
pressed?: boolean
disabled?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>
tooltip?: string
children: ReactNode
}) {
return (
<TooltipRoot>
<TooltipTrigger className="block">
<button
data-state={props.pressed ? 'on' : 'off'}
disabled={props.disabled}
onClick={props.onClick}
onMouseDown={(event) => {
// Prevent the editor from being blurred when the button is clicked
event.preventDefault()
}}
className="outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700"
>
{props.children}
{props.tooltip ? <span className="sr-only">{props.tooltip}</span> : null}
</button>
</TooltipTrigger>
{props.tooltip
? (
<TooltipContent className="z-50 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=bottom]:slide-out-to-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=left]:slide-out-to-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=right]:slide-out-to-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 motion-safe:data-[side=top]:slide-out-to-bottom-2">
{props.tooltip}
</TooltipContent>
)
: null}
</TooltipRoot>
)
}export { default as Button } from './button'import { defineTextAlign function defineTextAlign<NodeName extends string = string>(options: TextAlignOptions<NodeName>): TextAlignExtension<NodeName>Adds a `textAlign` attribute to the specified nodes. This will be rendered as
a CSS `text-align` style. } from 'prosekit/extensions/text-align'
const extension const extension: TextAlignExtension<"paragraph" | "heading"> = defineTextAlign defineTextAlign<"paragraph" | "heading">(options: TextAlignOptions<"paragraph" | "heading">): TextAlignExtension<"paragraph" | "heading">Adds a `textAlign` attribute to the specified nodes. This will be rendered as
a CSS `text-align` style. ({
// A list of nodes that will be applied.
types TextAlignOptions<"paragraph" | "heading">.types: ("paragraph" | "heading")[]The names of node to add the attribute to. : ['paragraph', 'heading'],
// An optional default alignment value. Defaults to `left`.
default TextAlignOptions<NodeName extends string = string>.default?: string | undefinedThe default value for the attribute. : 'center',
})Commands
Section titled “Commands”setTextAlign
Section titled “setTextAlign”Apply an alignment value to selected blocks.
editor const editor: Editor<TextAlignExtension<"paragraph" | "heading">> .commands Editor<TextAlignExtension<"paragraph" | "heading">>.commands: ToCommandAction<{
setTextAlign: [value: string | null];
}>
All
{@link
CommandAction
}
s defined by the editor. .setTextAlign setTextAlign: CommandAction
(value: string | null) => boolean
Execute the current command. Return `true` if the command was successfully
executed, otherwise `false`. ('right')Keyboard Shortcuts
Section titled “Keyboard Shortcuts”| Non-Apple | Apple | Description |
|---|---|---|
Ctrl-Shift-L | Command-Shift-L | Align selected blocks to the left |
Ctrl-Shift-R | Command-Shift-R | Align selected blocks to the right |
Ctrl-Shift-E | Command-Shift-E | Center selected blocks |
Ctrl-Shift-J | Command-Shift-J | Justify selected blocks |