@byline/richtext-lexical 6.4.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.
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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";
|
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
|
|
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
|
|
24
|
+
export declare function createEmptyEditorState(): SerializedEditorState;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { SerializedEditorState
|
|
2
|
-
export declare function hasText(value: SerializedEditorState
|
|
1
|
+
import type { SerializedEditorState } from 'lexical';
|
|
2
|
+
export declare function hasText(value: SerializedEditorState | null | undefined): boolean;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "6.
|
|
6
|
+
"version": "6.5.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -62,39 +62,40 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@lexical/code": "0.
|
|
66
|
-
"@lexical/
|
|
67
|
-
"@lexical/
|
|
68
|
-
"@lexical/
|
|
69
|
-
"@lexical/
|
|
70
|
-
"@lexical/
|
|
71
|
-
"@lexical/
|
|
72
|
-
"@lexical/
|
|
73
|
-
"@lexical/
|
|
74
|
-
"@lexical/
|
|
75
|
-
"@lexical/
|
|
76
|
-
"@lexical/
|
|
77
|
-
"@lexical/
|
|
65
|
+
"@lexical/code": "0.51.0",
|
|
66
|
+
"@lexical/code-prism": "0.51.0",
|
|
67
|
+
"@lexical/extension": "0.51.0",
|
|
68
|
+
"@lexical/headless": "0.51.0",
|
|
69
|
+
"@lexical/link": "0.51.0",
|
|
70
|
+
"@lexical/list": "0.51.0",
|
|
71
|
+
"@lexical/mark": "0.51.0",
|
|
72
|
+
"@lexical/markdown": "0.51.0",
|
|
73
|
+
"@lexical/overflow": "0.51.0",
|
|
74
|
+
"@lexical/react": "0.51.0",
|
|
75
|
+
"@lexical/rich-text": "0.51.0",
|
|
76
|
+
"@lexical/selection": "0.51.0",
|
|
77
|
+
"@lexical/table": "0.51.0",
|
|
78
|
+
"@lexical/utils": "0.51.0",
|
|
78
79
|
"clsx": "^2.1.1",
|
|
79
|
-
"lexical": "0.
|
|
80
|
+
"lexical": "0.51.0",
|
|
80
81
|
"react-error-boundary": "^6.1.5",
|
|
81
|
-
"@byline/admin": "6.
|
|
82
|
-
"@byline/
|
|
83
|
-
"@byline/
|
|
84
|
-
"@byline/
|
|
85
|
-
"@byline/
|
|
82
|
+
"@byline/admin": "6.5.0",
|
|
83
|
+
"@byline/core": "6.5.0",
|
|
84
|
+
"@byline/i18n": "6.5.0",
|
|
85
|
+
"@byline/client": "6.5.0",
|
|
86
|
+
"@byline/ui": "6.5.0"
|
|
86
87
|
},
|
|
87
88
|
"peerDependencies": {
|
|
88
89
|
"react": "^19.0.0",
|
|
89
90
|
"react-dom": "^19.0.0"
|
|
90
91
|
},
|
|
91
92
|
"devDependencies": {
|
|
92
|
-
"@biomejs/biome": "2.5.
|
|
93
|
-
"@lexical/clipboard": "0.
|
|
94
|
-
"@lexical/html": "0.
|
|
93
|
+
"@biomejs/biome": "2.5.14",
|
|
94
|
+
"@lexical/clipboard": "0.51.0",
|
|
95
|
+
"@lexical/html": "0.51.0",
|
|
95
96
|
"@rsbuild/plugin-react": "^2.1.0",
|
|
96
97
|
"@rslib/core": "^1.0.1",
|
|
97
|
-
"@types/node": "^26.6.
|
|
98
|
+
"@types/node": "^26.6.2",
|
|
98
99
|
"@types/react": "19.3.0",
|
|
99
100
|
"@types/react-dom": "19.3.0",
|
|
100
101
|
"@vitejs/plugin-react": "^6.1.1",
|