@payload-onpage/lexical 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/InlineRichText.d.ts +13 -0
- package/dist/InlineRichText.d.ts.map +1 -0
- package/dist/InlineRichText.js +311 -0
- package/dist/InlineRichText.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -0
- package/dist/convert.d.ts +15 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +65 -0
- package/dist/convert.js.map +1 -0
- package/dist/editor.d.ts +4 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +4 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/nodeList.d.ts +6 -0
- package/dist/nodeList.d.ts.map +1 -0
- package/dist/nodeList.js +13 -0
- package/dist/nodeList.js.map +1 -0
- package/dist/nodes.d.ts +76 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +136 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +69 -4
- package/src/InlineRichText.tsx +542 -0
- package/src/constants.ts +2 -0
- package/src/convert.ts +73 -0
- package/src/editor.ts +10 -0
- package/src/index.ts +33 -0
- package/src/nodeList.ts +13 -0
- package/src/nodes.tsx +213 -0
package/src/nodes.tsx
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lexical nodes for editing Payload rich text in place.
|
|
5
|
+
*
|
|
6
|
+
* - PayloadLinkNode — Payload stores links as
|
|
7
|
+
* { type: "link", id, fields: { url, newTab, linkType, doc? }, children, … }
|
|
8
|
+
* (not @lexical/link's { url, rel, target }). This node reads and writes that
|
|
9
|
+
* shape, keeping `id` and every key in `fields`.
|
|
10
|
+
* - OpaqueNode — any node type we don't edit (upload, block, relationship,
|
|
11
|
+
* horizontalrule…) is carried through untouched and shown as a locked chip,
|
|
12
|
+
* so editing a paragraph never loses an image or embed.
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react'
|
|
15
|
+
import {
|
|
16
|
+
DecoratorNode,
|
|
17
|
+
ElementNode,
|
|
18
|
+
type LexicalNode,
|
|
19
|
+
type NodeKey,
|
|
20
|
+
type SerializedElementNode,
|
|
21
|
+
type SerializedLexicalNode,
|
|
22
|
+
type Spread,
|
|
23
|
+
} from 'lexical'
|
|
24
|
+
|
|
25
|
+
export interface PayloadLinkFields {
|
|
26
|
+
url?: string
|
|
27
|
+
newTab?: boolean
|
|
28
|
+
linkType?: string
|
|
29
|
+
[key: string]: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SerializedPayloadLinkNode = Spread<
|
|
33
|
+
{ type: 'link'; version: number; fields: PayloadLinkFields; id?: string },
|
|
34
|
+
SerializedElementNode
|
|
35
|
+
>
|
|
36
|
+
|
|
37
|
+
export class PayloadLinkNode extends ElementNode {
|
|
38
|
+
__fields: PayloadLinkFields
|
|
39
|
+
__linkId?: string
|
|
40
|
+
|
|
41
|
+
static override getType(): string {
|
|
42
|
+
return 'link'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static override clone(node: PayloadLinkNode): PayloadLinkNode {
|
|
46
|
+
return new PayloadLinkNode({ ...node.__fields }, node.__linkId, node.__key)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor(fields: PayloadLinkFields = {}, id?: string, key?: NodeKey) {
|
|
50
|
+
super(key)
|
|
51
|
+
this.__fields = fields
|
|
52
|
+
this.__linkId = id
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override createDOM(): HTMLElement {
|
|
56
|
+
const a = document.createElement('a')
|
|
57
|
+
a.setAttribute('href', this.__fields.url || '#')
|
|
58
|
+
if (this.__fields.newTab) {
|
|
59
|
+
a.setAttribute('target', '_blank')
|
|
60
|
+
a.setAttribute('rel', 'noopener noreferrer')
|
|
61
|
+
}
|
|
62
|
+
a.addEventListener('click', (e) => e.preventDefault())
|
|
63
|
+
return a
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override updateDOM(prev: PayloadLinkNode, dom: HTMLElement): boolean {
|
|
67
|
+
if (prev.__fields.url !== this.__fields.url) dom.setAttribute('href', this.__fields.url || '#')
|
|
68
|
+
if (prev.__fields.newTab !== this.__fields.newTab) {
|
|
69
|
+
if (this.__fields.newTab) {
|
|
70
|
+
dom.setAttribute('target', '_blank')
|
|
71
|
+
dom.setAttribute('rel', 'noopener noreferrer')
|
|
72
|
+
} else {
|
|
73
|
+
dom.removeAttribute('target')
|
|
74
|
+
dom.removeAttribute('rel')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static override importJSON(json: SerializedPayloadLinkNode): PayloadLinkNode {
|
|
81
|
+
return $createPayloadLinkNode(json.fields ?? {}, json.id).updateFromJSON(json)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override exportJSON(): SerializedPayloadLinkNode {
|
|
85
|
+
return {
|
|
86
|
+
...super.exportJSON(),
|
|
87
|
+
type: 'link',
|
|
88
|
+
version: 3,
|
|
89
|
+
fields: { ...this.getLatest().__fields },
|
|
90
|
+
...(this.getLatest().__linkId ? { id: this.getLatest().__linkId } : {}),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getFields(): PayloadLinkFields {
|
|
95
|
+
return this.getLatest().__fields
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setFields(fields: PayloadLinkFields): this {
|
|
99
|
+
const writable = this.getWritable()
|
|
100
|
+
writable.__fields = fields
|
|
101
|
+
return writable
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override isInline(): boolean {
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override canBeEmpty(): boolean {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override canInsertTextBefore(): boolean {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override canInsertTextAfter(): boolean {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function $createPayloadLinkNode(fields: PayloadLinkFields, id?: string): PayloadLinkNode {
|
|
122
|
+
return new PayloadLinkNode(fields, id)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function $isPayloadLinkNode(node: LexicalNode | null | undefined): node is PayloadLinkNode {
|
|
126
|
+
return node instanceof PayloadLinkNode
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Opaque ───────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
import { OPAQUE_TYPE } from './constants.ts'
|
|
132
|
+
|
|
133
|
+
export { OPAQUE_TYPE }
|
|
134
|
+
const INLINE_TYPES = new Set(['inlineBlock', 'autolink'])
|
|
135
|
+
|
|
136
|
+
export type SerializedOpaqueNode = Spread<
|
|
137
|
+
{ type: typeof OPAQUE_TYPE; version: 1; data: OpaqueData },
|
|
138
|
+
SerializedLexicalNode
|
|
139
|
+
>
|
|
140
|
+
|
|
141
|
+
/** The Payload node an OpaqueNode carries, untouched. */
|
|
142
|
+
export interface OpaqueData {
|
|
143
|
+
type?: string
|
|
144
|
+
value?: { filename?: unknown } | string | number
|
|
145
|
+
fields?: { blockType?: unknown }
|
|
146
|
+
[key: string]: unknown
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function describeNode(data: OpaqueData): string {
|
|
150
|
+
const filename =
|
|
151
|
+
typeof data.value === 'object' && typeof data.value?.filename === 'string'
|
|
152
|
+
? data.value.filename
|
|
153
|
+
: ''
|
|
154
|
+
const blockType = typeof data.fields?.blockType === 'string' ? data.fields.blockType : 'Block'
|
|
155
|
+
switch (data.type) {
|
|
156
|
+
case 'upload':
|
|
157
|
+
return `Image${filename ? ` · ${filename}` : ''} — edit in admin`
|
|
158
|
+
case 'horizontalrule':
|
|
159
|
+
return 'Divider'
|
|
160
|
+
case 'block':
|
|
161
|
+
case 'inlineBlock':
|
|
162
|
+
return `${blockType} — edit in admin`
|
|
163
|
+
case 'relationship':
|
|
164
|
+
return 'Linked document — edit in admin'
|
|
165
|
+
default:
|
|
166
|
+
return `${data.type ?? 'Content'} — edit in admin`
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class OpaqueNode extends DecoratorNode<React.ReactNode> {
|
|
171
|
+
__data: OpaqueData
|
|
172
|
+
|
|
173
|
+
static override getType(): string {
|
|
174
|
+
return OPAQUE_TYPE
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static override clone(node: OpaqueNode): OpaqueNode {
|
|
178
|
+
return new OpaqueNode(node.__data, node.__key)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
constructor(data: OpaqueData, key?: NodeKey) {
|
|
182
|
+
super(key)
|
|
183
|
+
this.__data = data
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override createDOM(): HTMLElement {
|
|
187
|
+
return document.createElement(this.isInline() ? 'span' : 'div')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override updateDOM(): boolean {
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override isInline(): boolean {
|
|
195
|
+
return INLINE_TYPES.has(this.__data.type ?? '')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static override importJSON(json: SerializedOpaqueNode): OpaqueNode {
|
|
199
|
+
return new OpaqueNode(json.data)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
override exportJSON(): SerializedOpaqueNode {
|
|
203
|
+
return { type: OPAQUE_TYPE, version: 1, data: this.getLatest().__data }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
override decorate(): React.ReactNode {
|
|
207
|
+
return (
|
|
208
|
+
<span className="po-opaque" contentEditable={false} data-testid="onpage-opaque">
|
|
209
|
+
⧉ {describeNode(this.__data)}
|
|
210
|
+
</span>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|