@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.
Files changed (57) hide show
  1. package/dist/field/extensions/code-highlight/code-highlight-extension.js +2 -1
  2. package/dist/field/extensions/inline-image/inline-image-extension.js +4 -2
  3. package/dist/field/extensions/inline-image/inline-image-modal.css +67 -0
  4. package/dist/field/extensions/inline-image/inline-image-modal.js +173 -98
  5. package/dist/field/extensions/inline-image/inline-image-node-component.css +37 -0
  6. package/dist/field/extensions/inline-image/inline-image-node-component.d.ts +3 -1
  7. package/dist/field/extensions/inline-image/inline-image-node-component.js +39 -1
  8. package/dist/field/extensions/inline-image/inline-image-node.d.ts +13 -2
  9. package/dist/field/extensions/inline-image/inline-image-node.js +63 -13
  10. package/dist/field/extensions/inline-image/node-types.d.ts +19 -0
  11. package/dist/field/extensions/inline-image/populate.d.ts +22 -0
  12. package/dist/field/extensions/inline-image/populate.js +19 -1
  13. package/dist/field/extensions/inline-image/types.d.ts +7 -0
  14. package/dist/field/extensions/link/link-href.d.ts +30 -0
  15. package/dist/field/extensions/link/link-href.js +14 -0
  16. package/dist/field/extensions/link/link-modal.js +96 -288
  17. package/dist/field/extensions/link/link-target-fields.css +59 -0
  18. package/dist/field/extensions/link/link-target-fields.d.ts +38 -0
  19. package/dist/field/extensions/link/link-target-fields.js +227 -0
  20. package/dist/field/extensions/link/link-target-state.d.ts +54 -0
  21. package/dist/field/extensions/link/link-target-state.js +70 -0
  22. package/dist/field/extensions/link/populate.d.ts +15 -1
  23. package/dist/field/extensions/link/populate.js +55 -50
  24. package/dist/field/markdown/lexical-to-markdown.js +6 -1
  25. package/dist/field/plugins/toolbar-plugin/index.js +2 -1
  26. package/dist/server.d.ts +2 -2
  27. package/dist/server.js +4 -2
  28. package/dist/validate/createEmptyEditorState.d.ts +2 -2
  29. package/dist/validate/hasText.d.ts +2 -2
  30. package/package.json +25 -24
  31. package/src/field/extensions/code-highlight/code-highlight-extension.ts +8 -1
  32. package/src/field/extensions/inline-image/inline-image-extension.tsx +5 -0
  33. package/src/field/extensions/inline-image/inline-image-modal.css +100 -0
  34. package/src/field/extensions/inline-image/inline-image-modal.tsx +178 -85
  35. package/src/field/extensions/inline-image/inline-image-node-component.css +44 -0
  36. package/src/field/extensions/inline-image/inline-image-node-component.tsx +52 -0
  37. package/src/field/extensions/inline-image/inline-image-node.test.tsx +331 -0
  38. package/src/field/extensions/inline-image/inline-image-node.tsx +108 -11
  39. package/src/field/extensions/inline-image/node-types.ts +19 -0
  40. package/src/field/extensions/inline-image/populate-link.test.node.ts +320 -0
  41. package/src/field/extensions/inline-image/populate.ts +44 -0
  42. package/src/field/extensions/inline-image/types.ts +7 -0
  43. package/src/field/extensions/link/link-href.test.node.ts +78 -0
  44. package/src/field/extensions/link/link-href.ts +52 -0
  45. package/src/field/extensions/link/link-modal.tsx +86 -325
  46. package/src/field/extensions/link/link-target-fields.css +81 -0
  47. package/src/field/extensions/link/link-target-fields.tsx +275 -0
  48. package/src/field/extensions/link/link-target-state.test.node.ts +197 -0
  49. package/src/field/extensions/link/link-target-state.ts +158 -0
  50. package/src/field/extensions/link/populate.ts +94 -67
  51. package/src/field/markdown/lexical-to-markdown.test.node.ts +68 -0
  52. package/src/field/markdown/lexical-to-markdown.ts +10 -1
  53. package/src/field/plugins/toolbar-plugin/index.tsx +7 -4
  54. package/src/server.ts +11 -5
  55. package/src/validate/createEmptyEditorState.ts +2 -7
  56. package/src/validate/hasText.test.ts +1 -2
  57. package/src/validate/hasText.ts +2 -9
