feat(richtext-lexical): lists

This commit is contained in:
Alessio Gravili
2024-03-01 09:54:02 -05:00
parent 881a502cbc
commit d38bbd9603
10 changed files with 234 additions and 168 deletions

View File

@@ -0,0 +1,79 @@
'use client'
import { INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list'
import type { ClientFeature, FeatureProviderProviderClient } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { createClientComponent } from '../../createClientComponent'
import { LexicalListPlugin } from '../plugin'
import { CHECK_LIST } from './markdownTransformers'
import { LexicalCheckListPlugin } from './plugin'
const CheckListFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: ({ featureProviderMap }) => {
const plugins: ClientFeature<undefined>['plugins'] = [
{
Component: LexicalCheckListPlugin,
position: 'normal',
},
]
if (!featureProviderMap.has('unorderedlist') && !featureProviderMap.has('orderedlist')) {
plugins.push({
Component: LexicalListPlugin,
position: 'normal',
})
}
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: ChecklistIcon,
isActive: () => false,
key: 'checkList',
label: `Check List`,
onClick: ({ editor }) => {
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)
},
order: 12,
},
]),
],
},
markdownTransformers: [CHECK_LIST],
nodes:
featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist')
? []
: [ListNode, ListItemNode],
plugins,
slashMenu: {
options: [
{
displayName: 'Lists',
key: 'lists',
options: [
new SlashMenuOption('checklist', {
Icon: ChecklistIcon,
displayName: 'Check List',
keywords: ['check list', 'check', 'checklist', 'cl'],
onSelect: ({ editor }) => {
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)
},
}),
],
},
],
},
}
},
}
}
export const CheckListFeatureClientComponent = createClientComponent(CheckListFeatureClient)

View File

@@ -0,0 +1,38 @@
import { ListItemNode, ListNode } from '@lexical/list'
import type { FeatureProviderProviderServer } from '../../types'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { CheckListFeatureClientComponent } from './feature.client'
import { CHECK_LIST } from './markdownTransformers'
export const CheckListFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: ({ featureProviderMap }) => {
return {
ClientComponent: CheckListFeatureClientComponent,
markdownTransformers: [CHECK_LIST],
nodes:
featureProviderMap.has('unorderedlist') || featureProviderMap.has('orderedlist')
? []
: [
{
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
],
serverFeatureProps: props,
}
},
key: 'checklist',
serverFeatureProps: props,
}
}

View File

@@ -1,91 +0,0 @@
import { INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list'
import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { CHECK_LIST } from './markdownTransformers'
// 345
// carbs 7
export const CheckListFeature = (): FeatureProvider => {
return {
feature: ({ featureProviderMap }) => {
return {
floatingSelectToolbar: {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Checklist').then(
(module) => module.ChecklistIcon,
),
isActive: () => false,
key: 'checkList',
label: `Check List`,
onClick: ({ editor }) => {
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)
},
order: 12,
},
]),
],
},
markdownTransformers: [CHECK_LIST],
nodes:
featureProviderMap.has('unorderedList') || featureProviderMap.has('orderedList')
? []
: [
{
type: ListNode.getType(),
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
type: ListItemNode.getType(),
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
],
plugins: [
{
Component: () =>
// @ts-expect-error-next-line
import('./plugin').then((module) => module.LexicalCheckListPlugin),
position: 'normal',
},
],
props: null,
slashMenu: {
options: [
{
displayName: 'Lists',
key: 'lists',
options: [
new SlashMenuOption('checklist', {
Icon: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Checklist').then(
(module) => module.ChecklistIcon,
),
displayName: 'Check List',
keywords: ['check list', 'check', 'checklist', 'cl'],
onSelect: ({ editor }) => {
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)
},
}),
],
},
],
},
}
},
key: 'checklist',
}
}

View File

