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 { $setBlocksType } from '@lexical/selection'
import { $getSelection } from 'lexical' import { $getSelection } from 'lexical'
import type { HTMLConverter } from '../converters/html/converter/types' import type { FeatureProviderProviderClient } from '../types'
import type { FeatureProvider } from '../types' import type { HeadingFeatureProps } from './feature.server'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' 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 { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter' import { createClientComponent } from '../createClientComponent'
import { MarkdownTransformer } from './markdownTransformer' import { MarkdownTransformer } from './markdownTransformer'
const setHeading = (headingSize: HeadingTagType) => { const setHeading = (headingSize: HeadingTagType) => {
@@ -17,31 +26,23 @@ const setHeading = (headingSize: HeadingTagType) => {
$setBlocksType(selection, () => $createHeadingNode(headingSize)) $setBlocksType(selection, () => $createHeadingNode(headingSize))
} }
type Props = {
enabledHeadingSizes?: HeadingTagType[]
}
const iconImports = { const iconImports = {
// @ts-expect-error-next-line h1: H1Icon,
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon), h2: H2Icon,
// @ts-expect-error-next-line h3: H3Icon,
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon), h4: H4Icon,
// @ts-expect-error-next-line h5: H5Icon,
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon), h6: H6Icon,
// @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),
} }
export const HeadingFeature = (props: Props): FeatureProvider => { const HeadingFeatureClient: FeatureProviderProviderClient<HeadingFeatureProps> = (props) => {
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
...enabledHeadingSizes.map((headingSize, i) => ...enabledHeadingSizes.map((headingSize, i) =>
@@ -63,30 +64,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
], ],
}, },
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [ nodes: [HeadingNode],
{
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,
slashMenu: { slashMenu: {
options: [ options: [
...enabledHeadingSizes.map((headingSize) => { ...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: { floatingSelectToolbar: {
sections: [], sections: [],
}, },
hooks: { hooks: {
load: [], load: [],
save: [], save: [],
}, },
markdownTransformers: [],
nodes: [], nodes: [],
plugins: [], plugins: [],
slashMenu: { slashMenu: {
@@ -110,6 +110,11 @@ export const sanitizeClientFeatures = (
} }
} }
if (feature.markdownTransformers?.length) {
sanitized.markdownTransformers = sanitized.markdownTransformers.concat(
feature.markdownTransformers,
)
}
sanitized.enabledFeatures.push(feature.key) 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 { SubscriptFeature } from '../../../features/format/subscript/feature.server'
import { SuperscriptFeature } from '../../../features/format/superscript/feature.server' import { SuperscriptFeature } from '../../../features/format/superscript/feature.server'
import { UnderlineFeature } from '../../../features/format/underline/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 { IndentFeature } from '../../../features/indent'
import { LinkFeature } from '../../../features/link/feature.server' import { LinkFeature } from '../../../features/link/feature.server'
import { CheckListFeature } from '../../../features/lists/checklist' 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 { SubscriptFeature } from './field/features/format/subscript/feature.server'
export { SuperscriptFeature } from './field/features/format/superscript/feature.server' export { SuperscriptFeature } from './field/features/format/superscript/feature.server'
export { UnderlineFeature } from './field/features/format/underline/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 { IndentFeature } from './field/features/indent'
export { LinkFeature, type LinkFeatureServerProps } from './field/features/link/feature.server' export { LinkFeature, type LinkFeatureServerProps } from './field/features/link/feature.server'

View File

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