@byline/richtext-lexical 6.4.0 → 6.6.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/dist/field/extensions/code-highlight/code-highlight-extension.js +2 -1
- package/dist/field/extensions/inline-image/inline-image-extension.js +4 -2
- package/dist/field/extensions/inline-image/inline-image-modal.css +67 -0
- package/dist/field/extensions/inline-image/inline-image-modal.js +173 -98
- package/dist/field/extensions/inline-image/inline-image-node-component.css +37 -0
- package/dist/field/extensions/inline-image/inline-image-node-component.d.ts +3 -1
- package/dist/field/extensions/inline-image/inline-image-node-component.js +39 -1
- package/dist/field/extensions/inline-image/inline-image-node.d.ts +13 -2
- package/dist/field/extensions/inline-image/inline-image-node.js +63 -13
- package/dist/field/extensions/inline-image/node-types.d.ts +19 -0
- package/dist/field/extensions/inline-image/populate.d.ts +22 -0
- package/dist/field/extensions/inline-image/populate.js +19 -1
- package/dist/field/extensions/inline-image/types.d.ts +7 -0
- package/dist/field/extensions/link/link-href.d.ts +30 -0
- package/dist/field/extensions/link/link-href.js +14 -0
- package/dist/field/extensions/link/link-modal.js +96 -288
- package/dist/field/extensions/link/link-target-fields.css +59 -0
- package/dist/field/extensions/link/link-target-fields.d.ts +38 -0
- package/dist/field/extensions/link/link-target-fields.js +227 -0
- package/dist/field/extensions/link/link-target-state.d.ts +54 -0
- package/dist/field/extensions/link/link-target-state.js +70 -0
- package/dist/field/extensions/link/populate.d.ts +15 -1
- package/dist/field/extensions/link/populate.js +55 -50
- package/dist/field/markdown/lexical-to-markdown.js +6 -1
- package/dist/field/plugins/toolbar-plugin/index.js +2 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -2
- package/dist/validate/createEmptyEditorState.d.ts +2 -2
- package/dist/validate/hasText.d.ts +2 -2
- package/package.json +25 -24
- package/src/field/extensions/code-highlight/code-highlight-extension.ts +8 -1
- package/src/field/extensions/inline-image/inline-image-extension.tsx +5 -0
- package/src/field/extensions/inline-image/inline-image-modal.css +100 -0
- package/src/field/extensions/inline-image/inline-image-modal.tsx +178 -85
- package/src/field/extensions/inline-image/inline-image-node-component.css +44 -0
- package/src/field/extensions/inline-image/inline-image-node-component.tsx +52 -0
- package/src/field/extensions/inline-image/inline-image-node.test.tsx +331 -0
- package/src/field/extensions/inline-image/inline-image-node.tsx +108 -11
- package/src/field/extensions/inline-image/node-types.ts +19 -0
- package/src/field/extensions/inline-image/populate-link.test.node.ts +320 -0
- package/src/field/extensions/inline-image/populate.ts +44 -0
- package/src/field/extensions/inline-image/types.ts +7 -0
- package/src/field/extensions/link/link-href.test.node.ts +78 -0
- package/src/field/extensions/link/link-href.ts +52 -0
- package/src/field/extensions/link/link-modal.tsx +86 -325
- package/src/field/extensions/link/link-target-fields.css +81 -0
- package/src/field/extensions/link/link-target-fields.tsx +275 -0
- package/src/field/extensions/link/link-target-state.test.node.ts +197 -0
- package/src/field/extensions/link/link-target-state.ts +158 -0
- package/src/field/extensions/link/populate.ts +94 -67
- package/src/field/markdown/lexical-to-markdown.test.node.ts +68 -0
- package/src/field/markdown/lexical-to-markdown.ts +10 -1
- package/src/field/plugins/toolbar-plugin/index.tsx +7 -4
- package/src/server.ts +11 -5
- package/src/validate/createEmptyEditorState.ts +2 -7
- package/src/validate/hasText.test.ts +1 -2
- package/src/validate/hasText.ts +2 -9
|
@@ -2,6 +2,40 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { $applyNodeReplacement, DecoratorNode, createEditor } from "lexical";
|
|
3
3
|
import * as __rspack_external_react from "react";
|
|
4
4
|
const InlineImageComponent = /*#__PURE__*/ __rspack_external_react.lazy(async ()=>await import("./inline-image-node-component.js"));
|
|
5
|
+
function writeLinkAttributes(element, link) {
|
|
6
|
+
if (null == link) return;
|
|
7
|
+
element.setAttribute('data-link-type', link.linkType ?? 'custom');
|
|
8
|
+
if (true === link.newTab) element.setAttribute('data-link-new-tab', 'true');
|
|
9
|
+
if ('internal' === link.linkType) {
|
|
10
|
+
element.setAttribute('data-link-target-document-id', link.targetDocumentId);
|
|
11
|
+
element.setAttribute('data-link-target-collection-id', link.targetCollectionId);
|
|
12
|
+
element.setAttribute('data-link-target-collection-path', link.targetCollectionPath);
|
|
13
|
+
} else if (null != link.url) element.setAttribute('data-link-url', link.url);
|
|
14
|
+
}
|
|
15
|
+
function readLinkAttributes(domNode) {
|
|
16
|
+
const linkType = domNode.dataset.linkType;
|
|
17
|
+
if ('custom' !== linkType && 'internal' !== linkType) return;
|
|
18
|
+
const newTab = 'true' === domNode.dataset.linkNewTab;
|
|
19
|
+
if ('internal' === linkType) {
|
|
20
|
+
const targetDocumentId = domNode.dataset.linkTargetDocumentId;
|
|
21
|
+
const targetCollectionPath = domNode.dataset.linkTargetCollectionPath;
|
|
22
|
+
if (!targetDocumentId || !targetCollectionPath) return;
|
|
23
|
+
return {
|
|
24
|
+
linkType: 'internal',
|
|
25
|
+
newTab,
|
|
26
|
+
targetDocumentId,
|
|
27
|
+
targetCollectionId: domNode.dataset.linkTargetCollectionId ?? '',
|
|
28
|
+
targetCollectionPath
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const url = domNode.dataset.linkUrl;
|
|
32
|
+
if (!url) return;
|
|
33
|
+
return {
|
|
34
|
+
linkType: 'custom',
|
|
35
|
+
url,
|
|
36
|
+
newTab
|
|
37
|
+
};
|
|
38
|
+
}
|
|
5
39
|
function convertInlineImageElement(domNode) {
|
|
6
40
|
if (domNode instanceof HTMLImageElement) {
|
|
7
41
|
const { alt: altText, src, width, height } = domNode;
|
|
@@ -12,7 +46,8 @@ function convertInlineImageElement(domNode) {
|
|
|
12
46
|
src,
|
|
13
47
|
altText,
|
|
14
48
|
height,
|
|
15
|
-
width
|
|
49
|
+
width,
|
|
50
|
+
link: readLinkAttributes(domNode)
|
|
16
51
|
});
|
|
17
52
|
return {
|
|
18
53
|
node
|
|
@@ -25,10 +60,10 @@ class InlineImageNode extends DecoratorNode {
|
|
|
25
60
|
return 'inline-image';
|
|
26
61
|
}
|
|
27
62
|
static clone(node) {
|
|
28
|
-
return new InlineImageNode(node.__relation, node.__src, node.__position, node.__altText, node.__width, node.__height, node.__showCaption, node.__caption, node.__key);
|
|
63
|
+
return new InlineImageNode(node.__relation, node.__src, node.__position, node.__altText, node.__width, node.__height, node.__showCaption, node.__caption, node.__link, node.__key);
|
|
29
64
|
}
|
|
30
65
|
static importJSON(serializedNode) {
|
|
31
|
-
const { src, position, altText, height, width, showCaption, caption, targetDocumentId, targetCollectionId, targetCollectionPath, document: document1 } = serializedNode;
|
|
66
|
+
const { src, position, altText, height, width, showCaption, caption, link, targetDocumentId, targetCollectionId, targetCollectionPath, document: document1 } = serializedNode;
|
|
32
67
|
const node = $createInlineImageNode({
|
|
33
68
|
targetDocumentId,
|
|
34
69
|
targetCollectionId,
|
|
@@ -39,7 +74,8 @@ class InlineImageNode extends DecoratorNode {
|
|
|
39
74
|
altText,
|
|
40
75
|
width,
|
|
41
76
|
height,
|
|
42
|
-
showCaption
|
|
77
|
+
showCaption,
|
|
78
|
+
link
|
|
43
79
|
});
|
|
44
80
|
const nestedEditor = node.__caption;
|
|
45
81
|
const editorState = nestedEditor.parseEditorState(caption.editorState);
|
|
@@ -62,6 +98,7 @@ class InlineImageNode extends DecoratorNode {
|
|
|
62
98
|
element.setAttribute('alt', this.__altText);
|
|
63
99
|
if (null != this.__width) element.setAttribute('width', this.__width.toString());
|
|
64
100
|
if (null != this.__height) element.setAttribute('height', this.__height.toString());
|
|
101
|
+
writeLinkAttributes(element, this.__link);
|
|
65
102
|
return {
|
|
66
103
|
element
|
|
67
104
|
};
|
|
@@ -76,6 +113,7 @@ class InlineImageNode extends DecoratorNode {
|
|
|
76
113
|
width: this.__width,
|
|
77
114
|
showCaption: this.__showCaption,
|
|
78
115
|
caption: this.__caption.toJSON(),
|
|
116
|
+
link: this.__link,
|
|
79
117
|
type: 'inline-image',
|
|
80
118
|
version: 1
|
|
81
119
|
};
|
|
@@ -105,6 +143,13 @@ class InlineImageNode extends DecoratorNode {
|
|
|
105
143
|
const writable = this.getWritable();
|
|
106
144
|
writable.__showCaption = showCaption;
|
|
107
145
|
}
|
|
146
|
+
getLink() {
|
|
147
|
+
return this.__link;
|
|
148
|
+
}
|
|
149
|
+
setLink(link) {
|
|
150
|
+
const writable = this.getWritable();
|
|
151
|
+
writable.__link = link;
|
|
152
|
+
}
|
|
108
153
|
getPosition() {
|
|
109
154
|
return this.__position;
|
|
110
155
|
}
|
|
@@ -127,21 +172,24 @@ class InlineImageNode extends DecoratorNode {
|
|
|
127
172
|
if (null != width) writable.__width = width;
|
|
128
173
|
if (null != height) writable.__height = height;
|
|
129
174
|
if (null != showCaption) writable.__showCaption = showCaption;
|
|
175
|
+
if ('link' in payload) writable.__link = payload.link;
|
|
176
|
+
}
|
|
177
|
+
buildClassName(config) {
|
|
178
|
+
const linked = null != this.__link ? ' has-link' : '';
|
|
179
|
+
return `${config.theme.inlineImage} position-${this.__position}${linked}`;
|
|
130
180
|
}
|
|
131
181
|
createDOM(config) {
|
|
132
182
|
const span = document.createElement('span');
|
|
133
|
-
|
|
134
|
-
if (void 0 !== className) span.className = className;
|
|
183
|
+
span.className = this.buildClassName(config);
|
|
135
184
|
return span;
|
|
136
185
|
}
|
|
137
186
|
updateDOM(prevNode, dom, config) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const className = `${config.theme.inlineImage} position-${position}`;
|
|
141
|
-
if (void 0 !== className) dom.className = className;
|
|
187
|
+
if (this.__position !== prevNode.__position || null != this.__link !== (null != prevNode.__link)) {
|
|
188
|
+
dom.className = this.buildClassName(config);
|
|
142
189
|
return true;
|
|
143
190
|
}
|
|
144
191
|
if (this.__showCaption !== prevNode.__showCaption) return true;
|
|
192
|
+
if (this.__link !== prevNode.__link) return true;
|
|
145
193
|
return false;
|
|
146
194
|
}
|
|
147
195
|
decorate() {
|
|
@@ -156,11 +204,12 @@ class InlineImageNode extends DecoratorNode {
|
|
|
156
204
|
height: this.__height,
|
|
157
205
|
showCaption: this.__showCaption,
|
|
158
206
|
caption: this.__caption,
|
|
207
|
+
link: this.__link,
|
|
159
208
|
nodeKey: this.getKey()
|
|
160
209
|
})
|
|
161
210
|
});
|
|
162
211
|
}
|
|
163
|
-
constructor(relation, src, position, altText, width, height, showCaption, caption, key){
|
|
212
|
+
constructor(relation, src, position, altText, width, height, showCaption, caption, link, key){
|
|
164
213
|
super(key);
|
|
165
214
|
this.__relation = relation;
|
|
166
215
|
this.__src = src;
|
|
@@ -170,16 +219,17 @@ class InlineImageNode extends DecoratorNode {
|
|
|
170
219
|
this.__height = height;
|
|
171
220
|
this.__showCaption = showCaption ?? false;
|
|
172
221
|
this.__caption = caption ?? createEditor();
|
|
222
|
+
this.__link = link;
|
|
173
223
|
}
|
|
174
224
|
}
|
|
175
|
-
function $createInlineImageNode({ targetDocumentId, targetCollectionId, targetCollectionPath, document: document1, src, position, altText, height, width, showCaption, caption, key }) {
|
|
225
|
+
function $createInlineImageNode({ targetDocumentId, targetCollectionId, targetCollectionPath, document: document1, src, position, altText, height, width, showCaption, caption, link, key }) {
|
|
176
226
|
const relation = {
|
|
177
227
|
targetDocumentId,
|
|
178
228
|
targetCollectionId,
|
|
179
229
|
targetCollectionPath,
|
|
180
230
|
document: document1
|
|
181
231
|
};
|
|
182
|
-
return $applyNodeReplacement(new InlineImageNode(relation, src, position, altText, width, height, showCaption, caption, key));
|
|
232
|
+
return $applyNodeReplacement(new InlineImageNode(relation, src, position, altText, width, height, showCaption, caption, link, key));
|
|
183
233
|
}
|
|
184
234
|
function $isInlineImageNode(node) {
|
|
185
235
|
return node instanceof InlineImageNode;
|
|
@@ -7,7 +7,23 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { LexicalEditor, NodeKey, SerializedEditor, SerializedLexicalNode, Spread } from 'lexical';
|
|
9
9
|
import type { DocumentRelation } from '../../nodes/document-relation.js';
|
|
10
|
+
import type { LinkAttributes } from '../link/index.js';
|
|
10
11
|
export type Position = 'left' | 'right' | 'full' | 'wide' | 'default' | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* An inline image carries **two** independent document references, and the
|
|
14
|
+
* distinction matters at every layer:
|
|
15
|
+
*
|
|
16
|
+
* - the flat `DocumentRelation` this interface extends — the *media*
|
|
17
|
+
* document the image is sourced from. Always present.
|
|
18
|
+
* - `link` — an optional *click-through target*. Nested rather than
|
|
19
|
+
* flattened precisely because the flat envelope is already spent on
|
|
20
|
+
* the media pick, and the two resolve against different collections.
|
|
21
|
+
*
|
|
22
|
+
* `link` reuses the link extension's discriminated union, so an image can
|
|
23
|
+
* point at a Byline document (`linkType: 'internal'`, relation refreshed
|
|
24
|
+
* by `inlineImageLinkVisitor`) or at an arbitrary URL
|
|
25
|
+
* (`linkType: 'custom'`). Absent means the image is not clickable.
|
|
26
|
+
*/
|
|
11
27
|
export interface InlineImageAttributes extends DocumentRelation {
|
|
12
28
|
src: string;
|
|
13
29
|
altText?: string;
|
|
@@ -17,6 +33,7 @@ export interface InlineImageAttributes extends DocumentRelation {
|
|
|
17
33
|
key?: NodeKey;
|
|
18
34
|
showCaption?: boolean;
|
|
19
35
|
caption?: LexicalEditor;
|
|
36
|
+
link?: LinkAttributes;
|
|
20
37
|
}
|
|
21
38
|
export type SerializedInlineImageNode = Spread<DocumentRelation & {
|
|
22
39
|
src: string;
|
|
@@ -26,4 +43,6 @@ export type SerializedInlineImageNode = Spread<DocumentRelation & {
|
|
|
26
43
|
width?: number | string;
|
|
27
44
|
showCaption: boolean;
|
|
28
45
|
caption: SerializedEditor;
|
|
46
|
+
/** Optional click-through target; absent on unlinked images. */
|
|
47
|
+
link?: LinkAttributes;
|
|
29
48
|
}, SerializedLexicalNode>;
|
|
@@ -7,3 +7,25 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { LexicalNodeVisitor } from '../../lexical-populate-shared.js';
|
|
9
9
|
export declare const inlineImageVisitor: LexicalNodeVisitor;
|
|
10
|
+
/**
|
|
11
|
+
* Second visitor for the same node type. An inline image carries **two**
|
|
12
|
+
* independent relations:
|
|
13
|
+
*
|
|
14
|
+
* - the flat `DocumentRelation` envelope on the node — the *media*
|
|
15
|
+
* document the image comes from, refreshed by `inlineImageVisitor`
|
|
16
|
+
* above;
|
|
17
|
+
* - `node.link` — an optional *click-through target*, which is an
|
|
18
|
+
* unrelated document in an unrelated collection.
|
|
19
|
+
*
|
|
20
|
+
* They cannot share one visitor: `PendingHydration` addresses exactly one
|
|
21
|
+
* `{ collectionPath, documentId }` pair. `runLexicalPopulate` runs every
|
|
22
|
+
* visitor against every node and enqueues each match separately, so
|
|
23
|
+
* registering this alongside `inlineImageVisitor` hydrates both relations
|
|
24
|
+
* in the same batched pass with no change to the driver contract.
|
|
25
|
+
*
|
|
26
|
+
* Only `linkType: 'internal'` links carry a relation; custom-URL links are
|
|
27
|
+
* a literal href and are skipped. Resolution branches — found, hook threw,
|
|
28
|
+
* target missing — are the internal-link rules shared with `linkVisitor`
|
|
29
|
+
* (see `../link/populate.ts`).
|
|
30
|
+
*/
|
|
31
|
+
export declare const inlineImageLinkVisitor: LexicalNodeVisitor;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createInternalLinkHydration } from "../link/populate.js";
|
|
1
2
|
import { deriveImageSizes, getPreferredSize } from "./utils.js";
|
|
2
3
|
const inlineImageVisitor = {
|
|
3
4
|
match (node) {
|
|
@@ -33,4 +34,21 @@ const inlineImageVisitor = {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
|
-
|
|
37
|
+
const inlineImageLinkVisitor = {
|
|
38
|
+
match (node) {
|
|
39
|
+
if ('inline-image' !== node.type) return null;
|
|
40
|
+
const link = node.link;
|
|
41
|
+
if (null == link) return null;
|
|
42
|
+
if ('internal' !== link.linkType) return null;
|
|
43
|
+
const collectionPath = link.targetCollectionPath;
|
|
44
|
+
const documentId = link.targetDocumentId;
|
|
45
|
+
if (!collectionPath || !documentId) return null;
|
|
46
|
+
return {
|
|
47
|
+
node,
|
|
48
|
+
collectionPath,
|
|
49
|
+
documentId,
|
|
50
|
+
...createInternalLinkHydration(link, collectionPath, documentId)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
export { inlineImageLinkVisitor, inlineImageVisitor };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { DocumentRelation } from '../../nodes/document-relation.js';
|
|
9
|
+
import type { LinkAttributes } from '../link/index.js';
|
|
9
10
|
import type { Position } from './node-types.js';
|
|
10
11
|
/**
|
|
11
12
|
* The form-shape carried into / out of the inline image modal. Mirrors
|
|
@@ -23,6 +24,12 @@ export interface InlineImageData {
|
|
|
23
24
|
width?: number | string;
|
|
24
25
|
height?: number | string;
|
|
25
26
|
showCaption?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Optional click-through target. `undefined` means "no link" — and the
|
|
29
|
+
* modal always emits the key, so the node's `update()` can tell
|
|
30
|
+
* "unchanged" from "cleared".
|
|
31
|
+
*/
|
|
32
|
+
link?: LinkAttributes;
|
|
26
33
|
}
|
|
27
34
|
export interface InlineImageModalProps {
|
|
28
35
|
/** Modal visibility — driven by the plugin's open/close commands. */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a `LinkAttributes` to a renderable href, or `null` when no
|
|
10
|
+
* usable target exists.
|
|
11
|
+
*
|
|
12
|
+
* React-free and DOM-free, so the editor, the markdown serializer and any
|
|
13
|
+
* host renderer can share one definition of the internal-link fallback
|
|
14
|
+
* chain (documented in `docs/04-collections/07-rich-text.md`):
|
|
15
|
+
*
|
|
16
|
+
* 1. `document._resolved === false` — the server-side walker could not
|
|
17
|
+
* find the target on its last pass. Return null; callers degrade.
|
|
18
|
+
* 2. `document.path` starts with `/` — canonical, written by the walker
|
|
19
|
+
* via `buildDocumentPath`. Use as-is.
|
|
20
|
+
* 3. `document.path` is a bare slug — compose `/${collectionPath}/${path}`.
|
|
21
|
+
* Covers legacy nodes and picker-time writes not yet walked.
|
|
22
|
+
* 4. Anything else — null.
|
|
23
|
+
*
|
|
24
|
+
* `null` is never "render an empty href". It means the link is not
|
|
25
|
+
* renderable, and each caller has its own way of degrading: the editor
|
|
26
|
+
* withholds its cmd-click affordance, the markdown serializer emits the
|
|
27
|
+
* image without a wrapper, the public renderer drops the anchor.
|
|
28
|
+
*/
|
|
29
|
+
import type { LinkAttributes } from './index.js';
|
|
30
|
+
export declare function resolveLinkHref(link: LinkAttributes | undefined | null): string | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function resolveLinkHref(link) {
|
|
2
|
+
if (null == link) return null;
|
|
3
|
+
if ('internal' === link.linkType) {
|
|
4
|
+
if (link.document?._resolved === false) return null;
|
|
5
|
+
const path = link.document?.path;
|
|
6
|
+
if ('string' != typeof path || 0 === path.length) return null;
|
|
7
|
+
if (path.startsWith('/')) return path;
|
|
8
|
+
if (link.targetCollectionPath) return `/${link.targetCollectionPath}/${path}`;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const url = link.url;
|
|
12
|
+
return 'string' == typeof url && url.length > 0 ? url : null;
|
|
13
|
+
}
|
|
14
|
+
export { resolveLinkHref };
|