Compare commits
13 Commits
revert-121
...
feat/TextA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
799aadf54d | ||
|
|
8a941b3842 | ||
|
|
659744039f | ||
|
|
05422c9ba0 | ||
|
|
7259e457d6 | ||
|
|
643e333659 | ||
|
|
e8d8bb04c3 | ||
|
|
b6046a3100 | ||
|
|
2e4911a642 | ||
|
|
30a0e192fa | ||
|
|
affeea187d | ||
|
|
18daa90e69 | ||
|
|
aafc628650 |
@@ -15,6 +15,7 @@ export { StrikethroughFeatureClient } from '../../features/format/strikethrough/
|
||||
export { SubscriptFeatureClient } from '../../features/format/subscript/feature.client.js'
|
||||
export { SuperscriptFeatureClient } from '../../features/format/superscript/feature.client.js'
|
||||
export { UnderlineFeatureClient } from '../../features/format/underline/feature.client.js'
|
||||
export { TextColorFeatureClient } from '../../features/textColor/feature.client.js'
|
||||
export { HeadingFeatureClient } from '../../features/heading/client/index.js'
|
||||
export { HorizontalRuleFeatureClient } from '../../features/horizontalRule/client/index.js'
|
||||
export { IndentFeatureClient } from '../../features/indent/client/index.js'
|
||||
@@ -88,7 +89,7 @@ export {
|
||||
|
||||
export { ENABLE_SLASH_MENU_COMMAND } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.js'
|
||||
|
||||
export { getEnabledNodes } from '../../lexical/nodes/index.js'
|
||||
export { getEnabledNodes } from '../../lexical/nodes/utils.js'
|
||||
|
||||
export {
|
||||
$createUploadNode,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { $parseSerializedNode } from 'lexical'
|
||||
|
||||
import type { NodeWithHooks } from '../../typesServer.js'
|
||||
|
||||
import { getEnabledNodesFromServerNodes } from '../../../lexical/nodes/index.js'
|
||||
import { getEnabledNodesFromServerNodes } from '../../../lexical/nodes/utils.js'
|
||||
import {
|
||||
$convertToMarkdownString,
|
||||
type MultilineElementTransformer,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
|
||||
import { $getSelection, $isRangeSelection } from 'lexical'
|
||||
|
||||
import type { ToolbarDropdownGroup, ToolbarGroup } from '../toolbars/types.js'
|
||||
import type { TextColorFeatureProps } from './feature.server.js'
|
||||
|
||||
import { $mutateSelectedTextNodes } from '../../lexical/nodes/utils.js'
|
||||
import { BgColorIcon } from '../../lexical/ui/icons/BgColor/index.js'
|
||||
import { TextColorIcon } from '../../lexical/ui/icons/TextColor/index.js'
|
||||
import './index.scss'
|
||||
import { createClientFeature } from '../../utilities/createClientFeature.js'
|
||||
|
||||
const defaultColors = [
|
||||
{ classSuffix: 'red', label: 'Red' },
|
||||
{ classSuffix: 'blue', label: 'Blue' },
|
||||
{ classSuffix: 'green', label: 'Green' },
|
||||
{ classSuffix: 'yellow', label: 'Yellow' },
|
||||
{ classSuffix: 'purple', label: 'Purple' },
|
||||
{ classSuffix: 'pink', label: 'Pink' },
|
||||
{ classSuffix: 'orange', label: 'Orange' },
|
||||
{ classSuffix: 'gray', label: 'Gray' },
|
||||
]
|
||||
|
||||
const toolbarGroups = (props: TextColorFeatureProps): ToolbarGroup[] => {
|
||||
// TO-DO: I would rather have defaultBgColors but no defaultColors
|
||||
const { bgColors = defaultColors, colors = defaultColors } = props
|
||||
const colorItems: ToolbarDropdownGroup['items'] | undefined = colors?.map(({ classSuffix }) => {
|
||||
return {
|
||||
ChildComponent: TextColorIcon,
|
||||
key: `color-${classSuffix}`,
|
||||
label: classSuffix,
|
||||
onSelect: ({ editor }) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
$mutateSelectedTextNodes(selection, (textNode) => {
|
||||
textNode.mutateClasses((classes) => {
|
||||
classes['color'] = classSuffix
|
||||
delete classes['bgColor'] // TO-DECIDE: this should be like this be default? configurable in props?
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
order: 1,
|
||||
}
|
||||
})
|
||||
const bgColorItems: ToolbarDropdownGroup['items'] = bgColors.map(({ classSuffix }) => {
|
||||
return {
|
||||
ChildComponent: TextColorIcon,
|
||||
key: `bg-color-${classSuffix}`,
|
||||
label: classSuffix,
|
||||
onSelect: ({ editor }) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
$mutateSelectedTextNodes(selection, (textNode) => {
|
||||
textNode.mutateClasses((classes) => {
|
||||
// TO-DECIDE: prefix should be only bg?
|
||||
classes['bg-color'] = classSuffix
|
||||
delete classes['color']
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
order: 1,
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'dropdown' as const,
|
||||
ChildComponent: TextColorIcon,
|
||||
items: colorItems,
|
||||
key: 'color',
|
||||
order: 30,
|
||||
},
|
||||
{
|
||||
type: 'dropdown' as const,
|
||||
ChildComponent: BgColorIcon,
|
||||
items: bgColorItems,
|
||||
key: 'bg-color',
|
||||
order: 30,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const TextColorFeatureClient = createClientFeature<TextColorFeatureProps>(({ props }) => {
|
||||
return {
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups(props),
|
||||
},
|
||||
toolbarInline: {
|
||||
groups: toolbarGroups(props),
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createServerFeature } from '../../index.js'
|
||||
|
||||
export type TextColorFeatureProps = {
|
||||
bgColors?: { classSuffix: string; label: string }[]
|
||||
/**
|
||||
* TO-DO: see i18n patterns. The user should provide it in label
|
||||
*/
|
||||
colors?: { classSuffix: string; label: string }[]
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const TextColorFeature = createServerFeature<
|
||||
TextColorFeatureProps,
|
||||
TextColorFeatureProps,
|
||||
TextColorFeatureProps
|
||||
>({
|
||||
feature: ({ props }) => {
|
||||
return {
|
||||
ClientFeature: '@payloadcms/richtext-lexical/client#TextColorFeatureClient',
|
||||
clientFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'textColor',
|
||||
})
|
||||
23
packages/richtext-lexical/src/features/textColor/index.scss
Normal file
23
packages/richtext-lexical/src/features/textColor/index.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.color-green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.color-blue {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.color-red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bg-color-green {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.bg-color-blue {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.bg-color-red {
|
||||
background-color: red;
|
||||
}
|
||||
@@ -848,22 +848,22 @@ export { UnderlineFeature } from './features/format/underline/feature.server.js'
|
||||
export { HeadingFeature, type HeadingFeatureProps } from './features/heading/server/index.js'
|
||||
export { HorizontalRuleFeature } from './features/horizontalRule/server/index.js'
|
||||
export { IndentFeature } from './features/indent/server/index.js'
|
||||
|
||||
export { AutoLinkNode } from './features/link/nodes/AutoLinkNode.js'
|
||||
|
||||
export { LinkNode } from './features/link/nodes/LinkNode.js'
|
||||
|
||||
export type { LinkFields } from './features/link/nodes/types.js'
|
||||
export { LinkFeature, type LinkFeatureServerProps } from './features/link/server/index.js'
|
||||
export { ChecklistFeature } from './features/lists/checklist/server/index.js'
|
||||
export { OrderedListFeature } from './features/lists/orderedList/server/index.js'
|
||||
export { UnorderedListFeature } from './features/lists/unorderedList/server/index.js'
|
||||
|
||||
export type {
|
||||
SlateNode,
|
||||
SlateNodeConverter,
|
||||
} from './features/migrations/slateToLexical/converter/types.js'
|
||||
|
||||
export { ParagraphFeature } from './features/paragraph/server/index.js'
|
||||
|
||||
export {
|
||||
RelationshipFeature,
|
||||
type RelationshipFeatureProps,
|
||||
@@ -872,6 +872,7 @@ export {
|
||||
type RelationshipData,
|
||||
RelationshipServerNode,
|
||||
} from './features/relationship/server/nodes/RelationshipNode.js'
|
||||
export { TextColorFeature } from './features/textColor/feature.server.js'
|
||||
|
||||
export { FixedToolbarFeature } from './features/toolbars/fixed/server/index.js'
|
||||
export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js'
|
||||
@@ -938,7 +939,7 @@ export type {
|
||||
SanitizedServerEditorConfig,
|
||||
ServerEditorConfig,
|
||||
} from './lexical/config/types.js'
|
||||
export { getEnabledNodes, getEnabledNodesFromServerNodes } from './lexical/nodes/index.js'
|
||||
export { getEnabledNodes, getEnabledNodesFromServerNodes } from './lexical/nodes/utils.js'
|
||||
export type { AdapterProps }
|
||||
|
||||
export type {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useEditorConfigContext,
|
||||
} from './config/client/EditorConfigProvider.js'
|
||||
import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor.js'
|
||||
import { getEnabledNodes } from './nodes/index.js'
|
||||
import { getEnabledNodes } from './nodes/utils.js'
|
||||
|
||||
export type LexicalProviderProps = {
|
||||
composerKey: string
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical'
|
||||
|
||||
import { TextNode as LexicalTextNode } from 'lexical'
|
||||
|
||||
import type {
|
||||
ResolvedClientFeatureMap,
|
||||
SanitizedClientFeatures,
|
||||
@@ -9,6 +11,8 @@ import type {
|
||||
import type { LexicalFieldAdminProps } from '../../../types.js'
|
||||
import type { SanitizedClientEditorConfig } from '../types.js'
|
||||
|
||||
import { TextNode } from '../../nodes/TextNode.js'
|
||||
|
||||
export const sanitizeClientFeatures = (
|
||||
features: ResolvedClientFeatureMap,
|
||||
): SanitizedClientFeatures => {
|
||||
@@ -16,7 +20,10 @@ export const sanitizeClientFeatures = (
|
||||
enabledFeatures: [],
|
||||
enabledFormats: [],
|
||||
markdownTransformers: [],
|
||||
nodes: [],
|
||||
nodes: [
|
||||
TextNode,
|
||||
{ replace: LexicalTextNode, with: (node) => new TextNode(node.__text), withKlass: TextNode },
|
||||
],
|
||||
plugins: [],
|
||||
providers: [],
|
||||
slashMenu: {
|
||||
|
||||
76
packages/richtext-lexical/src/lexical/nodes/TextNode.ts
Normal file
76
packages/richtext-lexical/src/lexical/nodes/TextNode.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { EditorConfig, SerializedTextNode as SerializedLexicalTextNode } from 'lexical'
|
||||
|
||||
import { TextNode as LexicalTextNode } from 'lexical'
|
||||
|
||||
type MutableClasses = { [classSuffix: string]: boolean | string }
|
||||
type ReadOnlyClasses = { readonly [classSuffix: string]: boolean | string }
|
||||
|
||||
type SerializedTextNode = { classes?: ReadOnlyClasses } & SerializedLexicalTextNode
|
||||
|
||||
export class TextNode extends LexicalTextNode {
|
||||
/** Don't use this directly, use `this.getClasses()` and `this.mutateClasses()` instead */
|
||||
private __classes: ReadOnlyClasses = {}
|
||||
|
||||
static clone(node: TextNode) {
|
||||
const clonedNode = new TextNode(node.__text, node.__key)
|
||||
clonedNode.__classes = node.__classes
|
||||
return clonedNode
|
||||
}
|
||||
|
||||
static getType() {
|
||||
return '$text'
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): TextNode {
|
||||
const textNode = new TextNode(serializedNode.text)
|
||||
textNode.__classes = serializedNode.classes || {}
|
||||
return textNode
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig) {
|
||||
const dom = super.createDOM(config)
|
||||
// add classes to the text node
|
||||
if (this.__classes) {
|
||||
Object.entries(this.__classes).forEach(([classPrefix, classSufix]) => {
|
||||
if (typeof classSufix === 'string') {
|
||||
dom.classList.add(`${classPrefix}-${classSufix}`)
|
||||
} else if (typeof classSufix === 'boolean' && classSufix) {
|
||||
dom.classList.add(classPrefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return dom
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTextNode {
|
||||
const classes = Object.fromEntries(
|
||||
Object.entries(this.__classes).filter(([_, value]) => value !== undefined || value === false),
|
||||
)
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: TextNode.getType(),
|
||||
...(Object.keys(classes).keys.length > 0 && { classes: this.__classes }),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object of classes with the form `suffix: prefix`.
|
||||
* For example, the key-value `bg-color: “blue”`, will cause the node to be rendered with the class `bg-color-blue`.
|
||||
* This method is only for reading the classes object. If you need to mutate it use `muteClasses`.
|
||||
*/
|
||||
getClasses() {
|
||||
const self = this.getLatest()
|
||||
return self.__classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to mutate the object of classes whose form is `suffix: prefix`.
|
||||
* For example, the key-value `bg-color: “blue”`, will cause the node to be rendered with the class `bg-color-blue`.
|
||||
* @param fn A function that receives the current classes object and allows to mutate it.
|
||||
*/
|
||||
mutateClasses(fn: (currentClasses: MutableClasses) => void) {
|
||||
const self = this.getWritable()
|
||||
fn(self.__classes)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Klass, LexicalNode, LexicalNodeReplacement } from 'lexical'
|
||||
|
||||
import type { NodeWithHooks } from '../../features/typesServer.js'
|
||||
import type { SanitizedClientEditorConfig, SanitizedServerEditorConfig } from '../config/types.js'
|
||||
|
||||
export function getEnabledNodes({
|
||||
editorConfig,
|
||||
}: {
|
||||
editorConfig: SanitizedClientEditorConfig | SanitizedServerEditorConfig
|
||||
}): Array<Klass<LexicalNode> | LexicalNodeReplacement> {
|
||||
return getEnabledNodesFromServerNodes({
|
||||
nodes: editorConfig.features.nodes,
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnabledNodesFromServerNodes({
|
||||
nodes,
|
||||
}: {
|
||||
nodes: Array<Klass<LexicalNode> | LexicalNodeReplacement> | Array<NodeWithHooks>
|
||||
}): Array<Klass<LexicalNode> | LexicalNodeReplacement> {
|
||||
return nodes.map((node) => {
|
||||
if ('node' in node) {
|
||||
return node.node
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
169
packages/richtext-lexical/src/lexical/nodes/utils.ts
Normal file
169
packages/richtext-lexical/src/lexical/nodes/utils.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { BaseSelection, Klass, LexicalNode, LexicalNodeReplacement } from 'lexical'
|
||||
|
||||
import { $isTextNode, $isTokenOrSegmented } from 'lexical'
|
||||
|
||||
import type { NodeWithHooks } from '../../features/typesServer.js'
|
||||
import type { SanitizedClientEditorConfig, SanitizedServerEditorConfig } from '../config/types.js'
|
||||
import type { TextNode } from './TextNode.js'
|
||||
|
||||
export function getEnabledNodes({
|
||||
editorConfig,
|
||||
}: {
|
||||
editorConfig: SanitizedClientEditorConfig | SanitizedServerEditorConfig
|
||||
}): Array<Klass<LexicalNode> | LexicalNodeReplacement> {
|
||||
return getEnabledNodesFromServerNodes({
|
||||
nodes: editorConfig.features.nodes,
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnabledNodesFromServerNodes({
|
||||
nodes,
|
||||
}: {
|
||||
nodes: Array<Klass<LexicalNode> | LexicalNodeReplacement> | Array<NodeWithHooks>
|
||||
}): Array<Klass<LexicalNode> | LexicalNodeReplacement> {
|
||||
return nodes.map((node) => {
|
||||
if ('node' in node) {
|
||||
return node.node
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Will update partially selected TextNodes by splitting the TextNode and applying
|
||||
* the callback to the appropriate one.
|
||||
* @param selection - The selected node(s) to update.
|
||||
* @param fn - The callback to apply to the selected TextNodes.
|
||||
*/
|
||||
export function $mutateSelectedTextNodes(
|
||||
selection: BaseSelection,
|
||||
fn: (textNode: TextNode) => void,
|
||||
): void {
|
||||
const selectedNodes = selection.getNodes()
|
||||
const selectedNodesLength = selectedNodes.length
|
||||
const anchorAndFocus = selection.getStartEndPoints()
|
||||
if (anchorAndFocus === null) {
|
||||
return
|
||||
}
|
||||
const [anchor, focus] = anchorAndFocus
|
||||
|
||||
const lastIndex = selectedNodesLength - 1
|
||||
let firstNode = selectedNodes[0]
|
||||
let lastNode = selectedNodes[lastIndex]
|
||||
|
||||
const firstNodeText = firstNode.getTextContent()
|
||||
const firstNodeTextLength = firstNodeText.length
|
||||
const focusOffset = focus.offset
|
||||
let anchorOffset = anchor.offset
|
||||
const isBefore = anchor.isBefore(focus)
|
||||
let startOffset = isBefore ? anchorOffset : focusOffset
|
||||
let endOffset = isBefore ? focusOffset : anchorOffset
|
||||
const startType = isBefore ? anchor.type : focus.type
|
||||
const endType = isBefore ? focus.type : anchor.type
|
||||
const endKey = isBefore ? focus.key : anchor.key
|
||||
|
||||
// This is the case where the user only selected the very end of the
|
||||
// first node so we don't want to include it in the formatting change.
|
||||
if ($isTextNode(firstNode) && startOffset === firstNodeTextLength) {
|
||||
const nextSibling = firstNode.getNextSibling()
|
||||
|
||||
if ($isTextNode(nextSibling)) {
|
||||
// we basically make the second node the firstNode, changing offsets accordingly
|
||||
anchorOffset = 0
|
||||
startOffset = 0
|
||||
firstNode = nextSibling
|
||||
}
|
||||
}
|
||||
|
||||
// This is the case where we only selected a single node
|
||||
if (selectedNodes.length === 1) {
|
||||
if ($isTextNode(firstNode) && firstNode.canHaveFormat()) {
|
||||
startOffset =
|
||||
startType === 'element' ? 0 : anchorOffset > focusOffset ? focusOffset : anchorOffset
|
||||
endOffset =
|
||||
endType === 'element'
|
||||
? firstNodeTextLength
|
||||
: anchorOffset > focusOffset
|
||||
? anchorOffset
|
||||
: focusOffset
|
||||
|
||||
// No actual text is selected, so do nothing.
|
||||
if (startOffset === endOffset) {
|
||||
return
|
||||
}
|
||||
|
||||
// The entire node is selected or a token/segment, so just format it
|
||||
if (
|
||||
$isTokenOrSegmented(firstNode) ||
|
||||
(startOffset === 0 && endOffset === firstNodeTextLength)
|
||||
) {
|
||||
fn(firstNode as TextNode)
|
||||
firstNode.select(startOffset, endOffset)
|
||||
} else {
|
||||
// The node is partially selected, so split it into two nodes
|
||||
// and style the selected one.
|
||||
const splitNodes = firstNode.splitText(startOffset, endOffset)
|
||||
const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1]
|
||||
fn(replacement as TextNode)
|
||||
replacement.select(0, endOffset - startOffset)
|
||||
}
|
||||
} // multiple nodes selected.
|
||||
} else {
|
||||
if (
|
||||
$isTextNode(firstNode) &&
|
||||
startOffset < firstNode.getTextContentSize() &&
|
||||
firstNode.canHaveFormat()
|
||||
) {
|
||||
if (startOffset !== 0 && !$isTokenOrSegmented(firstNode)) {
|
||||
// the entire first node isn't selected and it isn't a token or segmented, so split it
|
||||
firstNode = firstNode.splitText(startOffset)[1]
|
||||
startOffset = 0
|
||||
if (isBefore) {
|
||||
anchor.set(firstNode.getKey(), startOffset, 'text')
|
||||
} else {
|
||||
focus.set(firstNode.getKey(), startOffset, 'text')
|
||||
}
|
||||
}
|
||||
|
||||
fn(firstNode as TextNode)
|
||||
}
|
||||
|
||||
if ($isTextNode(lastNode) && lastNode.canHaveFormat()) {
|
||||
const lastNodeText = lastNode.getTextContent()
|
||||
const lastNodeTextLength = lastNodeText.length
|
||||
|
||||
// The last node might not actually be the end node
|
||||
//
|
||||
// If not, assume the last node is fully-selected unless the end offset is
|
||||
// zero.
|
||||
if (lastNode.__key !== endKey && endOffset !== 0) {
|
||||
endOffset = lastNodeTextLength
|
||||
}
|
||||
|
||||
// if the entire last node isn't selected and it isn't a token or segmented, split it
|
||||
if (endOffset !== lastNodeTextLength && !$isTokenOrSegmented(lastNode)) {
|
||||
;[lastNode] = lastNode.splitText(endOffset)
|
||||
}
|
||||
|
||||
if (endOffset !== 0 || endType === 'element') {
|
||||
fn(lastNode as TextNode)
|
||||
}
|
||||
}
|
||||
|
||||
// style all the text nodes in between
|
||||
for (let i = 1; i < lastIndex; i++) {
|
||||
const selectedNode = selectedNodes[i]
|
||||
const selectedNodeKey = selectedNode.getKey()
|
||||
|
||||
if (
|
||||
$isTextNode(selectedNode) &&
|
||||
selectedNode.canHaveFormat() &&
|
||||
selectedNodeKey !== firstNode.getKey() &&
|
||||
selectedNodeKey !== lastNode.getKey() &&
|
||||
!selectedNode.isToken()
|
||||
) {
|
||||
fn(selectedNode as TextNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const BgColorIcon: React.FC = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m19 11-8-8-8.6 8.6a2 2 0 0 0 0 2.8l5.2 5.2c.8.8 2 .8 2.8 0L19 11Z" />
|
||||
<path d="m5 2 5 5" />
|
||||
<path d="M2 13h15" />
|
||||
<path d="M22 20a2 2 0 1 1-4 0c0-1.6 1.7-2.4 2-4 .3 1.6 2 2.4 2 4Z" />
|
||||
</svg>
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
export const TextColorIcon: React.FC = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4 20h16" />
|
||||
<path d="m6 16 6-12 6 12" />
|
||||
<path d="M8 12h8" />
|
||||
</svg>
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType, tabHasName } fro
|
||||
|
||||
import type { LexicalRichTextAdapter } from '../../types.js'
|
||||
|
||||
import { getEnabledNodes } from '../../lexical/nodes/index.js'
|
||||
import { getEnabledNodes } from '../../lexical/nodes/utils.js'
|
||||
|
||||
type NestedRichTextFieldsArgs = {
|
||||
data: Record<string, unknown>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
TextClassesFeature,
|
||||
TextColorFeature,
|
||||
TreeViewFeature,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
@@ -12,6 +21,42 @@ export const PostsCollection: CollectionConfig = {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures.filter((feature) => feature === InlineToolbarFeature()),
|
||||
FixedToolbarFeature(),
|
||||
TextColorFeature({
|
||||
colors: [
|
||||
{ label: 'Red', classSuffix: 'red' },
|
||||
{ label: 'Green', classSuffix: 'green' },
|
||||
{ label: 'Blue', classSuffix: 'blue' },
|
||||
],
|
||||
bgColors: [
|
||||
{ label: 'Red', classSuffix: 'red' },
|
||||
{ label: 'Green', classSuffix: 'green' },
|
||||
{ label: 'Blue', classSuffix: 'blue' },
|
||||
],
|
||||
}),
|
||||
// TextColorFeature({
|
||||
// colors: [
|
||||
// { label: 'Red', value: '#ff0000' },
|
||||
// { label: 'Green', value: 'green' },
|
||||
// { label: 'Blue', value: 'blue' },
|
||||
// ],
|
||||
// // normalizeColor: (color) => {
|
||||
// // if (color !== '#ff0000' && color !== 'green' && color !== 'blue') {
|
||||
// // return null
|
||||
// // }
|
||||
// // return color
|
||||
// // },
|
||||
// }),
|
||||
TreeViewFeature(),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
|
||||
@@ -70,6 +70,21 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
@@ -202,6 +217,7 @@ export interface PayloadMigration {
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
|
||||
@@ -125,9 +125,9 @@ export interface Config {
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs?: {
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows?: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
|
||||
Reference in New Issue
Block a user