@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
@@ -0,0 +1,227 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { RelationPicker } from "@byline/admin/react";
5
+ import { getCollectionDefinition, isSingleton } from "@byline/core";
6
+ import { Button, Checkbox, CloseIcon, EditIcon, IconButton, Input, Label, RadioGroup, RadioGroupItem, Select } from "@byline/ui/react";
7
+ import "./link-target-fields.css";
8
+ const LinkTargetFields = ({ idPrefix, state, onChange, linkable, allowNone, error, legend = 'Link' })=>{
9
+ const [pickerOpen, setPickerOpen] = useState(false);
10
+ const targetDefinition = state.targetCollection ? getCollectionDefinition(state.targetCollection) : null;
11
+ const targetDef = null == targetDefinition || isSingleton(targetDefinition) ? null : targetDefinition;
12
+ const collectionItems = useMemo(()=>linkable.map((c)=>({
13
+ label: c.labels.singular,
14
+ value: c.path
15
+ })), [
16
+ linkable
17
+ ]);
18
+ const pickedLabel = useMemo(()=>{
19
+ if ('internal' !== state.kind || !state.picked) return null;
20
+ const title = state.picked.document?.title;
21
+ if ('string' == typeof title && title.length > 0) return title;
22
+ const pickedDefinition = getCollectionDefinition(state.picked.targetCollectionPath);
23
+ const pickedDef = null == pickedDefinition || isSingleton(pickedDefinition) ? null : pickedDefinition;
24
+ const short = state.picked.targetDocumentId.slice(0, 8);
25
+ return `${pickedDef?.labels.singular ?? state.picked.targetCollectionPath} · ${short}…`;
26
+ }, [
27
+ state.kind,
28
+ state.picked
29
+ ]);
30
+ const handlePickerSelect = (selection)=>{
31
+ setPickerOpen(false);
32
+ const targetCollection = state.targetCollection;
33
+ const titleField = getCollectionDefinition(targetCollection)?.useAsTitle;
34
+ const title = titleField ? selection.record?.fields?.[titleField] : void 0;
35
+ const path = selection.record?.path;
36
+ const document = {};
37
+ if ('string' == typeof title && title.length > 0) document.title = title;
38
+ if ('string' == typeof path && path.length > 0) document.path = path;
39
+ onChange({
40
+ ...state,
41
+ picked: {
42
+ targetDocumentId: selection.targetDocumentId,
43
+ targetCollectionId: selection.targetCollectionId,
44
+ targetCollectionPath: targetCollection,
45
+ document: Object.keys(document).length > 0 ? document : void 0
46
+ }
47
+ });
48
+ };
49
+ return /*#__PURE__*/ jsxs(Fragment, {
50
+ children: [
51
+ /*#__PURE__*/ jsxs("div", {
52
+ className: "flex flex-col gap-3",
53
+ children: [
54
+ /*#__PURE__*/ jsx(Label, {
55
+ id: `${idPrefix}-kind-label`,
56
+ htmlFor: `${idPrefix}-kind`,
57
+ className: "text-sm font-medium",
58
+ label: legend
59
+ }),
60
+ /*#__PURE__*/ jsxs(RadioGroup, {
61
+ id: `${idPrefix}-kind`,
62
+ name: `${idPrefix}-kind`,
63
+ "aria-labelledby": `${idPrefix}-kind-label`,
64
+ direction: "row",
65
+ value: state.kind,
66
+ onValueChange: (value)=>onChange({
67
+ ...state,
68
+ kind: value
69
+ }),
70
+ children: [
71
+ allowNone && /*#__PURE__*/ jsx(RadioGroupItem, {
72
+ id: `${idPrefix}-kind-none`,
73
+ value: "none",
74
+ label: "No link"
75
+ }),
76
+ linkable.length > 0 && /*#__PURE__*/ jsx(RadioGroupItem, {
77
+ id: `${idPrefix}-kind-internal`,
78
+ value: "internal",
79
+ label: "Document"
80
+ }),
81
+ /*#__PURE__*/ jsx(RadioGroupItem, {
82
+ id: `${idPrefix}-kind-custom`,
83
+ value: "custom",
84
+ label: "Custom URL"
85
+ })
86
+ ]
87
+ }),
88
+ 'custom' === state.kind && /*#__PURE__*/ jsx(Input, {
89
+ id: `${idPrefix}-url`,
90
+ name: "url",
91
+ label: "URL",
92
+ placeholder: "https://example.com or /path",
93
+ value: state.url,
94
+ error: null != error,
95
+ errorText: error ?? void 0,
96
+ onChange: (e)=>onChange({
97
+ ...state,
98
+ url: e.target.value
99
+ })
100
+ }),
101
+ 'internal' === state.kind && /*#__PURE__*/ jsxs("div", {
102
+ className: "flex flex-col gap-3",
103
+ children: [
104
+ linkable.length > 1 && /*#__PURE__*/ jsxs("div", {
105
+ children: [
106
+ /*#__PURE__*/ jsx(Label, {
107
+ id: `${idPrefix}-collection-label`,
108
+ htmlFor: `${idPrefix}-collection`,
109
+ className: "mb-1",
110
+ label: "Target collection"
111
+ }),
112
+ /*#__PURE__*/ jsx(Select, {
113
+ size: "sm",
114
+ items: collectionItems,
115
+ placeholder: "Target collection",
116
+ value: state.targetCollection ?? void 0,
117
+ onValueChange: (value)=>{
118
+ if (null == value) return;
119
+ onChange({
120
+ ...state,
121
+ targetCollection: value
122
+ });
123
+ }
124
+ })
125
+ ]
126
+ }),
127
+ /*#__PURE__*/ jsxs("div", {
128
+ children: [
129
+ /*#__PURE__*/ jsx(Label, {
130
+ id: `${idPrefix}-document-label`,
131
+ htmlFor: `${idPrefix}-document`,
132
+ className: "mb-1",
133
+ label: "Target document"
134
+ }),
135
+ /*#__PURE__*/ jsx("div", {
136
+ className: "byline-link-target-frame",
137
+ children: state.picked ? /*#__PURE__*/ jsxs("div", {
138
+ className: "byline-link-target-tile",
139
+ children: [
140
+ /*#__PURE__*/ jsxs("span", {
141
+ className: "byline-link-target-summary",
142
+ children: [
143
+ /*#__PURE__*/ jsx("span", {
144
+ className: "byline-link-target-kind",
145
+ children: targetDef?.labels.singular ?? state.picked.targetCollectionPath
146
+ }),
147
+ /*#__PURE__*/ jsx("span", {
148
+ className: "byline-link-target-value",
149
+ children: pickedLabel
150
+ })
151
+ ]
152
+ }),
153
+ /*#__PURE__*/ jsxs("span", {
154
+ className: "byline-link-target-actions",
155
+ children: [
156
+ /*#__PURE__*/ jsx(IconButton, {
157
+ id: `${idPrefix}-document`,
158
+ type: "button",
159
+ intent: "noeffect",
160
+ size: "xs",
161
+ "aria-label": `Change ${targetDef?.labels.singular ?? 'document'}`,
162
+ onClick: ()=>setPickerOpen(true),
163
+ disabled: !state.targetCollection,
164
+ children: /*#__PURE__*/ jsx(EditIcon, {
165
+ width: "15px",
166
+ height: "15px"
167
+ })
168
+ }),
169
+ /*#__PURE__*/ jsx(IconButton, {
170
+ type: "button",
171
+ intent: "noeffect",
172
+ size: "xs",
173
+ "aria-label": `Remove ${targetDef?.labels.singular ?? 'document'}`,
174
+ onClick: ()=>onChange({
175
+ ...state,
176
+ picked: null
177
+ }),
178
+ children: /*#__PURE__*/ jsx(CloseIcon, {
179
+ width: "15px",
180
+ height: "15px"
181
+ })
182
+ })
183
+ ]
184
+ })
185
+ ]
186
+ }) : /*#__PURE__*/ jsx(Button, {
187
+ id: `${idPrefix}-document`,
188
+ size: "xs",
189
+ variant: "outlined",
190
+ intent: "noeffect",
191
+ type: "button",
192
+ onClick: ()=>setPickerOpen(true),
193
+ disabled: !state.targetCollection,
194
+ children: `Select ${targetDef?.labels.singular ?? 'document'}`
195
+ })
196
+ })
197
+ ]
198
+ }),
199
+ error && /*#__PURE__*/ jsx("span", {
200
+ className: "text-xs text-red-400",
201
+ children: error
202
+ })
203
+ ]
204
+ }),
205
+ 'none' !== state.kind && /*#__PURE__*/ jsx(Checkbox, {
206
+ id: `${idPrefix}-new-tab`,
207
+ name: "newTab",
208
+ label: "Open in new tab",
209
+ checked: state.newTab,
210
+ onCheckedChange: (checked)=>onChange({
211
+ ...state,
212
+ newTab: true === checked
213
+ })
214
+ })
215
+ ]
216
+ }),
217
+ 'internal' === state.kind && state.targetCollection && /*#__PURE__*/ jsx(RelationPicker, {
218
+ targetCollectionPath: state.targetCollection,
219
+ targetDefinition: targetDef,
220
+ isOpen: pickerOpen,
221
+ onSelect: handlePickerSelect,
222
+ onDismiss: ()=>setPickerOpen(false)
223
+ })
224
+ ]
225
+ });
226
+ };
227
+ export { LinkTargetFields };
@@ -0,0 +1,54 @@
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
+ * The state machine behind "where does this link point?" — shared by the
10
+ * link modal and the inline-image modal so both offer the same choice of
11
+ * a Byline document or an arbitrary URL, with the same validation.
12
+ *
13
+ * React-free on purpose. The JSX shell lives in `link-target-fields.tsx`;
14
+ * everything that decides anything lives here, where it can be tested
15
+ * without a DOM.
16
+ *
17
+ * The one asymmetry between callers is `allowNone`. A `LinkNode` exists
18
+ * only because it has a target, so its form always resolves to a link. An
19
+ * inline image's target is optional, so its form adds a "no link" choice
20
+ * and `toLinkAttributes` may return `undefined` — which is precisely how
21
+ * an editor removes a link that is already there.
22
+ */
23
+ import type { MultiCollectionDefinition } from '@byline/core';
24
+ import type { DocumentRelation } from '../../nodes/document-relation.js';
25
+ import type { LinkAttributes } from './index.js';
26
+ export interface LinkTargetState {
27
+ /** Narrow with `LinkTargetState['kind']` rather than a separate alias. */
28
+ kind: 'none' | 'custom' | 'internal';
29
+ /** Custom-URL text. Meaningful only when `kind === 'custom'`. */
30
+ url: string;
31
+ newTab: boolean;
32
+ /**
33
+ * Which collection the picker is currently pointed at — UI state only,
34
+ * and deliberately independent of `picked.targetCollectionPath` so the
35
+ * user can browse other collections without losing their selection.
36
+ */
37
+ targetCollection: string | null;
38
+ /** The chosen document. Meaningful only when `kind === 'internal'`. */
39
+ picked: DocumentRelation | null;
40
+ }
41
+ export interface LinkTargetOptions {
42
+ /** Whether "no link at all" is a valid outcome. */
43
+ allowNone: boolean;
44
+ }
45
+ export declare function fromLinkAttributes(link: LinkAttributes | undefined, linkable: MultiCollectionDefinition[], options: LinkTargetOptions): LinkTargetState;
46
+ /**
47
+ * Collapse the form back to a `LinkAttributes`, or `undefined` when there
48
+ * is no usable target. Callers that require a link should run
49
+ * `validateLinkTarget` first — this function reports "nothing to store"
50
+ * rather than "the user made a mistake".
51
+ */
52
+ export declare function toLinkAttributes(state: LinkTargetState): LinkAttributes | undefined;
53
+ /** Returns a human-readable problem, or `null` when the form is usable. */
54
+ export declare function validateLinkTarget(state: LinkTargetState): string | null;
@@ -0,0 +1,70 @@
1
+ import { validateUrl } from "../../utils/url.js";
2
+ function isPlaceholderUrl(url) {
3
+ return null == url || '' === url || 'https://' === url;
4
+ }
5
+ function emptyLinkTargetState(linkable, options) {
6
+ const firstLinkable = linkable[0]?.path ?? null;
7
+ const kind = options.allowNone ? 'none' : linkable.length > 0 ? 'internal' : 'custom';
8
+ return {
9
+ kind,
10
+ url: '',
11
+ newTab: false,
12
+ targetCollection: firstLinkable,
13
+ picked: null
14
+ };
15
+ }
16
+ function fromLinkAttributes(link, linkable, options) {
17
+ const base = emptyLinkTargetState(linkable, options);
18
+ if (null == link) return base;
19
+ if ('internal' === link.linkType) {
20
+ const picked = {
21
+ targetDocumentId: link.targetDocumentId,
22
+ targetCollectionId: link.targetCollectionId,
23
+ targetCollectionPath: link.targetCollectionPath,
24
+ document: link.document
25
+ };
26
+ return {
27
+ ...base,
28
+ kind: 'internal',
29
+ newTab: link.newTab ?? false,
30
+ targetCollection: picked.targetCollectionPath || base.targetCollection,
31
+ picked
32
+ };
33
+ }
34
+ const placeholder = isPlaceholderUrl(link.url);
35
+ const kind = placeholder && linkable.length > 0 ? 'internal' : 'custom';
36
+ return {
37
+ ...base,
38
+ kind,
39
+ url: placeholder ? '' : link.url ?? '',
40
+ newTab: link.newTab ?? false
41
+ };
42
+ }
43
+ function toLinkAttributes(state) {
44
+ if ('none' === state.kind) return;
45
+ if ('internal' === state.kind) {
46
+ if (null == state.picked) return;
47
+ return {
48
+ linkType: 'internal',
49
+ newTab: state.newTab,
50
+ targetDocumentId: state.picked.targetDocumentId,
51
+ targetCollectionId: state.picked.targetCollectionId,
52
+ targetCollectionPath: state.picked.targetCollectionPath,
53
+ document: state.picked.document
54
+ };
55
+ }
56
+ if (0 === state.url.length) return;
57
+ return {
58
+ linkType: 'custom',
59
+ url: state.url,
60
+ newTab: state.newTab
61
+ };
62
+ }
63
+ function validateLinkTarget(state) {
64
+ if ('none' === state.kind) return null;
65
+ if ('internal' === state.kind) return null == state.picked ? 'Pick a target document' : null;
66
+ if (0 === state.url.length) return 'Enter a URL or a root-relative path starting with /';
67
+ if (!state.url.startsWith('/') && !validateUrl(state.url)) return 'Enter a valid URL or a root-relative path starting with /';
68
+ return null;
69
+ }
70
+ export { fromLinkAttributes, toLinkAttributes, validateLinkTarget };
@@ -5,5 +5,19 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { LexicalNodeVisitor } from '../../lexical-populate-shared.js';
8
+ import type { LexicalNodeVisitor, PendingHydration } from '../../lexical-populate-shared.js';
9
+ /**
10
+ * Build the `apply` / `applyMissing` pair that refreshes an internal-link
11
+ * relation envelope in place.
12
+ *
13
+ * Shared by `linkVisitor` (where the envelope is `node.attributes`) and by
14
+ * the inline-image plugin's `inlineImageLinkVisitor` (where it is
15
+ * `node.link`). Both carry the same `DocumentRelation` shape and want the
16
+ * same three branches, so the resolution rules documented above live here
17
+ * once rather than being copied per node type.
18
+ *
19
+ * `envelope` is mutated in place — the caller owns where it hangs off the
20
+ * node.
21
+ */
22
+ export declare function createInternalLinkHydration(envelope: Record<string, any>, collectionPath: string, documentId: string): Pick<PendingHydration, 'apply' | 'applyMissing'>;
9
23
  export declare const linkVisitor: LexicalNodeVisitor;
