feat(richtext-lexical): link markdown transformers (#6543)
Closes https://github.com/payloadcms/payload/issues/6507 --------- Co-authored-by: ShawnVogt <41651465+shawnvogt@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { LinkIcon } from '../../lexical/ui/icons/Link/index.js'
|
|||||||
import { getSelectedNode } from '../../lexical/utils/getSelectedNode.js'
|
import { getSelectedNode } from '../../lexical/utils/getSelectedNode.js'
|
||||||
import { createClientComponent } from '../createClientComponent.js'
|
import { createClientComponent } from '../createClientComponent.js'
|
||||||
import { toolbarFeatureButtonsGroupWithItems } from '../shared/toolbar/featureButtonsGroup.js'
|
import { toolbarFeatureButtonsGroupWithItems } from '../shared/toolbar/featureButtonsGroup.js'
|
||||||
|
import { LinkMarkdownTransformer } from './markdownTransformer.js'
|
||||||
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
||||||
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode.js'
|
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode.js'
|
||||||
import { AutoLinkPlugin } from './plugins/autoLink/index.js'
|
import { AutoLinkPlugin } from './plugins/autoLink/index.js'
|
||||||
@@ -84,6 +85,7 @@ const LinkFeatureClient: FeatureProviderProviderClient<ClientProps> = (props) =>
|
|||||||
clientFeatureProps: props,
|
clientFeatureProps: props,
|
||||||
feature: () => ({
|
feature: () => ({
|
||||||
clientFeatureProps: props,
|
clientFeatureProps: props,
|
||||||
|
markdownTransformers: [LinkMarkdownTransformer],
|
||||||
nodes: [LinkNode, AutoLinkNode],
|
nodes: [LinkNode, AutoLinkNode],
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js
|
|||||||
import { createNode } from '../typeUtilities.js'
|
import { createNode } from '../typeUtilities.js'
|
||||||
import { LinkFeatureClientComponent } from './feature.client.js'
|
import { LinkFeatureClientComponent } from './feature.client.js'
|
||||||
import { i18n } from './i18n.js'
|
import { i18n } from './i18n.js'
|
||||||
|
import { LinkMarkdownTransformer } from './markdownTransformer.js'
|
||||||
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
||||||
import { LinkNode } from './nodes/LinkNode.js'
|
import { LinkNode } from './nodes/LinkNode.js'
|
||||||
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
||||||
@@ -110,6 +111,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
return schemaMap
|
return schemaMap
|
||||||
},
|
},
|
||||||
i18n,
|
i18n,
|
||||||
|
markdownTransformers: [LinkMarkdownTransformer],
|
||||||
nodes: [
|
nodes: [
|
||||||
createNode({
|
createNode({
|
||||||
converters: {
|
converters: {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Code taken from https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/MarkdownTransformers.ts#L357
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Order of text transformers matters:
|
||||||
|
//
|
||||||
|
// - code should go first as it prevents any transformations inside
|
||||||
|
|
||||||
|
import type { TextMatchTransformer } from '@lexical/markdown'
|
||||||
|
|
||||||
|
import { $createTextNode, $isTextNode } from 'lexical'
|
||||||
|
|
||||||
|
import { $createLinkNode, $isLinkNode, LinkNode } from './nodes/LinkNode.js'
|
||||||
|
|
||||||
|
// - then longer tags match (e.g. ** or __ should go before * or _)
|
||||||
|
export const LinkMarkdownTransformer: TextMatchTransformer = {
|
||||||
|
type: 'text-match',
|
||||||
|
dependencies: [LinkNode],
|
||||||
|
export: (_node, exportChildren, exportFormat) => {
|
||||||
|
if (!$isLinkNode(_node)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const node: LinkNode = _node
|
||||||
|
const { url } = node.getFields()
|
||||||
|
const linkContent = `[${node.getTextContent()}](${url})`
|
||||||
|
const firstChild = node.getFirstChild()
|
||||||
|
// Add text styles only if link has single text node inside. If it's more
|
||||||
|
// then one we ignore it as markdown does not support nested styles for links
|
||||||
|
if (node.getChildrenSize() === 1 && $isTextNode(firstChild)) {
|
||||||
|
return exportFormat(firstChild, linkContent)
|
||||||
|
} else {
|
||||||
|
return linkContent
|
||||||
|
}
|
||||||
|
},
|
||||||
|
importRegExp: /\[([^[]+)\]\(([^()\s]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)/,
|
||||||
|
regExp: /\[([^[]+)\]\(([^()\s]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)$/,
|
||||||
|
replace: (textNode, match) => {
|
||||||
|
const [, linkText, linkUrl] = match
|
||||||
|
const linkNode = $createLinkNode({
|
||||||
|
fields: {
|
||||||
|
doc: null,
|
||||||
|
linkType: 'custom',
|
||||||
|
newTab: false,
|
||||||
|
url: linkUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const linkTextNode = $createTextNode(linkText)
|
||||||
|
linkTextNode.setFormat(textNode.getFormat())
|
||||||
|
linkNode.append(linkTextNode)
|
||||||
|
textNode.replace(linkNode)
|
||||||
|
},
|
||||||
|
trigger: ')',
|
||||||
|
}
|
||||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -1677,6 +1677,12 @@ importers:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@lexical/headless':
|
||||||
|
specifier: 0.15.0
|
||||||
|
version: 0.15.0
|
||||||
|
'@lexical/markdown':
|
||||||
|
specifier: 0.15.0
|
||||||
|
version: 0.15.0
|
||||||
'@payloadcms/db-mongodb':
|
'@payloadcms/db-mongodb':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/db-mongodb
|
version: link:../packages/db-mongodb
|
||||||
@@ -5585,7 +5591,6 @@ packages:
|
|||||||
'@lexical/selection': 0.15.0
|
'@lexical/selection': 0.15.0
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/code@0.15.0:
|
/@lexical/code@0.15.0:
|
||||||
resolution: {integrity: sha512-n185gjinGhz/M4BW1ayNPYAEgwW4T/NEFl2Wey/O+07W3zvh9k9ai7RjWd0c8Qzqc4DLlqvibvWPebWObQHA4w==}
|
resolution: {integrity: sha512-n185gjinGhz/M4BW1ayNPYAEgwW4T/NEFl2Wey/O+07W3zvh9k9ai7RjWd0c8Qzqc4DLlqvibvWPebWObQHA4w==}
|
||||||
@@ -5593,7 +5598,6 @@ packages:
|
|||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
prismjs: 1.29.0
|
prismjs: 1.29.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/devtools-core@0.15.0(react-dom@19.0.0-rc-f994737d14-20240522)(react@19.0.0-rc-f994737d14-20240522):
|
/@lexical/devtools-core@0.15.0(react-dom@19.0.0-rc-f994737d14-20240522)(react@19.0.0-rc-f994737d14-20240522):
|
||||||
resolution: {integrity: sha512-kK/IVEiQyqs2DsY4QRYFaFiKQMpaAukAl8PXmNeGTZ7cfFVsP29E4n0/pjY+oxmiRvxbO1s2i14q58nfuhj4VQ==}
|
resolution: {integrity: sha512-kK/IVEiQyqs2DsY4QRYFaFiKQMpaAukAl8PXmNeGTZ7cfFVsP29E4n0/pjY+oxmiRvxbO1s2i14q58nfuhj4VQ==}
|
||||||
@@ -5636,7 +5640,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-soLjCphUEHw+z2ulV9cOtisTWmGj6k7TU+O/6nzgn7E1FlvskrrykGhYFrXDsXqB1wJRaILHKlHxQSoNzf931A==}
|
resolution: {integrity: sha512-soLjCphUEHw+z2ulV9cOtisTWmGj6k7TU+O/6nzgn7E1FlvskrrykGhYFrXDsXqB1wJRaILHKlHxQSoNzf931A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/history@0.15.0:
|
/@lexical/history@0.15.0:
|
||||||
resolution: {integrity: sha512-r+pzR2k/51AL6l8UfXeVe/GWPIeWY1kEOuKx9nsYB9tmAkTF66tTFz33DJIMWBVtAHWN7Dcdv0/yy6q8R6CAUQ==}
|
resolution: {integrity: sha512-r+pzR2k/51AL6l8UfXeVe/GWPIeWY1kEOuKx9nsYB9tmAkTF66tTFz33DJIMWBVtAHWN7Dcdv0/yy6q8R6CAUQ==}
|
||||||
@@ -5651,21 +5654,18 @@ packages:
|
|||||||
'@lexical/selection': 0.15.0
|
'@lexical/selection': 0.15.0
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/link@0.15.0:
|
/@lexical/link@0.15.0:
|
||||||
resolution: {integrity: sha512-KBV/zWk5FxqZGNcq3IKGBDCcS4t0uteU1osAIG+pefo4waTkOOgibxxEJDop2QR5wtjkYva3Qp0D8ZyJDMMMlw==}
|
resolution: {integrity: sha512-KBV/zWk5FxqZGNcq3IKGBDCcS4t0uteU1osAIG+pefo4waTkOOgibxxEJDop2QR5wtjkYva3Qp0D8ZyJDMMMlw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/list@0.15.0:
|
/@lexical/list@0.15.0:
|
||||||
resolution: {integrity: sha512-JuF4k7uo4rZFOSZGrmkxo1+sUrwTKNBhhJAiCgtM+6TO90jppxzCFNKur81yPzF1+g4GWLC9gbjzKb52QPb6cQ==}
|
resolution: {integrity: sha512-JuF4k7uo4rZFOSZGrmkxo1+sUrwTKNBhhJAiCgtM+6TO90jppxzCFNKur81yPzF1+g4GWLC9gbjzKb52QPb6cQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/mark@0.15.0:
|
/@lexical/mark@0.15.0:
|
||||||
resolution: {integrity: sha512-cdePA98sOJRc4/HHqcOcPBFq4UDwzaFJOK1N1E6XUGcXH1GU8zHtV1ElTgmbsGkyjBRwhR+OqKm9eso1PBOUkg==}
|
resolution: {integrity: sha512-cdePA98sOJRc4/HHqcOcPBFq4UDwzaFJOK1N1E6XUGcXH1GU8zHtV1ElTgmbsGkyjBRwhR+OqKm9eso1PBOUkg==}
|
||||||
@@ -5684,7 +5684,6 @@ packages:
|
|||||||
'@lexical/text': 0.15.0
|
'@lexical/text': 0.15.0
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/offset@0.15.0:
|
/@lexical/offset@0.15.0:
|
||||||
resolution: {integrity: sha512-VO1f3m8+RRdRjuXMtCBhi1COVKRC2LhP8AFYxnFlvbV+Waz9R5xB9pqFFUe4RtyqyTLmOUj6+LtsUFhq+23voQ==}
|
resolution: {integrity: sha512-VO1f3m8+RRdRjuXMtCBhi1COVKRC2LhP8AFYxnFlvbV+Waz9R5xB9pqFFUe4RtyqyTLmOUj6+LtsUFhq+23voQ==}
|
||||||
@@ -5746,26 +5745,22 @@ packages:
|
|||||||
'@lexical/selection': 0.15.0
|
'@lexical/selection': 0.15.0
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/selection@0.15.0:
|
/@lexical/selection@0.15.0:
|
||||||
resolution: {integrity: sha512-S+AQC6eJiQYSa5zOPuecN85prCT0Bcb8miOdJaE17Zh+vgdUH5gk9I0tEBeG5T7tkSpq6lFiEqs2FZSfaHflbQ==}
|
resolution: {integrity: sha512-S+AQC6eJiQYSa5zOPuecN85prCT0Bcb8miOdJaE17Zh+vgdUH5gk9I0tEBeG5T7tkSpq6lFiEqs2FZSfaHflbQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/table@0.15.0:
|
/@lexical/table@0.15.0:
|
||||||
resolution: {integrity: sha512-3IRBg8IoIHetqKozRQbJQ2aPyG0ziXZ+lc8TOIAGs6METW/wxntaV+rTNrODanKAgvk2iJTIyfFkYjsqS9+VFg==}
|
resolution: {integrity: sha512-3IRBg8IoIHetqKozRQbJQ2aPyG0ziXZ+lc8TOIAGs6METW/wxntaV+rTNrODanKAgvk2iJTIyfFkYjsqS9+VFg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lexical/utils': 0.15.0
|
'@lexical/utils': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/text@0.15.0:
|
/@lexical/text@0.15.0:
|
||||||
resolution: {integrity: sha512-WsAkAt9T1RH1iDrVuWeoRUeMCOAWar5oSFtnQ4m9vhT/zuf5b8efK87GiqCH00ZAn4DGzOuAfyXlMFqBVCQdkQ==}
|
resolution: {integrity: sha512-WsAkAt9T1RH1iDrVuWeoRUeMCOAWar5oSFtnQ4m9vhT/zuf5b8efK87GiqCH00ZAn4DGzOuAfyXlMFqBVCQdkQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/utils@0.15.0:
|
/@lexical/utils@0.15.0:
|
||||||
resolution: {integrity: sha512-/6954LDmTcVFgexhy5WOZDa4TxNQOEZNrf8z7TRAFiAQkihcME/GRoq1en5cbXoVNF8jv5AvNyyc7x0MByRJ6A==}
|
resolution: {integrity: sha512-/6954LDmTcVFgexhy5WOZDa4TxNQOEZNrf8z7TRAFiAQkihcME/GRoq1en5cbXoVNF8jv5AvNyyc7x0MByRJ6A==}
|
||||||
@@ -5774,7 +5769,6 @@ packages:
|
|||||||
'@lexical/selection': 0.15.0
|
'@lexical/selection': 0.15.0
|
||||||
'@lexical/table': 0.15.0
|
'@lexical/table': 0.15.0
|
||||||
lexical: 0.15.0
|
lexical: 0.15.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@lexical/yjs@0.15.0(yjs@13.6.14):
|
/@lexical/yjs@0.15.0(yjs@13.6.14):
|
||||||
resolution: {integrity: sha512-Rf4AIu620Cq90li6GU58gkzlGRdntHP4ZeZrbJ3ToW7vEEnkW6Wl9/HhO647GG4OL5w46M0iWvx1b1b8xjYT1w==}
|
resolution: {integrity: sha512-Rf4AIu620Cq90li6GU58gkzlGRdntHP4ZeZrbJ3ToW7vEEnkW6Wl9/HhO647GG4OL5w46M0iWvx1b1b8xjYT1w==}
|
||||||
@@ -15437,7 +15431,6 @@ packages:
|
|||||||
/prismjs@1.29.0:
|
/prismjs@1.29.0:
|
||||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/process-nextick-args@2.0.1:
|
/process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import type { ServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||||
|
import type { SerializedEditorState } from 'lexical'
|
||||||
import type { CollectionConfig } from 'payload/types'
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { createHeadlessEditor } from '@lexical/headless'
|
||||||
|
import { $convertToMarkdownString } from '@lexical/markdown'
|
||||||
|
import { getEnabledNodes } from '@payloadcms/richtext-lexical'
|
||||||
|
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||||
import {
|
import {
|
||||||
BlocksFeature,
|
BlocksFeature,
|
||||||
FixedToolbarFeature,
|
FixedToolbarFeature,
|
||||||
@@ -7,6 +13,7 @@ import {
|
|||||||
LinkFeature,
|
LinkFeature,
|
||||||
TreeViewFeature,
|
TreeViewFeature,
|
||||||
UploadFeature,
|
UploadFeature,
|
||||||
|
defaultEditorFeatures,
|
||||||
lexicalEditor,
|
lexicalEditor,
|
||||||
} from '@payloadcms/richtext-lexical'
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
@@ -23,6 +30,58 @@ import {
|
|||||||
UploadAndRichTextBlock,
|
UploadAndRichTextBlock,
|
||||||
} from './blocks.js'
|
} from './blocks.js'
|
||||||
|
|
||||||
|
const editorConfig: ServerEditorConfig = {
|
||||||
|
features: [
|
||||||
|
...defaultEditorFeatures,
|
||||||
|
//TestRecorderFeature(),
|
||||||
|
TreeViewFeature(),
|
||||||
|
//HTMLConverterFeature(),
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
LinkFeature({
|
||||||
|
fields: ({ defaultFields }) => [
|
||||||
|
...defaultFields,
|
||||||
|
{
|
||||||
|
name: 'rel',
|
||||||
|
label: 'Rel Attribute',
|
||||||
|
type: 'select',
|
||||||
|
hasMany: true,
|
||||||
|
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
UploadFeature({
|
||||||
|
collections: {
|
||||||
|
uploads: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'caption',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
RichTextBlock,
|
||||||
|
TextBlock,
|
||||||
|
UploadAndRichTextBlock,
|
||||||
|
SelectFieldBlock,
|
||||||
|
RelationshipBlock,
|
||||||
|
RelationshipHasManyBlock,
|
||||||
|
SubBlockBlock,
|
||||||
|
RadioButtonsBlock,
|
||||||
|
ConditionalLayoutBlock,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
export const LexicalFields: CollectionConfig = {
|
export const LexicalFields: CollectionConfig = {
|
||||||
slug: lexicalFieldsSlug,
|
slug: lexicalFieldsSlug,
|
||||||
admin: {
|
admin: {
|
||||||
@@ -70,56 +129,44 @@ export const LexicalFields: CollectionConfig = {
|
|||||||
admin: {
|
admin: {
|
||||||
hideGutter: false,
|
hideGutter: false,
|
||||||
},
|
},
|
||||||
features: ({ defaultFeatures }) => [
|
features: editorConfig.features,
|
||||||
...defaultFeatures,
|
|
||||||
//TestRecorderFeature(),
|
|
||||||
TreeViewFeature(),
|
|
||||||
//HTMLConverterFeature(),
|
|
||||||
FixedToolbarFeature(),
|
|
||||||
LinkFeature({
|
|
||||||
fields: ({ defaultFields }) => [
|
|
||||||
...defaultFields,
|
|
||||||
{
|
|
||||||
name: 'rel',
|
|
||||||
label: 'Rel Attribute',
|
|
||||||
type: 'select',
|
|
||||||
hasMany: true,
|
|
||||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
|
||||||
admin: {
|
|
||||||
description:
|
|
||||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
UploadFeature({
|
|
||||||
collections: {
|
|
||||||
uploads: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'caption',
|
|
||||||
type: 'richText',
|
|
||||||
editor: lexicalEditor(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
BlocksFeature({
|
|
||||||
blocks: [
|
|
||||||
RichTextBlock,
|
|
||||||
TextBlock,
|
|
||||||
UploadAndRichTextBlock,
|
|
||||||
SelectFieldBlock,
|
|
||||||
RelationshipBlock,
|
|
||||||
RelationshipHasManyBlock,
|
|
||||||
SubBlockBlock,
|
|
||||||
RadioButtonsBlock,
|
|
||||||
ConditionalLayoutBlock,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'lexicalWithBlocks_markdown',
|
||||||
|
type: 'textarea',
|
||||||
|
hooks: {
|
||||||
|
afterRead: [
|
||||||
|
async ({ data, req, siblingData }) => {
|
||||||
|
const yourSanitizedEditorConfig = await sanitizeServerEditorConfig(
|
||||||
|
editorConfig,
|
||||||
|
req.payload.config,
|
||||||
|
)
|
||||||
|
|
||||||
|
const headlessEditor = createHeadlessEditor({
|
||||||
|
nodes: getEnabledNodes({
|
||||||
|
editorConfig: yourSanitizedEditorConfig,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const yourEditorState: SerializedEditorState = siblingData.lexicalWithBlocks
|
||||||
|
try {
|
||||||
|
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||||
|
} catch (e) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to markdown
|
||||||
|
let markdown: string
|
||||||
|
headlessEditor.getEditorState().read(() => {
|
||||||
|
markdown = $convertToMarkdownString(
|
||||||
|
yourSanitizedEditorConfig?.features?.markdownTransformers,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return markdown
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,62 @@ describe('Lexical', () => {
|
|||||||
expect((uploadNode.value.media as any).filename).toStrictEqual('payload.png')
|
expect((uploadNode.value.media as any).filename).toStrictEqual('payload.png')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('ensure link nodes convert to markdown', async () => {
|
||||||
|
const newLexicalDoc = await payload.create({
|
||||||
|
collection: lexicalFieldsSlug,
|
||||||
|
data: {
|
||||||
|
title: 'Lexical Markdown Test',
|
||||||
|
lexicalWithBlocks: {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
detail: 0,
|
||||||
|
format: 0,
|
||||||
|
mode: 'normal',
|
||||||
|
style: '',
|
||||||
|
text: 'link to payload',
|
||||||
|
type: 'text',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'autolink',
|
||||||
|
version: 2,
|
||||||
|
fields: {
|
||||||
|
linkType: 'custom',
|
||||||
|
url: 'https://payloadcms.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
type: 'paragraph',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newLexicalDoc.lexicalWithBlocks_markdown).toEqual(
|
||||||
|
'[link to payload](https://payloadcms.com)',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
describe('converters and migrations', () => {
|
describe('converters and migrations', () => {
|
||||||
it('htmlConverter: should output correct HTML for top-level lexical field', async () => {
|
it('htmlConverter: should output correct HTML for top-level lexical field', async () => {
|
||||||
const lexicalDoc: LexicalMigrateField = (
|
const lexicalDoc: LexicalMigrateField = (
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export interface LexicalField {
|
|||||||
};
|
};
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
|
lexicalWithBlocks_markdown?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -851,7 +852,7 @@ export interface GroupField {
|
|||||||
nestedField?: string | null;
|
nestedField?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
groups: {
|
groups?: {
|
||||||
groupInRow?: {
|
groupInRow?: {
|
||||||
field?: string | null;
|
field?: string | null;
|
||||||
secondField?: string | null;
|
secondField?: string | null;
|
||||||
@@ -1207,16 +1208,16 @@ export interface TabsField {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
};
|
};
|
||||||
namedTabWithDefaultValue: {
|
namedTabWithDefaultValue?: {
|
||||||
defaultValue?: string | null;
|
defaultValue?: string | null;
|
||||||
};
|
};
|
||||||
localizedTab: {
|
localizedTab?: {
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
};
|
};
|
||||||
accessControlTab: {
|
accessControlTab?: {
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
};
|
};
|
||||||
hooksTab: {
|
hooksTab?: {
|
||||||
beforeValidate?: boolean | null;
|
beforeValidate?: boolean | null;
|
||||||
beforeChange?: boolean | null;
|
beforeChange?: boolean | null;
|
||||||
afterChange?: boolean | null;
|
afterChange?: boolean | null;
|
||||||
@@ -1224,7 +1225,7 @@ export interface TabsField {
|
|||||||
};
|
};
|
||||||
textarea?: string | null;
|
textarea?: string | null;
|
||||||
anotherText: string;
|
anotherText: string;
|
||||||
nestedTab: {
|
nestedTab?: {
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
};
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -1262,6 +1263,8 @@ export interface Upload {
|
|||||||
filesize?: number | null;
|
filesize?: number | null;
|
||||||
width?: number | null;
|
width?: number | null;
|
||||||
height?: number | null;
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -1280,6 +1283,8 @@ export interface Uploads2 {
|
|||||||
filesize?: number | null;
|
filesize?: number | null;
|
||||||
width?: number | null;
|
width?: number | null;
|
||||||
height?: number | null;
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -1312,6 +1317,8 @@ export interface Uploads3 {
|
|||||||
filesize?: number | null;
|
filesize?: number | null;
|
||||||
width?: number | null;
|
width?: number | null;
|
||||||
height?: number | null;
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -1363,7 +1370,7 @@ export interface PayloadMigration {
|
|||||||
*/
|
*/
|
||||||
export interface TabsWithRichText {
|
export interface TabsWithRichText {
|
||||||
id: string;
|
id: string;
|
||||||
tab1: {
|
tab1?: {
|
||||||
rt1?: {
|
rt1?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -1380,7 +1387,7 @@ export interface TabsWithRichText {
|
|||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
tab2: {
|
tab2?: {
|
||||||
rt2?: {
|
rt2?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -1413,6 +1420,6 @@ export interface LexicalBlocksRadioButtonsBlock {
|
|||||||
|
|
||||||
|
|
||||||
declare module 'payload' {
|
declare module 'payload' {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export interface GeneratedTypes extends Config {}
|
export interface GeneratedTypes extends Config {}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
"typecheck": "pnpm turbo build --filter test && tsc --project tsconfig.typecheck.json"
|
"typecheck": "pnpm turbo build --filter test && tsc --project tsconfig.typecheck.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@lexical/headless": "0.15.0",
|
||||||
|
"@lexical/markdown": "0.15.0",
|
||||||
"@payloadcms/db-mongodb": "workspace:*",
|
"@payloadcms/db-mongodb": "workspace:*",
|
||||||
"@payloadcms/db-postgres": "workspace:*",
|
"@payloadcms/db-postgres": "workspace:*",
|
||||||
"@payloadcms/email-nodemailer": "workspace:*",
|
"@payloadcms/email-nodemailer": "workspace:*",
|
||||||
|
|||||||
Reference in New Issue
Block a user