@@ -1,25 +1,27 @@
'use client'
import { INSERT_ORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list'
import type { FeatureProvider } from '../../types'
import type { FeatureProviderProviderClient } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { createClientComponent } from '../../createClientComponent'
import { LexicalListPlugin } from '../plugin'
import { ORDERED_LIST } from './markdownTransformer'
export const OrderedListFeature = (): FeatureProvider => {
const OrderedListFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: ({ featureProviderMap }) => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/OrderedList').then(
(module) => module.OrderedListIcon,
),
ChildComponent: OrderedListIcon,
isActive: () => false,
key: 'orderedList',
label: `Ordered List`,
@@ -32,35 +34,15 @@ export const OrderedListFeature = (): FeatureProvider => {
],
},
markdownTransformers: [ORDERED_LIST],
nodes: featureProviderMap.has('unorderedList')
nodes: featureProviderMap.has('unorderedlist') ? [] : [ListNode, ListItemNode],
plugins: featureProviderMap.has('unorderedlist')
? []
: [
{
type: ListNode.getType(),
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
type: ListItemNode.getType(),
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
],
plugins: featureProviderMap.has('unorderedList')
? []
: [
{
Component: () =>
// @ts-expect-error-next-line
import('../plugin').then((module) => module.LexicalListPlugin),
Component: LexicalListPlugin,
position: 'normal',
},
],
props: null,
slashMenu: {
options: [
{
@@ -68,11 +50,7 @@ export const OrderedListFeature = (): FeatureProvider => {
key: 'lists',
options: [
new SlashMenuOption('orderedlist', {
Icon: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/OrderedList').then(
(module) => module.OrderedListIcon,
),
Icon: OrderedListIcon,
displayName: 'Ordered List',
keywords: ['ordered list', 'ol'],
onSelect: ({ editor }) => {
@@ -85,6 +63,7 @@ export const OrderedListFeature = (): FeatureProvider => {
},
}
},
key: 'orderedlist',
}
}
export const OrderedListFeatureClientComponent = createClientComponent(OrderedListFeatureClient)

View File

@@ -0,0 +1,37 @@
import { ListItemNode, ListNode } from '@lexical/list'
import type { FeatureProviderProviderServer } from '../../types'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { OrderedListFeatureClientComponent } from './feature.client'
import { ORDERED_LIST } from './markdownTransformer'
export const OrderedListFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: ({ featureProviderMap }) => {
return {
ClientComponent: OrderedListFeatureClientComponent,
markdownTransformers: [ORDERED_LIST],
nodes: featureProviderMap.has('unorderedlist')
? []
: [
{
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
],
serverFeatureProps: props,
}
},
key: 'orderedlist',
serverFeatureProps: props,
}
}

View File

@@ -1,25 +1,27 @@
'use client'
import { INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list'
import type { FeatureProvider } from '../../types'
import type { FeatureProviderProviderClient } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { createClientComponent } from '../../createClientComponent'
import { LexicalListPlugin } from '../plugin'
import { UNORDERED_LIST } from './markdownTransformer'
export const UnorderedListFeature = (): FeatureProvider => {
const UnorderedListFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/UnorderedList').then(
(module) => module.UnorderedListIcon,
),
ChildComponent: UnorderedListIcon,
isActive: () => false,
key: 'unorderedList',
label: `Unordered List`,
@@ -32,31 +34,13 @@ export const UnorderedListFeature = (): FeatureProvider => {
],
},
markdownTransformers: [UNORDERED_LIST],
nodes: [
{
type: ListNode.getType(),
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
type: ListItemNode.getType(),
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
],
nodes: [ListNode, ListItemNode],
plugins: [
{
Component: () =>
// @ts-expect-error-next-line
import('../plugin').then((module) => module.LexicalListPlugin),
Component: LexicalListPlugin,
position: 'normal',
},
],
props: null,
slashMenu: {
options: [
{
@@ -64,11 +48,7 @@ export const UnorderedListFeature = (): FeatureProvider => {
key: 'lists',
options: [
new SlashMenuOption('unorderedlist', {
Icon: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/UnorderedList').then(
(module) => module.UnorderedListIcon,
),
Icon: UnorderedListIcon,
displayName: 'Unordered List',
keywords: ['unordered list', 'ul'],
onSelect: ({ editor }) => {
@@ -81,6 +61,7 @@ export const UnorderedListFeature = (): FeatureProvider => {
},
}
},
key: 'unorderedlist',
}
}
export const UnorderedListFeatureClientComponent = createClientComponent(UnorderedListFeatureClient)

View File

@@ -0,0 +1,37 @@
import { ListItemNode, ListNode } from '@lexical/list'
import type { FeatureProviderProviderServer } from '../../types'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { UnorderedListFeatureClientComponent } from './feature.client'
import { UNORDERED_LIST } from './markdownTransformer'
export const UnorderedListFeature: FeatureProviderProviderServer<undefined, undefined> = (
props,
) => {
return {
feature: () => {
return {
ClientComponent: UnorderedListFeatureClientComponent,
markdownTransformers: [UNORDERED_LIST],
nodes: [
{
converters: {
html: ListHTMLConverter,
},
node: ListNode,
},
{
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
},
],
serverFeatureProps: props,
}
},
key: 'unorderedlist',
serverFeatureProps: props,
}
}

View File

@@ -15,9 +15,9 @@ import { UnderlineFeature } from '../../../features/format/underline/feature.ser
import { HeadingFeature } from '../../../features/heading/feature.server'
import { IndentFeature } from '../../../features/indent/feature.server'
import { LinkFeature } from '../../../features/link/feature.server'
import { CheckListFeature } from '../../../features/lists/checklist'
import { OrderedListFeature } from '../../../features/lists/orderedlist'
import { UnorderedListFeature } from '../../../features/lists/unorderedlist'
import { CheckListFeature } from '../../../features/lists/checklist/feature.server'
import { OrderedListFeature } from '../../../features/lists/orderedlist/feature.server'
import { UnorderedListFeature } from '../../../features/lists/unorderedlist/feature.server'
import { ParagraphFeature } from '../../../features/paragraph'
import { RelationshipFeature } from '../../../features/relationship'
import { UploadFeature } from '../../../features/upload'

View File

@@ -291,9 +291,9 @@ export type {
SerializedAutoLinkNode,
SerializedLinkNode,
} from './field/features/link/nodes/types'
export { CheckListFeature } from './field/features/lists/checklist'
export { OrderedListFeature } from './field/features/lists/orderedlist'
export { UnorderedListFeature } from './field/features/lists/unorderedlist'
export { CheckListFeature } from './field/features/lists/checklist/feature.server'
export { OrderedListFeature } from './field/features/lists/orderedlist/feature.server'
export { UnorderedListFeature } from './field/features/lists/unorderedlist/feature.server'
export { LexicalPluginToLexicalFeature } from './field/features/migrations/lexicalPluginToLexical'
export { SlateToLexicalFeature } from './field/features/migrations/slateToLexical'
export { SlateBlockquoteConverter } from './field/features/migrations/slateToLexical/converter/converters/blockquote'

View File

@@ -3,16 +3,19 @@ import {
BlockQuoteFeature,
BlocksFeature,
BoldFeature,
CheckListFeature,
HeadingFeature,
IndentFeature,
InlineCodeFeature,
ItalicFeature,
LinkFeature,
OrderedListFeature,
StrikethroughFeature,
SubscriptFeature,
SuperscriptFeature,
TreeViewFeature,
UnderlineFeature,
UnorderedListFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import path from 'path'
@@ -91,6 +94,9 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
editor: lexicalEditor({
features: [
LinkFeature(),
CheckListFeature(),
UnorderedListFeature(),
OrderedListFeature(),
AlignFeature(),
BlockQuoteFeature(),
BoldFeature(),