@@ -1,4 +1,57 @@
1
1
  import { getCollectionDefinition, getLogger } from "@byline/core";
2
+ function createInternalLinkHydration(envelope, collectionPath, documentId) {
3
+ return {
4
+ apply (target) {
5
+ const definition = getCollectionDefinition(collectionPath);
6
+ const useAsTitle = definition?.useAsTitle ?? 'title';
7
+ const targetFields = target.fields ?? {};
8
+ const next = {
9
+ ...envelope.document ?? {}
10
+ };
11
+ const title = targetFields[useAsTitle];
12
+ if ('string' == typeof title && title.length > 0) next.title = title;
13
+ let pathThrew = false;
14
+ let built;
15
+ if (definition?.buildDocumentPath != null) try {
16
+ built = definition.buildDocumentPath({
17
+ id: target.id,
18
+ path: target.path,
19
+ status: target.status,
20
+ fields: targetFields
21
+ }, {
22
+ collectionPath
23
+ });
24
+ } catch (err) {
25
+ pathThrew = true;
26
+ getLogger().info({
27
+ collectionPath,
28
+ documentId,
29
+ err
30
+ }, 'buildDocumentPath threw');
31
+ }
32
+ if (!pathThrew) if ('string' == typeof built) next.path = built;
33
+ else {
34
+ const targetPath = target.path;
35
+ if ('string' == typeof targetPath && targetPath.length > 0) next.path = `/${collectionPath}/${targetPath}`;
36
+ }
37
+ if ('_resolved' in next) delete next._resolved;
38
+ envelope.document = next;
39
+ },
40
+ applyMissing () {
41
+ getLogger().warn({
42
+ collectionPath,
43
+ documentId
44
+ }, 'internal link target not found');
45
+ const next = {
46
+ ...envelope.document ?? {}
47
+ };
48
+ delete next.title;
49
+ delete next.path;
50
+ next._resolved = false;
51
+ envelope.document = next;
52
+ }
53
+ };
54
+ }
2
55
  const linkVisitor = {
3
56
  match (node) {
4
57
  if ('link' !== node.type) return null;
@@ -12,56 +65,8 @@ const linkVisitor = {
12
65
  node,
13
66
  collectionPath,
14
67
  documentId,
15
- apply (target) {
16
- const definition = getCollectionDefinition(collectionPath);
17
- const useAsTitle = definition?.useAsTitle ?? 'title';
18
- const targetFields = target.fields ?? {};
19
- const next = {
20
- ...attributes.document ?? {}
21
- };
22
- const title = targetFields[useAsTitle];
23
- if ('string' == typeof title && title.length > 0) next.title = title;
24
- let pathThrew = false;
25
- let built;
26
- if (definition?.buildDocumentPath != null) try {
27
- built = definition.buildDocumentPath({
28
- id: target.id,
29
- path: target.path,
30
- status: target.status,
31
- fields: targetFields
32
- }, {
33
- collectionPath
34
- });
35
- } catch (err) {
36
- pathThrew = true;
37
- getLogger().info({
38
- collectionPath,
39
- documentId,
40
- err
41
- }, 'buildDocumentPath threw');
42
- }
43
- if (!pathThrew) if ('string' == typeof built) next.path = built;
44
- else {
45
- const targetPath = target.path;
46
- if ('string' == typeof targetPath && targetPath.length > 0) next.path = `/${collectionPath}/${targetPath}`;
47
- }
48
- if ('_resolved' in next) delete next._resolved;
49
- attributes.document = next;
50
- },
51
- applyMissing () {
52
- getLogger().warn({
53
- collectionPath,
54
- documentId
55
- }, 'internal link target not found');
56
- const next = {
57
- ...attributes.document ?? {}
58
- };
59
- delete next.title;
60
- delete next.path;
61
- next._resolved = false;
62
- attributes.document = next;
63
- }
68
+ ...createInternalLinkHydration(attributes, collectionPath, documentId)
64
69
  };
65
70
  }
66
71
  };