@@ -1,311 +1,119 @@
1
1
  "use client";
2
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useState } from "react";
4
- import { RelationPicker } from "@byline/admin/react";
5
- import { getAdminConfig, getCollectionDefinition, isSingleton } from "@byline/core";
6
- import { Button, Checkbox, CloseIcon, IconButton, Input, Label, Modal, RadioGroup, RadioGroupItem, Select } from "@byline/ui/react";
4
+ import { getAdminConfig, isSingleton } from "@byline/core";
5
+ import { Button, CloseIcon, IconButton, Input, Modal } from "@byline/ui/react";
7
6
  import { useModalFormState } from "../../shared/useModalFormState.js";
8
- import { validateUrl } from "../../utils/url.js";
9
- function emptyState(linkable) {
10
- return {
11
- text: '',
12
- linkType: linkable.length > 0 ? 'internal' : 'custom',
13
- url: '',
14
- newTab: false,
15
- targetCollection: linkable[0]?.path ?? null,
16
- picked: null
17
- };
18
- }
19
- function fromLinkData(data, linkable) {
20
- const base = emptyState(linkable);
21
- if (!data) return base;
22
- const fields = data.fields;
23
- if (!fields) return base;
24
- const url = 'internal' === fields.linkType ? '' : fields.url ?? '';
25
- const isPlaceholderUrl = '' === url || 'https://' === url;
26
- const wantsInternal = linkable.length > 0 && ('internal' === fields.linkType || isPlaceholderUrl);
27
- const linkType = wantsInternal ? 'internal' : 'custom';
28
- const picked = 'internal' === fields.linkType ? {
29
- targetDocumentId: fields.targetDocumentId,
30
- targetCollectionId: fields.targetCollectionId,
31
- targetCollectionPath: fields.targetCollectionPath,
32
- document: fields.document
33
- } : null;
34
- return {
35
- text: data.text ?? '',
36
- linkType,
37
- url: isPlaceholderUrl ? '' : url,
38
- newTab: fields.newTab ?? false,
39
- targetCollection: picked?.targetCollectionPath ?? linkable[0]?.path ?? null,
40
- picked
41
- };
42
- }
7
+ import { LinkTargetFields } from "./link-target-fields.js";
8
+ import { fromLinkAttributes, toLinkAttributes, validateLinkTarget } from "./link-target-state.js";
9
+ const LINK_TARGET_OPTIONS = {
10
+ allowNone: false
11
+ };
43
12
  const LinkModal = ({ isOpen = false, onSubmit, onClose, data: dataFromProps })=>{
44
13
  const linkable = useMemo(()=>getAdminConfig().collections.filter((collection)=>!isSingleton(collection) && true === collection.linksInEditor), []);
45
- const [pickerOpen, setPickerOpen] = useState(false);
46
14
  const [urlError, setUrlError] = useState(null);
47
- const [state, setState] = useModalFormState(isOpen, ()=>fromLinkData(dataFromProps, linkable), ()=>setUrlError(null));
48
- const targetDefinition = state.targetCollection ? getCollectionDefinition(state.targetCollection) : null;
49
- const targetDef = null == targetDefinition || isSingleton(targetDefinition) ? null : targetDefinition;
50
- const collectionItems = useMemo(()=>linkable.map((c)=>({
51
- label: c.labels.singular,
52
- value: c.path
53
- })), [
54
- linkable
55
- ]);
56
- const pickedLabel = useMemo(()=>{
57
- if ('internal' !== state.linkType || !state.picked) return null;
58
- const title = state.picked.document?.title;
59
- if ('string' == typeof title && title.length > 0) return title;
60
- const pickedDefinition = getCollectionDefinition(state.picked.targetCollectionPath);
61
- const pickedDef = null == pickedDefinition || isSingleton(pickedDefinition) ? null : pickedDefinition;
62
- const short = state.picked.targetDocumentId.slice(0, 8);
63
- return `${pickedDef?.labels.singular ?? state.picked.targetCollectionPath} · ${short}…`;
64
- }, [
65
- state.linkType,
66
- state.picked
67
- ]);
68
- const handlePickerSelect = (selection)=>{
69
- setPickerOpen(false);
70
- setState((s)=>{
71
- const targetCollection = s.targetCollection;
72
- const titleField = getCollectionDefinition(targetCollection)?.useAsTitle;
73
- const title = titleField ? selection.record?.fields?.[titleField] : void 0;
74
- const path = selection.record?.path;
75
- const document = {};
76
- if ('string' == typeof title && title.length > 0) document.title = title;
77
- if ('string' == typeof path && path.length > 0) document.path = path;
78
- return {
79
- ...s,
80
- picked: {
81
- targetDocumentId: selection.targetDocumentId,
82
- targetCollectionId: selection.targetCollectionId,
83
- targetCollectionPath: targetCollection,
84
- document: Object.keys(document).length > 0 ? document : void 0
85
- }
86
- };
87
- });
88
- };
15
+ const [state, setState] = useModalFormState(isOpen, ()=>({
16
+ text: dataFromProps?.text ?? '',
17
+ target: fromLinkAttributes(dataFromProps?.fields, linkable, LINK_TARGET_OPTIONS)
18
+ }), ()=>setUrlError(null));
89
19
  const handleSave = ()=>{
90
- if ('custom' === state.linkType) {
91
- const url = state.url;
92
- if (!url.startsWith('/') && !validateUrl(url)) return void setUrlError('Enter a valid URL or a root-relative path starting with /');
93
- }
94
- if ('internal' === state.linkType) {
95
- if (!state.picked) return void setUrlError('Pick a target document');
96
- }
97
- const picked = state.picked;
98
- const fields = 'custom' === state.linkType ? {
99
- linkType: 'custom',
100
- url: state.url,
101
- newTab: state.newTab
102
- } : {
103
- linkType: 'internal',
104
- newTab: state.newTab,
105
- targetDocumentId: picked.targetDocumentId,
106
- targetCollectionId: picked.targetCollectionId,
107
- targetCollectionPath: picked.targetCollectionPath,
108
- document: picked.document
109
- };
20
+ const problem = validateLinkTarget(state.target);
21
+ if (null != problem) return void setUrlError(problem);
22
+ const fields = toLinkAttributes(state.target);
23
+ if (null == fields) return void setUrlError('Pick a target document');
110
24
  onSubmit({
111
25
  text: state.text.length > 0 ? state.text : null,
112
- fields
26
+ fields: fields
113
27
  });
114
28
  onClose();
115
29
  };
116
30
  if (!isOpen) return null;
117
- return /*#__PURE__*/ jsxs(Fragment, {
118
- children: [
119
- /*#__PURE__*/ jsx(Modal, {
120
- isOpen: isOpen,
121
- onDismiss: onClose,
122
- closeOnOverlayClick: false,
123
- children: /*#__PURE__*/ jsxs(Modal.Container, {
124
- style: {
125
- maxWidth: '480px',
126
- width: '100%'
127
- },
31
+ return /*#__PURE__*/ jsx(Modal, {
32
+ isOpen: isOpen,
33
+ onDismiss: onClose,
34
+ closeOnOverlayClick: false,
35
+ children: /*#__PURE__*/ jsxs(Modal.Container, {
36
+ style: {
37
+ maxWidth: '480px',
38
+ width: '100%'
39
+ },
40
+ children: [
41
+ /*#__PURE__*/ jsxs(Modal.Header, {
42
+ className: "flex items-center justify-between pt-4 mb-4",
128
43
  children: [
129
- /*#__PURE__*/ jsxs(Modal.Header, {
130
- className: "flex items-center justify-between pt-4 mb-4",
131
- children: [
132
- /*#__PURE__*/ jsx("h3", {
133
- className: "m-0 text-xl",
134
- children: "Edit link"
135
- }),
136
- /*#__PURE__*/ jsx(IconButton, {
137
- "aria-label": "Close",
138
- size: "xs",
139
- onClick: onClose,
140
- children: /*#__PURE__*/ jsx(CloseIcon, {
141
- width: "15px",
142
- height: "15px",
143
- svgClassName: "white-icon"
144
- })
145
- })
146
- ]
44
+ /*#__PURE__*/ jsx("h3", {
45
+ className: "m-0 text-xl",
46
+ children: "Edit link"
147
47
  }),
148
- /*#__PURE__*/ jsx(Modal.Content, {
149
- children: /*#__PURE__*/ jsxs("div", {
150
- className: "flex flex-col gap-4",
151
- children: [
152
- /*#__PURE__*/ jsx(Input, {
153
- id: "link-text",
154
- className: "mb-2",
155
- name: "text",
156
- label: "Link text",
157
- placeholder: "Visible link text",
158
- value: state.text,
159
- onChange: (e)=>setState((s)=>({
160
- ...s,
161
- text: e.target.value
162
- }))
163
- }),
164
- linkable.length > 0 && /*#__PURE__*/ jsxs(RadioGroup, {
165
- id: "link-type",
166
- name: "linkType",
167
- "aria-label": "Link type",
168
- direction: "row",
169
- value: state.linkType,
170
- onValueChange: (value)=>setState((s)=>({
171
- ...s,
172
- linkType: 'internal' === value ? 'internal' : 'custom'
173
- })),
174
- children: [
175
- /*#__PURE__*/ jsx(RadioGroupItem, {
176
- id: "link-type-internal",
177
- value: "internal",
178
- label: "Document"
179
- }),
180
- /*#__PURE__*/ jsx(RadioGroupItem, {
181
- id: "link-type-custom",
182
- value: "custom",
183
- label: "Custom URL"
184
- })
185
- ]
186
- }),
187
- 'custom' === state.linkType && /*#__PURE__*/ jsx(Input, {
188
- id: "link-url",
189
- name: "url",
190
- label: "URL",
191
- placeholder: "https://example.com or /path",
192
- value: state.url,
193
- errorText: urlError ?? void 0,
194
- error: null != urlError,
195
- onChange: (e)=>{
196
- setUrlError(null);
197
- setState((s)=>({
198
- ...s,
199
- url: e.target.value
200
- }));
201
- }
202
- }),
203
- 'internal' === state.linkType && /*#__PURE__*/ jsxs("div", {
204
- className: "flex flex-col gap-3 mt-2",
205
- children: [
206
- linkable.length > 1 && /*#__PURE__*/ jsxs("div", {
207
- children: [
208
- /*#__PURE__*/ jsx(Label, {
209
- id: "link-target-collection",
210
- htmlFor: "link-target-collection",
211
- className: "mb-1",
212
- label: "Target collection"
213
- }),
214
- /*#__PURE__*/ jsx(Select, {
215
- size: "sm",
216
- items: collectionItems,
217
- placeholder: "Target collection",
218
- value: state.targetCollection ?? void 0,
219
- onValueChange: (value)=>{
220
- if (null == value) return;
221
- setState((s)=>({
222
- ...s,
223
- targetCollection: value
224
- }));
225
- }
226
- })
227
- ]
228
- }),
229
- /*#__PURE__*/ jsxs("div", {
230
- className: "border rounded p-3",
231
- children: [
232
- /*#__PURE__*/ jsx(Label, {
233
- id: "link-target-document",
234
- htmlFor: "link-target-document",
235
- className: "mb-1",
236
- label: "Target document"
237
- }),
238
- /*#__PURE__*/ jsxs("div", {
239
- className: "flex items-center justify-between gap-2",
240
- children: [
241
- pickedLabel && /*#__PURE__*/ jsx("span", {
242
- className: "text-sm text-accent-400 truncate",
243
- children: pickedLabel
244
- }),
245
- /*#__PURE__*/ jsx(Button, {
246
- size: "sm",
247
- variant: "outlined",
248
- intent: "noeffect",
249
- type: "button",
250
- onClick: ()=>setPickerOpen(true),
251
- disabled: !state.targetCollection,
252
- children: pickedLabel ? 'Change' : `Pick ${targetDef?.labels.singular ?? 'document'}…`
253
- })
254
- ]
255
- })
256
- ]
257
- }),
258
- urlError && 'internal' === state.linkType && /*#__PURE__*/ jsx("span", {
259
- className: "text-xs text-red-400",
260
- children: urlError
261
- })
262
- ]
263
- }),
264
- /*#__PURE__*/ jsx(Checkbox, {
265
- id: "link-new-tab",
266
- name: "newTab",
267
- label: "Open in new tab",
268
- checked: state.newTab,
269
- onCheckedChange: (checked)=>setState((s)=>({
270
- ...s,
271
- newTab: true === checked
272
- }))
273
- })
274
- ]
48
+ /*#__PURE__*/ jsx(IconButton, {
49
+ "aria-label": "Close",
50
+ size: "xs",
51
+ onClick: onClose,
52
+ children: /*#__PURE__*/ jsx(CloseIcon, {
53
+ width: "15px",
54
+ height: "15px",
55
+ svgClassName: "white-icon"
275
56
  })
57
+ })
58
+ ]
59
+ }),
60
+ /*#__PURE__*/ jsx(Modal.Content, {
61
+ children: /*#__PURE__*/ jsxs("div", {
62
+ className: "flex flex-col gap-4",
63
+ children: [
64
+ /*#__PURE__*/ jsx(Input, {
65
+ id: "link-text",
66
+ className: "mb-2",
67
+ name: "text",
68
+ label: "Link text",
69
+ placeholder: "Visible link text",
70
+ value: state.text,
71
+ onChange: (e)=>setState((s)=>({
72
+ ...s,
73
+ text: e.target.value
74
+ }))
75
+ }),
76
+ /*#__PURE__*/ jsx(LinkTargetFields, {
77
+ idPrefix: "link",
78
+ state: state.target,
79
+ onChange: (target)=>{
80
+ setUrlError(null);
81
+ setState((s)=>({
82
+ ...s,
83
+ target
84
+ }));
85
+ },
86
+ linkable: linkable,
87
+ allowNone: false,
88
+ error: urlError,
89
+ legend: "Link type"
90
+ })
91
+ ]
92
+ })
93
+ }),
94
+ /*#__PURE__*/ jsxs(Modal.Actions, {
95
+ className: "flex gap-3",
96
+ children: [
97
+ /*#__PURE__*/ jsx(Button, {
98
+ size: "sm",
99
+ intent: "noeffect",
100
+ type: "button",
101
+ onClick: onClose,
102
+ className: "min-w-[70px]",
103
+ children: "Cancel"
276
104
  }),
277
- /*#__PURE__*/ jsxs(Modal.Actions, {
278
- className: "flex gap-3",
279
- children: [
280
- /*#__PURE__*/ jsx(Button, {
281
- size: "sm",
282
- intent: "noeffect",
283
- type: "button",
284
- onClick: onClose,
285
- className: "min-w-[70px]",
286
- children: "Cancel"
287
- }),
288
- /*#__PURE__*/ jsx(Button, {
289
- size: "sm",
290
- intent: "primary",
291
- type: "button",
292
- onClick: handleSave,
293
- className: "min-w-[70px]",
294
- children: "Save"
295
- })
296
- ]
105
+ /*#__PURE__*/ jsx(Button, {
106
+ size: "sm",
107
+ intent: "primary",
108
+ type: "button",
109
+ onClick: handleSave,
110
+ className: "min-w-[70px]",
111
+ children: "Save"
297
112
  })
298
113
  ]
299
114
  })
300
- }),
301
- 'internal' === state.linkType && state.targetCollection && /*#__PURE__*/ jsx(RelationPicker, {
302
- targetCollectionPath: state.targetCollection,
303
- targetDefinition: targetDef,
304
- isOpen: pickerOpen,
305
- onSelect: handlePickerSelect,
306
- onDismiss: ()=>setPickerOpen(false)
307
- })
308
- ]
115
+ ]
116
+ })
309
117
  });
310
118
  };
311
119
  export { LinkModal };
@@ -0,0 +1,59 @@
1
+ .byline-link-target-frame {
2
+ border: var(--border-width-thin, 1px) var(--border-style-solid, solid) var(--field-border);
3
+ border-radius: var(--border-radius-md, 6px);
4
+ margin-top: .25rem;
5
+ padding: .5rem;
6
+ }
7
+
8
+ .byline-link-target-tile {
9
+ justify-content: space-between;
10
+ align-items: flex-start;
11
+ gap: var(--spacing-8, .5rem);
12
+ padding: var(--spacing-8, .5rem);
13
+ border: var(--border-width-thin, 1px) var(--border-style-solid, solid) var(--primary-500);
14
+ border-radius: var(--border-radius-md, 6px);
15
+ font-size: var(--font-size-xs);
16
+ display: flex;
17
+ }
18
+
19
+ .byline-link-target-summary {
20
+ flex-direction: column;
21
+ gap: .125rem;
22
+ min-width: 0;
23
+ display: flex;
24
+ }
25
+
26
+ .byline-link-target-kind {
27
+ color: var(--gray-500);
28
+ }
29
+
30
+ .byline-link-target-value {
31
+ color: var(--gray-900);
32
+ text-overflow: ellipsis;
33
+ white-space: nowrap;
34
+ overflow: hidden;
35
+ }
36
+
37
+ .byline-link-target-actions {
38
+ align-items: center;
39
+ gap: var(--spacing-4, .25rem);
40
+ flex-shrink: 0;
41
+ display: flex;
42
+ }
43
+
44
+ .byline-link-target-actions .byline-button {
45
+ color: var(--gray-900);
46
+ }
47
+
48
+ :is([data-theme="dark"], .dark) .byline-link-target-frame {
49
+ border-color: var(--canvas-700, var(--field-border));
50
+ }
51
+
52
+ :is([data-theme="dark"], .dark) .byline-link-target-value {
53
+ color: var(--gray-100);
54
+ }
55
+
56
+ :is([data-theme="dark"], .dark) .byline-link-target-actions .byline-button {
57
+ color: var(--gray-200);
58
+ }
59
+
@@ -0,0 +1,38 @@
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 "where does this point?" sub-form, shared by the link modal and the
10
+ * inline-image modal.
11
+ *
12
+ * Purely presentational — every decision lives in `link-target-state.ts`,
13
+ * which is where the rules are tested. This file owns the markup, the
14
+ * picker's open/closed state, and nothing else.
15
+ *
16
+ * `idPrefix` is required rather than defaulted: two of these can be
17
+ * mounted in one document (an image modal over a link modal), and
18
+ * duplicate element ids break the `Label`/`htmlFor` association that
19
+ * screen readers depend on.
20
+ */
21
+ import type * as React from 'react';
22
+ import type { MultiCollectionDefinition } from '@byline/core';
23
+ import type { LinkTargetState } from './link-target-state.js';
24
+ import './link-target-fields.css';
25
+ export interface LinkTargetFieldsProps {
26
+ idPrefix: string;
27
+ state: LinkTargetState;
28
+ onChange: (next: LinkTargetState) => void;
29
+ /** Collections with `linksInEditor: true`. Empty disables the picker. */
30
+ linkable: MultiCollectionDefinition[];
31
+ /** Offer a "no link" choice. See `LinkTargetOptions.allowNone`. */
32
+ allowNone: boolean;
33
+ /** Validation message from `validateLinkTarget`, if the form was submitted. */
34
+ error?: string | null;
35
+ /** Label above the kind radio. */
36
+ legend?: string;
37
+ }
38
+ export declare const LinkTargetFields: React.FC<LinkTargetFieldsProps>;