@byline/richtext-lexical 6.3.0 → 6.5.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.
Files changed (62) hide show
  1. package/dist/field/context/shared-on-change-context.js +1 -1
  2. package/dist/field/extensions/code-highlight/code-highlight-extension.js +2 -1
  3. package/dist/field/extensions/inline-image/inline-image-extension.js +4 -2
  4. package/dist/field/extensions/inline-image/inline-image-modal.css +67 -0
  5. package/dist/field/extensions/inline-image/inline-image-modal.js +173 -98
  6. package/dist/field/extensions/inline-image/inline-image-node-component.css +37 -0
  7. package/dist/field/extensions/inline-image/inline-image-node-component.d.ts +3 -1
  8. package/dist/field/extensions/inline-image/inline-image-node-component.js +39 -1
  9. package/dist/field/extensions/inline-image/inline-image-node.d.ts +13 -2
  10. package/dist/field/extensions/inline-image/inline-image-node.js +63 -13
  11. package/dist/field/extensions/inline-image/node-types.d.ts +19 -0
  12. package/dist/field/extensions/inline-image/populate.d.ts +22 -0
  13. package/dist/field/extensions/inline-image/populate.js +19 -1
  14. package/dist/field/extensions/inline-image/types.d.ts +7 -0
  15. package/dist/field/extensions/link/link-href.d.ts +30 -0
  16. package/dist/field/extensions/link/link-href.js +14 -0
  17. package/dist/field/extensions/link/link-modal.js +96 -288
  18. package/dist/field/extensions/link/link-target-fields.css +59 -0
  19. package/dist/field/extensions/link/link-target-fields.d.ts +38 -0
  20. package/dist/field/extensions/link/link-target-fields.js +227 -0
  21. package/dist/field/extensions/link/link-target-state.d.ts +54 -0
  22. package/dist/field/extensions/link/link-target-state.js +70 -0
  23. package/dist/field/extensions/link/populate.d.ts +15 -1
  24. package/dist/field/extensions/link/populate.js +55 -50
  25. package/dist/field/markdown/lexical-to-markdown.js +6 -1
  26. package/dist/field/plugins/toolbar-plugin/index.js +2 -1
  27. package/dist/field/utils/getSelectedNode.js +1 -1
  28. package/dist/field/utils/rect.js +1 -2
  29. package/dist/richtext-field.js +1 -1
  30. package/dist/server.d.ts +2 -2
  31. package/dist/server.js +4 -2
  32. package/dist/validate/createEmptyEditorState.d.ts +2 -2
  33. package/dist/validate/hasText.d.ts +2 -2
  34. package/package.json +27 -26
  35. package/src/field/extensions/code-highlight/code-highlight-extension.ts +8 -1
  36. package/src/field/extensions/inline-image/inline-image-extension.tsx +5 -0
  37. package/src/field/extensions/inline-image/inline-image-modal.css +100 -0
  38. package/src/field/extensions/inline-image/inline-image-modal.tsx +178 -85
  39. package/src/field/extensions/inline-image/inline-image-node-component.css +44 -0
  40. package/src/field/extensions/inline-image/inline-image-node-component.tsx +52 -0
  41. package/src/field/extensions/inline-image/inline-image-node.test.tsx +331 -0
  42. package/src/field/extensions/inline-image/inline-image-node.tsx +108 -11
  43. package/src/field/extensions/inline-image/node-types.ts +19 -0
  44. package/src/field/extensions/inline-image/populate-link.test.node.ts +320 -0
  45. package/src/field/extensions/inline-image/populate.ts +44 -0
  46. package/src/field/extensions/inline-image/types.ts +7 -0
  47. package/src/field/extensions/link/link-href.test.node.ts +78 -0
  48. package/src/field/extensions/link/link-href.ts +52 -0
  49. package/src/field/extensions/link/link-modal.tsx +86 -325
  50. package/src/field/extensions/link/link-target-fields.css +81 -0
  51. package/src/field/extensions/link/link-target-fields.tsx +275 -0
  52. package/src/field/extensions/link/link-target-state.test.node.ts +197 -0
  53. package/src/field/extensions/link/link-target-state.ts +158 -0
  54. package/src/field/extensions/link/populate.ts +94 -67
  55. package/src/field/markdown/lexical-to-markdown.test.node.ts +68 -0
  56. package/src/field/markdown/lexical-to-markdown.ts +10 -1
  57. package/src/field/plugins/toolbar-plugin/index.tsx +7 -4
  58. package/src/richtext-field.tsx +1 -1
  59. package/src/server.ts +11 -5
  60. package/src/validate/createEmptyEditorState.ts +2 -7
  61. package/src/validate/hasText.test.ts +1 -2
  62. 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
- const className = `${config.theme.inlineImage} position-${this.__position}`;
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
- const position = this.__position;
139
- if (position !== prevNode.__position) {
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
- export { inlineImageVisitor };
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 };