67
- export { linkVisitor };
72
+ export { createInternalLinkHydration, linkVisitor };
@@ -253,7 +253,12 @@ function serializeImage(node, ctx) {
253
253
  const src = 'string' == typeof node.src ? node.src : '';
254
254
  if (0 === src.length) return '';
255
255
  const alt = 'string' == typeof node.altText ? node.altText : '';
256
- const image = `![${alt.replace(/[[\]]/g, '')}](${src})`;
256
+ let image = `![${alt.replace(/[[\]]/g, '')}](${src})`;
257
+ const link = node.link;
258
+ if (null != link) {
259
+ const href = resolveLinkUrl(link, ctx);
260
+ if (null != href) image = `[${image}](${href})`;
261
+ }
257
262
  const caption = node.caption;
258
263
  if (true === node.showCaption && caption?.editorState != null) {
259
264
  const captionRoot = resolveRoot(caption.editorState);
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Fragment as external_react_Fragment, useCallback, useEffect, useMemo, useState } from "react";
4
- import { $createCodeNode, $isCodeNode, CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, CodeNode, getLanguageFriendlyName } from "@lexical/code";
4
+ import { $createCodeNode, $isCodeNode, CodeNode } from "@lexical/code";
5
+ import { CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, getLanguageFriendlyName } from "@lexical/code-prism";
5
6
  import { $isListNode, CheckListExtension, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
6
7
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
7
8
  import { $isDecoratorBlockNode } from "@lexical/react/LexicalDecoratorBlockNode";
@@ -7,6 +7,6 @@ function getSelectedNode(selection) {
7
7
  if (anchorNode === focusNode) return anchorNode;
8
8
  const isBackward = selection.isBackward();
9
9
  if (isBackward) return $isAtNodeEnd(focus) ? anchorNode : focusNode;
10
- return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
10
+ else return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
11
11
  }
12
12
  export { getSelectedNode };
@@ -38,8 +38,7 @@ class Rect {
38
38
  },
39
39
  result
40
40
  };
41
- }
42
- {
41
+ } else {
43
42
  const { top, left, bottom, right } = target;
44
43
  return top >= this._top && top <= this._bottom && bottom >= this._top && bottom <= this._bottom && left >= this._left && left <= this._right && right >= this._left && right <= this._right;
45
44
  }
