feat(richtext-lexical): headings
This commit is contained in:
@@ -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)
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user