feat(richtext-lexical): headings

This commit is contained in:
Alessio Gravili
2024-03-01 09:24:03 -05:00
parent e75917a4d3
commit 486b3a1f44
6 changed files with 96 additions and 50 deletions

View File

@@ -1,15 +1,24 @@
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
'use client'
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
import type { HeadingTagType } from '@lexical/rich-text'
import { HeadingNode } from '@lexical/rich-text'
import { $createHeadingNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection'
import { $getSelection } from 'lexical'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types'
import type { FeatureProviderProviderClient } from '../types'
import type { HeadingFeatureProps } from './feature.server'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { H1Icon } from '../../lexical/ui/icons/H1'
import { H2Icon } from '../../lexical/ui/icons/H2'
import { H3Icon } from '../../lexical/ui/icons/H3'
import { H4Icon } from '../../lexical/ui/icons/H4'
import { H5Icon } from '../../lexical/ui/icons/H5'
import { H6Icon } from '../../lexical/ui/icons/H6'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { createClientComponent } from '../createClientComponent'
import { MarkdownTransformer } from './markdownTransformer'
const setHeading = (headingSize: HeadingTagType) => {
@@ -17,31 +26,23 @@ const setHeading = (headingSize: HeadingTagType) => {
$setBlocksType(selection, () => $createHeadingNode(headingSize))
}
type Props = {
enabledHeadingSizes?: HeadingTagType[]
}
const iconImports = {
// @ts-expect-error-next-line
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
// @ts-expect-error-next-line
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
// @ts-expect-error-next-line
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon),
// @ts-expect-error-next-line
h4: () => import('../../lexical/ui/icons/H4').then((module) => module.H4Icon),
// @ts-expect-error-next-line
h5: () => import('../../lexical/ui/icons/H5').then((module) => module.H5Icon),
// @ts-expect-error-next-line
h6: () => import('../../lexical/ui/icons/H6').then((module) => module.H6Icon),
h1: H1Icon,
h2: H2Icon,
h3: H3Icon,
h4: H4Icon,
h5: H5Icon,
h6: H6Icon,
}
export const HeadingFeature = (props: Props): FeatureProvider => {
const HeadingFeatureClient: FeatureProviderProviderClient<HeadingFeatureProps> = (props) => {
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
...enabledHeadingSizes.map((headingSize, i) =>
@@ -63,30 +64,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
],
},
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [
{
type: HeadingNode.getType(),
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
},
nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>,
},
node: HeadingNode,
},
],
props,
nodes: [HeadingNode],
slashMenu: {
options: [
...enabledHeadingSizes.map((headingSize) => {
@@ -109,6 +87,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
},
}
},
key: 'heading',
}
}
export const HeadingFeatureClientComponent = createClientComponent(HeadingFeatureClient)

View File

@@ -0,0 +1,60 @@
import type { HeadingTagType } from '@lexical/rich-text'
import { HeadingNode, type SerializedHeadingNode } from '@lexical/rich-text'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProviderProviderServer } from '../types'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { HeadingFeatureClientComponent } from './feature.client'
import { MarkdownTransformer } from './markdownTransformer'
export type HeadingFeatureProps = {
enabledHeadingSizes?: HeadingTagType[]
}
export const HeadingFeature: FeatureProviderProviderServer<
HeadingFeatureProps,
HeadingFeatureProps
> = (props) => {
if (!props) {
props = {}
}
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return {
feature: () => {
return {
ClientComponent: HeadingFeatureClientComponent,
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [
{
type: HeadingNode.getType(),
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
},
nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>,
},
node: HeadingNode,
},
],
serverFeatureProps: props,
}
},
key: 'heading',
serverFeatureProps: props,
}
}

View File

@@ -13,11 +13,11 @@ export const sanitizeClientFeatures = (
floatingSelectToolbar: {
sections: [],
},
hooks: {
load: [],
save: [],
},
markdownTransformers: [],
nodes: [],
plugins: [],
slashMenu: {
@@ -110,6 +110,11 @@ export const sanitizeClientFeatures = (
}
}
if (feature.markdownTransformers?.length) {
sanitized.markdownTransformers = sanitized.markdownTransformers.concat(
feature.markdownTransformers,
)
}
sanitized.enabledFeatures.push(feature.key)
})

View File

@@ -12,7 +12,7 @@ import { StrikethroughFeature } from '../../../features/format/strikethrough/fea
import { SubscriptFeature } from '../../../features/format/subscript/feature.server'
import { SuperscriptFeature } from '../../../features/format/superscript/feature.server'
import { UnderlineFeature } from '../../../features/format/underline/feature.server'
import { HeadingFeature } from '../../../features/heading'
import { HeadingFeature } from '../../../features/heading/feature.server'
import { IndentFeature } from '../../../features/indent'
import { LinkFeature } from '../../../features/link/feature.server'
import { CheckListFeature } from '../../../features/lists/checklist'

View File

@@ -270,7 +270,7 @@ export { StrikethroughFeature } from './field/features/format/strikethrough/feat
export { SubscriptFeature } from './field/features/format/subscript/feature.server'
export { SuperscriptFeature } from './field/features/format/superscript/feature.server'
export { UnderlineFeature } from './field/features/format/underline/feature.server'
export { HeadingFeature } from './field/features/heading'
export { HeadingFeature } from './field/features/heading/feature.server'
export { IndentFeature } from './field/features/indent'
export { LinkFeature, type LinkFeatureServerProps } from './field/features/link/feature.server'

View File

@@ -3,6 +3,7 @@ import {
BlockQuoteFeature,
BlocksFeature,
BoldFeature,
HeadingFeature,
InlineCodeFeature,
ItalicFeature,
LinkFeature,
@@ -99,6 +100,7 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
SuperscriptFeature(),
InlineCodeFeature(),
TreeViewFeature(),
HeadingFeature(),
BlocksFeature({
blocks: [
{