@@ -45,7 +45,7 @@ const RichTextField = ({ field, value, defaultValue, editorConfig, readonly = fa
45
45
  id: fieldId,
46
46
  name: field.name,
47
47
  description: field.helpText,
48
- readonly: readonly,
48
+ readonly: readonly || true === field.readOnly,
49
49
  label: labelNode,
50
50
  required: !field.optional,
51
51
  value: incomingValue,
package/dist/server.d.ts CHANGED
@@ -42,7 +42,7 @@ export { defaultEditorConfig } from './field/config/default.js';
42
42
  export { lexicalToText } from './field/lexical-to-text.js';
43
43
  export { type LexicalToMarkdownOptions, type LexicalToMarkdownResult, type LexicalToMarkdownWarning, lexicalToMarkdown, } from './field/markdown/lexical-to-markdown.js';
44
44
  import { type LexicalToMarkdownOptions } from './field/markdown/lexical-to-markdown.js';
45
- export { inlineImageVisitor } from './field/extensions/inline-image/populate.js';
45
+ export { inlineImageLinkVisitor, inlineImageVisitor, } from './field/extensions/inline-image/populate.js';
46
46
  export { linkVisitor } from './field/extensions/link/populate.js';
47
47
  export type { EditorConfig, EditorSettings, EditorSettingsOverride } from './field/config/types.js';
48
48
  export type { LexicalNodeLike, LexicalNodeVisitor, PendingHydration, } from './field/lexical-populate-shared.js';
@@ -56,7 +56,7 @@ export interface LexicalServerOptions {
56
56
  getClient: () => BylineClient<any>;
57
57
  /**
58
58
  * Override the visitor list. Defaults to every visitor the package
59
- * ships — currently `[inlineImageVisitor, linkVisitor]`. Useful when a
59
+ * ships — currently `[inlineImageVisitor, inlineImageLinkVisitor, linkVisitor]`. Useful when a
60
60
  * host wants to register additional custom visitors alongside the
61
61
  * built-ins, or temporarily disable a built-in:
62
62
  *
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { inlineImageVisitor } from "./field/extensions/inline-image/populate.js";
1
+ import { inlineImageLinkVisitor, inlineImageVisitor } from "./field/extensions/inline-image/populate.js";
2
2
  import { linkVisitor } from "./field/extensions/link/populate.js";
3
3
  import { runLexicalPopulate } from "./field/lexical-populate-shared.js";
4
4
  import { lexicalToMarkdown } from "./field/markdown/lexical-to-markdown.js";
@@ -6,6 +6,7 @@ import { lexicalToText } from "./field/lexical-to-text.js";
6
6
  function lexicalEditorPopulateServer(options) {
7
7
  const visitors = options.visitors ?? [
8
8
  inlineImageVisitor,
9
+ inlineImageLinkVisitor,
9
10
  linkVisitor
10
11
  ];
11
12
  return async (ctx)=>{
@@ -28,6 +29,7 @@ function lexicalEditorToTextServer() {
28
29
  function lexicalEditorEmbedServer(options) {
29
30
  const visitors = options.visitors ?? [
30
31
  inlineImageVisitor,
32
+ inlineImageLinkVisitor,
31
33
  linkVisitor
32
34
  ];
33
35
  return async (ctx)=>{
@@ -42,4 +44,4 @@ function lexicalEditorEmbedServer(options) {
42
44
  };
43
45
  }
44
46
  export { defaultEditorConfig } from "./field/config/default.js";
45
- export { inlineImageVisitor, lexicalEditorEmbedServer, lexicalEditorPopulateServer, lexicalEditorToMarkdownServer, lexicalEditorToTextServer, lexicalToMarkdown, lexicalToText, linkVisitor };
47
+ export { inlineImageLinkVisitor, inlineImageVisitor, lexicalEditorEmbedServer, lexicalEditorPopulateServer, lexicalEditorToMarkdownServer, lexicalEditorToTextServer, lexicalToMarkdown, lexicalToText, linkVisitor };
@@ -1,4 +1,4 @@
1
- import type { SerializedEditorState, SerializedLexicalNode } from 'lexical';
1
+ import type { SerializedEditorState } from 'lexical';
2
2
  /**
3
3
  * createEmptyEditorState
4
4
  * @returns
@@ -21,4 +21,4 @@ import type { SerializedEditorState, SerializedLexicalNode } from 'lexical';
21
21
  "version": 1
22
22
  }
23
23
  */
24
- export declare function createEmptyEditorState(): SerializedEditorState<SerializedLexicalNode>;
24
+ export declare function createEmptyEditorState(): SerializedEditorState;
@@ -1,2 +1,2 @@
1
- import type { SerializedEditorState, SerializedLexicalNode } from 'lexical';
2
- export declare function hasText(value: SerializedEditorState<SerializedLexicalNode> | null | undefined): boolean;
1
+ import type { SerializedEditorState } from 'lexical';
2
+ export declare function hasText(value: SerializedEditorState | null | undefined): boolean;