@bettercms-ai/kit 2.0.0 → 2.0.1
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/adapters/lossy.d.ts +12 -4
- package/dist/adapters/lossy.js +19 -8
- package/dist/adapters/lossy.js.map +1 -1
- package/dist/adapters/prod-types.d.ts +46 -14
- package/dist/adapters/prod-types.js +37 -35
- package/dist/adapters/prod-types.js.map +1 -1
- package/dist/adapters/schema.d.ts +39 -0
- package/dist/adapters/schema.js +281 -82
- package/dist/adapters/schema.js.map +1 -1
- package/dist/context.d.ts +39 -0
- package/dist/context.js +40 -1
- package/dist/context.js.map +1 -1
- package/dist/controls/form-picker-register.d.ts +7 -1
- package/dist/controls/form-picker-register.js +1 -5
- package/dist/controls/form-picker-register.js.map +1 -1
- package/dist/controls/icon.d.ts +1 -1
- package/dist/controls/icon.js +1 -1
- package/dist/controls/icon.js.map +1 -1
- package/dist/controls/link.d.ts +11 -4
- package/dist/controls/link.js +120 -202
- package/dist/controls/link.js.map +1 -1
- package/dist/controls/location.js +3 -8
- package/dist/controls/location.js.map +1 -1
- package/dist/controls/media.d.ts +0 -14
- package/dist/controls/media.js +52 -95
- package/dist/controls/media.js.map +1 -1
- package/dist/controls/reference-services.d.ts +23 -2
- package/dist/controls/reference.d.ts +2 -2
- package/dist/controls/reference.js +79 -28
- package/dist/controls/reference.js.map +1 -1
- package/dist/controls/rich-text/article-shell.d.ts +145 -0
- package/dist/controls/rich-text/article-shell.js +514 -0
- package/dist/controls/rich-text/article-shell.js.map +1 -0
- package/dist/controls/rich-text/author.d.ts +25 -0
- package/dist/controls/rich-text/author.js +107 -0
- package/dist/controls/rich-text/author.js.map +1 -0
- package/dist/controls/rich-text/blocks.d.ts +19 -2
- package/dist/controls/rich-text/blocks.js +275 -638
- package/dist/controls/rich-text/blocks.js.map +1 -1
- package/dist/controls/rich-text/button.d.ts +23 -0
- package/dist/controls/rich-text/button.js +81 -0
- package/dist/controls/rich-text/button.js.map +1 -0
- package/dist/controls/rich-text/catalog.d.ts +2 -5
- package/dist/controls/rich-text/catalog.js +121 -7
- package/dist/controls/rich-text/catalog.js.map +1 -1
- package/dist/controls/rich-text/checklist.d.ts +24 -0
- package/dist/controls/rich-text/checklist.js +115 -0
- package/dist/controls/rich-text/checklist.js.map +1 -0
- package/dist/controls/rich-text/codec.d.ts +15 -1
- package/dist/controls/rich-text/codec.js +11 -1
- package/dist/controls/rich-text/codec.js.map +1 -1
- package/dist/controls/rich-text/columns.d.ts +41 -0
- package/dist/controls/rich-text/columns.js +181 -0
- package/dist/controls/rich-text/columns.js.map +1 -0
- package/dist/controls/rich-text/control.js +1 -0
- package/dist/controls/rich-text/control.js.map +1 -1
- package/dist/controls/rich-text/dom.d.ts +84 -4
- package/dist/controls/rich-text/dom.js +171 -10
- package/dist/controls/rich-text/dom.js.map +1 -1
- package/dist/controls/rich-text/editor.d.ts +4 -2
- package/dist/controls/rich-text/editor.js +199 -26
- package/dist/controls/rich-text/editor.js.map +1 -1
- package/dist/controls/rich-text/embed.d.ts +28 -0
- package/dist/controls/rich-text/embed.js +210 -0
- package/dist/controls/rich-text/embed.js.map +1 -0
- package/dist/controls/rich-text/footnote.d.ts +41 -0
- package/dist/controls/rich-text/footnote.js +185 -0
- package/dist/controls/rich-text/footnote.js.map +1 -0
- package/dist/controls/rich-text/gallery.d.ts +28 -0
- package/dist/controls/rich-text/gallery.js +167 -0
- package/dist/controls/rich-text/gallery.js.map +1 -0
- package/dist/controls/rich-text/link-form.d.ts +13 -1
- package/dist/controls/rich-text/link-form.js +41 -7
- package/dist/controls/rich-text/link-form.js.map +1 -1
- package/dist/controls/rich-text/ops.js +6 -0
- package/dist/controls/rich-text/ops.js.map +1 -1
- package/dist/controls/rich-text/related.d.ts +49 -0
- package/dist/controls/rich-text/related.js +233 -0
- package/dist/controls/rich-text/related.js.map +1 -0
- package/dist/controls/rich-text/toc.d.ts +47 -0
- package/dist/controls/rich-text/toc.js +160 -0
- package/dist/controls/rich-text/toc.js.map +1 -0
- package/dist/controls/slot-register.d.ts +6 -1
- package/dist/controls/slot-register.js +1 -4
- package/dist/controls/slot-register.js.map +1 -1
- package/dist/core/core.d.ts +11 -3
- package/dist/core/core.js +24 -6
- package/dist/core/core.js.map +1 -1
- package/dist/core/facets.d.ts +16 -0
- package/dist/core/facets.js +17 -1
- package/dist/core/facets.js.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/ledgers/exclusions.v1.js +141 -0
- package/dist/ledgers/exclusions.v1.js.map +1 -0
- package/dist/library/coverage.d.ts +13 -1
- package/dist/library/coverage.js +12 -1
- package/dist/library/coverage.js.map +1 -1
- package/dist/library/kit-catalog.json +156 -153
- package/dist/schema/types.d.ts +17 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/testing/coverage-extras.d.ts +8 -0
- package/dist/testing/coverage-extras.js +44 -0
- package/dist/testing/coverage-extras.js.map +1 -0
- package/dist/testing/exclusions.d.ts +30 -0
- package/dist/testing/exclusions.js +13 -0
- package/dist/testing/exclusions.js.map +1 -0
- package/dist/ui/autocomplete.d.ts +29 -1
- package/dist/ui/autocomplete.js +102 -47
- package/dist/ui/autocomplete.js.map +1 -1
- package/dist/ui/button.js +3 -5
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/index.js +2 -2
- package/dist/ui/overlay.d.ts +4 -1
- package/dist/ui/overlay.js +2 -1
- package/dist/ui/overlay.js.map +1 -1
- package/dist/ui/toast.js +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { ArticleEmpty, ArticlePanelBlock } from "./article-shell.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { IconPhoto } from "@tabler/icons-react";
|
|
5
|
+
//#region src/controls/rich-text/gallery.tsx
|
|
6
|
+
/**
|
|
7
|
+
* RICH TEXT · Gallery (kit-richtext-ui-spec.md §2): V2's article block, stored as the PT block object
|
|
8
|
+
* `{ _type: "gallery", layout: "grid" | "carousel", images: { _key, image, caption? }[] }`; each image is the rich-text
|
|
9
|
+
* image's own shape (src/alt + media id), so media rewrites and alt audits reach it.
|
|
10
|
+
*
|
|
11
|
+
* form V2 article.ts:75-114: Images (a grid list, 2–24, each a required image + caption ≤160), Layout
|
|
12
|
+
* (Grid | Carousel, segmented). A list of objects → the floating properties panel (D54); a fresh insert opens it.
|
|
13
|
+
* preview V2 article-blocks.tsx:451-480: Grid in 2 columns for 2 or 4 images, else 3, square crops; Carousel a
|
|
14
|
+
* 72%-wide scroll-snap row of 4:3 crops with "N images · carousel"; "Add images" when there are none.
|
|
15
|
+
*/
|
|
16
|
+
var GALLERY_LABEL = "Gallery";
|
|
17
|
+
var galleryDefault = () => ({
|
|
18
|
+
layout: "grid",
|
|
19
|
+
images: []
|
|
20
|
+
});
|
|
21
|
+
var GALLERY_SCHEMA = {
|
|
22
|
+
name: "gallery",
|
|
23
|
+
title: GALLERY_LABEL,
|
|
24
|
+
kind: "object",
|
|
25
|
+
icon: "photo",
|
|
26
|
+
fields: [{
|
|
27
|
+
name: "images",
|
|
28
|
+
title: "Images",
|
|
29
|
+
type: "array",
|
|
30
|
+
itemNoun: "image",
|
|
31
|
+
arrayLayout: "grid",
|
|
32
|
+
validation: {
|
|
33
|
+
min: 2,
|
|
34
|
+
max: 24
|
|
35
|
+
},
|
|
36
|
+
of: [{
|
|
37
|
+
name: "photo",
|
|
38
|
+
title: "Image",
|
|
39
|
+
type: "object",
|
|
40
|
+
icon: "photo",
|
|
41
|
+
preview: {
|
|
42
|
+
title: "caption",
|
|
43
|
+
media: "image",
|
|
44
|
+
fallback: "Untitled image"
|
|
45
|
+
},
|
|
46
|
+
fields: [{
|
|
47
|
+
name: "image",
|
|
48
|
+
title: "Image",
|
|
49
|
+
type: "image",
|
|
50
|
+
validation: { required: true }
|
|
51
|
+
}, {
|
|
52
|
+
name: "caption",
|
|
53
|
+
title: "Caption",
|
|
54
|
+
type: "string",
|
|
55
|
+
validation: { max: 160 }
|
|
56
|
+
}]
|
|
57
|
+
}]
|
|
58
|
+
}, {
|
|
59
|
+
name: "layout",
|
|
60
|
+
title: "Layout",
|
|
61
|
+
type: "select",
|
|
62
|
+
layout: "segmented",
|
|
63
|
+
initialValue: "grid",
|
|
64
|
+
options: [{
|
|
65
|
+
value: "grid",
|
|
66
|
+
label: "Grid"
|
|
67
|
+
}, {
|
|
68
|
+
value: "carousel",
|
|
69
|
+
label: "Carousel"
|
|
70
|
+
}]
|
|
71
|
+
}]
|
|
72
|
+
};
|
|
73
|
+
function GalleryPreview({ m, onAdd, ro }) {
|
|
74
|
+
const imgs = m.images.filter((x) => x.image?.url);
|
|
75
|
+
if (!imgs.length) return /* @__PURE__ */ jsx(ArticleEmpty, {
|
|
76
|
+
icon: /* @__PURE__ */ jsx(IconPhoto, { stroke: 1.5 }),
|
|
77
|
+
title: GALLERY_LABEL,
|
|
78
|
+
action: "Add images",
|
|
79
|
+
onAction: onAdd,
|
|
80
|
+
ro
|
|
81
|
+
});
|
|
82
|
+
if (m.layout === "carousel") return /* @__PURE__ */ jsxs("figure", {
|
|
83
|
+
className: "m-0",
|
|
84
|
+
"data-field": "images",
|
|
85
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
86
|
+
className: "m-0 flex snap-x gap-2 overflow-x-auto p-0 pb-1",
|
|
87
|
+
"aria-label": GALLERY_LABEL,
|
|
88
|
+
children: imgs.map((x) => /* @__PURE__ */ jsxs("li", {
|
|
89
|
+
className: "w-[72%] shrink-0 snap-start list-none",
|
|
90
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
91
|
+
src: x.image.url,
|
|
92
|
+
alt: x.image.alt ?? "",
|
|
93
|
+
draggable: false,
|
|
94
|
+
className: "block aspect-[4/3] w-full rounded-kit-lg border border-kit-line object-cover"
|
|
95
|
+
}), x.caption ? /* @__PURE__ */ jsx("p", {
|
|
96
|
+
className: "mt-1 truncate text-kit-xs text-kit-fg-3",
|
|
97
|
+
children: x.caption
|
|
98
|
+
}) : null]
|
|
99
|
+
}, x._key))
|
|
100
|
+
}), /* @__PURE__ */ jsxs("figcaption", {
|
|
101
|
+
className: "mt-1 text-kit-2xs text-kit-fg-3",
|
|
102
|
+
children: [
|
|
103
|
+
imgs.length,
|
|
104
|
+
" ",
|
|
105
|
+
imgs.length === 1 ? "image" : "images",
|
|
106
|
+
" · carousel"
|
|
107
|
+
]
|
|
108
|
+
})]
|
|
109
|
+
});
|
|
110
|
+
return /* @__PURE__ */ jsx("figure", {
|
|
111
|
+
className: "m-0",
|
|
112
|
+
"data-field": "images",
|
|
113
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
114
|
+
className: cn("m-0 grid gap-2 p-0", imgs.length === 2 || imgs.length === 4 ? "grid-cols-2" : "grid-cols-3"),
|
|
115
|
+
"aria-label": GALLERY_LABEL,
|
|
116
|
+
children: imgs.map((x) => /* @__PURE__ */ jsxs("li", {
|
|
117
|
+
className: "list-none",
|
|
118
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
119
|
+
src: x.image.url,
|
|
120
|
+
alt: x.image.alt ?? "",
|
|
121
|
+
draggable: false,
|
|
122
|
+
className: "block aspect-square w-full rounded-kit-lg border border-kit-line object-cover"
|
|
123
|
+
}), x.caption ? /* @__PURE__ */ jsx("p", {
|
|
124
|
+
className: "mt-1 truncate text-kit-xs text-kit-fg-3",
|
|
125
|
+
children: x.caption
|
|
126
|
+
}) : null]
|
|
127
|
+
}, x._key))
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
var asImages = (v) => Array.isArray(v) ? v.map((x, i) => {
|
|
132
|
+
const o = x ?? {};
|
|
133
|
+
return {
|
|
134
|
+
_key: typeof o._key === "string" ? o._key : `g${i}`,
|
|
135
|
+
image: o.image ?? null,
|
|
136
|
+
caption: typeof o.caption === "string" ? o.caption : ""
|
|
137
|
+
};
|
|
138
|
+
}) : [];
|
|
139
|
+
function GalleryBlock({ m, set, el, ctx, autoFocus }) {
|
|
140
|
+
return /* @__PURE__ */ jsx(ArticlePanelBlock, {
|
|
141
|
+
label: GALLERY_LABEL,
|
|
142
|
+
kind: "gallery",
|
|
143
|
+
schema: GALLERY_SCHEMA,
|
|
144
|
+
m,
|
|
145
|
+
set,
|
|
146
|
+
el,
|
|
147
|
+
ctx,
|
|
148
|
+
autoFocus,
|
|
149
|
+
toForm: (x) => ({
|
|
150
|
+
images: x.images,
|
|
151
|
+
layout: x.layout
|
|
152
|
+
}),
|
|
153
|
+
fromForm: (v) => ({
|
|
154
|
+
images: asImages(v.images),
|
|
155
|
+
layout: v.layout === "carousel" ? "carousel" : "grid"
|
|
156
|
+
}),
|
|
157
|
+
children: (open, ro) => /* @__PURE__ */ jsx(GalleryPreview, {
|
|
158
|
+
m,
|
|
159
|
+
onAdd: () => open("images"),
|
|
160
|
+
ro
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { GALLERY_LABEL, GALLERY_SCHEMA, GalleryBlock, GalleryPreview, galleryDefault };
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=gallery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gallery.js","names":[],"sources":["../../../src/controls/rich-text/gallery.tsx"],"sourcesContent":["/**\n * RICH TEXT · Gallery (kit-richtext-ui-spec.md §2): V2's article block, stored as the PT block object\n * `{ _type: \"gallery\", layout: \"grid\" | \"carousel\", images: { _key, image, caption? }[] }`; each image is the rich-text\n * image's own shape (src/alt + media id), so media rewrites and alt audits reach it.\n *\n * form V2 article.ts:75-114: Images (a grid list, 2–24, each a required image + caption ≤160), Layout\n * (Grid | Carousel, segmented). A list of objects → the floating properties panel (D54); a fresh insert opens it.\n * preview V2 article-blocks.tsx:451-480: Grid in 2 columns for 2 or 4 images, else 3, square crops; Carousel a\n * 72%-wide scroll-snap row of 4:3 crops with \"N images · carousel\"; \"Add images\" when there are none.\n */\nimport { IconPhoto } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport type { ImageValue } from \"../media\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport interface GalleryImage {\n _key: string;\n image: ImageValue | null;\n caption: string;\n}\nexport interface GalleryModel {\n layout: \"grid\" | \"carousel\";\n images: GalleryImage[];\n}\nexport const GALLERY_LABEL = \"Gallery\";\nexport const galleryDefault = (): GalleryModel => ({ layout: \"grid\", images: [] });\n\nexport const GALLERY_SCHEMA: TypeSchema = {\n name: \"gallery\",\n title: GALLERY_LABEL,\n kind: \"object\",\n icon: \"photo\",\n fields: [\n {\n name: \"images\",\n title: \"Images\",\n type: \"array\",\n itemNoun: \"image\",\n arrayLayout: \"grid\",\n validation: { min: 2, max: 24 },\n of: [\n {\n name: \"photo\",\n title: \"Image\",\n type: \"object\",\n icon: \"photo\",\n preview: { title: \"caption\", media: \"image\", fallback: \"Untitled image\" },\n fields: [\n { name: \"image\", title: \"Image\", type: \"image\", validation: { required: true } },\n { name: \"caption\", title: \"Caption\", type: \"string\", validation: { max: 160 } },\n ],\n },\n ],\n },\n {\n name: \"layout\",\n title: \"Layout\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"grid\",\n options: [\n { value: \"grid\", label: \"Grid\" },\n { value: \"carousel\", label: \"Carousel\" },\n ],\n },\n ],\n};\n\nexport function GalleryPreview({ m, onAdd, ro }: { m: GalleryModel; onAdd: () => void; ro: boolean }) {\n const imgs = m.images.filter((x) => x.image?.url);\n if (!imgs.length) return <ArticleEmpty icon={<IconPhoto stroke={1.5} />} title={GALLERY_LABEL} action=\"Add images\" onAction={onAdd} ro={ro} />;\n if (m.layout === \"carousel\")\n return (\n <figure className=\"m-0\" data-field=\"images\">\n <ul className=\"m-0 flex snap-x gap-2 overflow-x-auto p-0 pb-1\" aria-label={GALLERY_LABEL}>\n {imgs.map((x) => (\n <li key={x._key} className=\"w-[72%] shrink-0 snap-start list-none\">\n <img src={x.image!.url} alt={x.image!.alt ?? \"\"} draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-lg border border-kit-line object-cover\" />\n {x.caption ? <p className=\"mt-1 truncate text-kit-xs text-kit-fg-3\">{x.caption}</p> : null}\n </li>\n ))}\n </ul>\n <figcaption className=\"mt-1 text-kit-2xs text-kit-fg-3\">\n {imgs.length} {imgs.length === 1 ? \"image\" : \"images\"} · carousel\n </figcaption>\n </figure>\n );\n return (\n <figure className=\"m-0\" data-field=\"images\">\n <ul className={cn(\"m-0 grid gap-2 p-0\", imgs.length === 2 || imgs.length === 4 ? \"grid-cols-2\" : \"grid-cols-3\")} aria-label={GALLERY_LABEL}>\n {imgs.map((x) => (\n <li key={x._key} className=\"list-none\">\n <img src={x.image!.url} alt={x.image!.alt ?? \"\"} draggable={false} className=\"block aspect-square w-full rounded-kit-lg border border-kit-line object-cover\" />\n {x.caption ? <p className=\"mt-1 truncate text-kit-xs text-kit-fg-3\">{x.caption}</p> : null}\n </li>\n ))}\n </ul>\n </figure>\n );\n}\n\nconst asImages = (v: unknown): GalleryImage[] =>\n Array.isArray(v)\n ? v.map((x, i) => {\n const o = (x ?? {}) as Record<string, unknown>;\n return { _key: typeof o._key === \"string\" ? o._key : `g${i}`, image: (o.image as ImageValue | null) ?? null, caption: typeof o.caption === \"string\" ? o.caption : \"\" };\n })\n : [];\n\nexport function GalleryBlock({\n m, set, el, ctx, autoFocus,\n}: {\n m: GalleryModel;\n set: (fn: (m: GalleryModel) => GalleryModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n}) {\n return (\n <ArticlePanelBlock<GalleryModel>\n label={GALLERY_LABEL}\n kind=\"gallery\"\n schema={GALLERY_SCHEMA}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n toForm={(x) => ({ images: x.images, layout: x.layout })}\n fromForm={(v) => ({ images: asImages(v.images), layout: v.layout === \"carousel\" ? \"carousel\" : \"grid\" })}\n >\n {(open, ro) => <GalleryPreview m={m} onAdd={() => open(\"images\")} ro={ro} />}\n </ArticlePanelBlock>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
import { type LinkRel, type LinkValue } from "../link";
|
|
1
2
|
export interface TextLink {
|
|
2
3
|
href: string;
|
|
3
4
|
newTab?: boolean;
|
|
5
|
+
/** absent = follow (V2 writes no rel for it) */
|
|
6
|
+
rel?: Exclude<LinkRel, "follow">;
|
|
4
7
|
}
|
|
8
|
+
/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */
|
|
9
|
+
export declare function textLinkOf(a: HTMLAnchorElement): TextLink;
|
|
10
|
+
/** Writes a link onto its anchor: href, target and rel (none for follow). */
|
|
11
|
+
export declare function applyTextLink(a: HTMLAnchorElement, l: TextLink): void;
|
|
12
|
+
export declare const toLinkValue: (l: TextLink) => LinkValue;
|
|
13
|
+
/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */
|
|
14
|
+
export declare const fromLinkValue: (v: LinkValue, allowNewTab: boolean) => TextLink;
|
|
5
15
|
/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
|
|
6
16
|
export declare function linkProblem(href: string): string | null;
|
|
7
|
-
export declare function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, }: {
|
|
17
|
+
export declare function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty, }: {
|
|
8
18
|
initial: TextLink;
|
|
9
19
|
allowNewTab: boolean;
|
|
10
20
|
onSave: (v: TextLink) => void;
|
|
11
21
|
onCancel: () => void;
|
|
12
22
|
onDetach?: () => void;
|
|
23
|
+
/** something the host draws beside the form changed (a button's label), so Apply has something to save */
|
|
24
|
+
dirty?: boolean;
|
|
13
25
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -7,11 +7,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
import { IconLink } from "@tabler/icons-react";
|
|
8
8
|
//#region src/controls/rich-text/link-form.tsx
|
|
9
9
|
/**
|
|
10
|
-
* RICH TEXT ·
|
|
11
|
-
* LinkPopoverBody:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* RICH TEXT · a link inside the text. A text link and a button are edited in V2's own link panel (controls/link.tsx
|
|
11
|
+
* LinkPopoverBody: Type, the page/section/address, new tab and Rel; RTE:1696) and stored as production's link mark:
|
|
12
|
+
* the resolved href, target and rel (`toLinkValue` / `fromLinkValue`). `LinkForm` below is the address-only form an
|
|
13
|
+
* image block links with.
|
|
14
14
|
*/
|
|
15
|
+
var RELS = [
|
|
16
|
+
"nofollow",
|
|
17
|
+
"sponsored",
|
|
18
|
+
"ugc"
|
|
19
|
+
];
|
|
20
|
+
/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */
|
|
21
|
+
function textLinkOf(a) {
|
|
22
|
+
const tokens = (a.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
|
|
23
|
+
const rel = RELS.find((r) => tokens.includes(r));
|
|
24
|
+
return {
|
|
25
|
+
href: a.getAttribute("href") ?? "",
|
|
26
|
+
newTab: a.getAttribute("target") === "_blank",
|
|
27
|
+
...rel ? { rel } : {}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Writes a link onto its anchor: href, target and rel (none for follow). */
|
|
31
|
+
function applyTextLink(a, l) {
|
|
32
|
+
a.setAttribute("href", l.href);
|
|
33
|
+
if (l.newTab) a.setAttribute("target", "_blank");
|
|
34
|
+
else a.removeAttribute("target");
|
|
35
|
+
if (l.rel) a.setAttribute("rel", l.rel);
|
|
36
|
+
else a.removeAttribute("rel");
|
|
37
|
+
}
|
|
38
|
+
var toLinkValue = (l) => ({
|
|
39
|
+
href: l.href,
|
|
40
|
+
newTab: !!l.newTab,
|
|
41
|
+
rel: l.rel ?? "follow"
|
|
42
|
+
});
|
|
43
|
+
/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */
|
|
44
|
+
var fromLinkValue = (v, allowNewTab) => ({
|
|
45
|
+
href: v.href,
|
|
46
|
+
newTab: allowNewTab && !!v.newTab,
|
|
47
|
+
...v.rel && v.rel !== "follow" ? { rel: v.rel } : {}
|
|
48
|
+
});
|
|
15
49
|
/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
|
|
16
50
|
function linkProblem(href) {
|
|
17
51
|
const h = href.trim();
|
|
@@ -21,7 +55,7 @@ function linkProblem(href) {
|
|
|
21
55
|
if (/^[^\s/]+\.[^\s]+$/.test(h)) return `Add https:// in front: https://${h}`;
|
|
22
56
|
return "Use a full address (https://…), a page path (/about) or an anchor (#section).";
|
|
23
57
|
}
|
|
24
|
-
function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach }) {
|
|
58
|
+
function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty = false }) {
|
|
25
59
|
const [href, setHref] = React.useState(initial.href);
|
|
26
60
|
const [newTab, setNewTab] = React.useState(!!initial.newTab);
|
|
27
61
|
const [problem, setProblem] = React.useState(null);
|
|
@@ -29,7 +63,7 @@ function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach }) {
|
|
|
29
63
|
const tabId = React.useId();
|
|
30
64
|
const errId = React.useId();
|
|
31
65
|
const start = initial.href.trim();
|
|
32
|
-
const changed = href.trim() !== start || allowNewTab && newTab !== !!initial.newTab;
|
|
66
|
+
const changed = dirty || href.trim() !== start || allowNewTab && newTab !== !!initial.newTab;
|
|
33
67
|
const ok = !linkProblem(href);
|
|
34
68
|
return /* @__PURE__ */ jsxs("form", {
|
|
35
69
|
className: "flex flex-col gap-3",
|
|
@@ -142,6 +176,6 @@ function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach }) {
|
|
|
142
176
|
});
|
|
143
177
|
}
|
|
144
178
|
//#endregion
|
|
145
|
-
export { LinkForm, linkProblem };
|
|
179
|
+
export { LinkForm, applyTextLink, fromLinkValue, linkProblem, textLinkOf, toLinkValue };
|
|
146
180
|
|
|
147
181
|
//# sourceMappingURL=link-form.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT ·
|
|
1
|
+
{"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT · a link inside the text. A text link and a button are edited in V2's own link panel (controls/link.tsx\n * LinkPopoverBody: Type, the page/section/address, new tab and Rel; RTE:1696) and stored as production's link mark:\n * the resolved href, target and rel (`toLinkValue` / `fromLinkValue`). `LinkForm` below is the address-only form an\n * image block links with.\n */\nimport * as React from \"react\";\nimport { IconLink } from \"@tabler/icons-react\";\nimport { Button } from \"../../ui/button\";\nimport { Switch } from \"../../ui/controls\";\nimport { Input } from \"../../ui/input\";\nimport { LinkLine, normLink, type LinkRel, type LinkValue } from \"../link\";\n\nexport interface TextLink {\n href: string;\n newTab?: boolean;\n /** absent = follow (V2 writes no rel for it) */\n rel?: Exclude<LinkRel, \"follow\">;\n}\n\nconst RELS: Exclude<LinkRel, \"follow\">[] = [\"nofollow\", \"sponsored\", \"ugc\"];\n/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */\nexport function textLinkOf(a: HTMLAnchorElement): TextLink {\n const tokens = (a.getAttribute(\"rel\") ?? \"\").toLowerCase().split(/\\s+/);\n const rel = RELS.find((r) => tokens.includes(r));\n return { href: a.getAttribute(\"href\") ?? \"\", newTab: a.getAttribute(\"target\") === \"_blank\", ...(rel ? { rel } : {}) };\n}\n/** Writes a link onto its anchor: href, target and rel (none for follow). */\nexport function applyTextLink(a: HTMLAnchorElement, l: TextLink) {\n a.setAttribute(\"href\", l.href);\n if (l.newTab) a.setAttribute(\"target\", \"_blank\");\n else a.removeAttribute(\"target\");\n if (l.rel) a.setAttribute(\"rel\", l.rel);\n else a.removeAttribute(\"rel\");\n}\nexport const toLinkValue = (l: TextLink): LinkValue => ({ href: l.href, newTab: !!l.newTab, rel: l.rel ?? \"follow\" });\n/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */\nexport const fromLinkValue = (v: LinkValue, allowNewTab: boolean): TextLink => ({\n href: v.href,\n newTab: allowNewTab && !!v.newTab,\n ...(v.rel && v.rel !== \"follow\" ? { rel: v.rel } : {}),\n});\n\n/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */\nexport function linkProblem(href: string): string | null {\n const h = href.trim();\n if (!h) return \"Add an address first.\";\n if (/^(https?:\\/\\/[^\\s]+|mailto:[^\\s]+|tel:[+\\d][\\d\\s()-]*|\\/(?!\\/)[^\\s]*|#[^\\s]+)$/i.test(h)) return null;\n if (/^[a-z][a-z0-9+.-]*:/i.test(h)) return \"Use an address that starts with https://, mailto: or tel:.\";\n if (/^[^\\s/]+\\.[^\\s]+$/.test(h)) return `Add https:// in front: https://${h}`;\n return \"Use a full address (https://…), a page path (/about) or an anchor (#section).\";\n}\n\nexport function LinkForm({\n initial,\n allowNewTab,\n onSave,\n onCancel,\n onDetach,\n dirty = false,\n}: {\n initial: TextLink;\n allowNewTab: boolean;\n onSave: (v: TextLink) => void;\n onCancel: () => void;\n onDetach?: () => void;\n /** something the host draws beside the form changed (a button's label), so Apply has something to save */\n dirty?: boolean;\n}) {\n const [href, setHref] = React.useState(initial.href);\n const [newTab, setNewTab] = React.useState(!!initial.newTab);\n const [problem, setProblem] = React.useState<string | null>(null);\n const urlId = React.useId();\n const tabId = React.useId();\n const errId = React.useId();\n const start = initial.href.trim();\n // V2 LinkPopoverBody (controls/link.tsx): Apply only when something changed and the link can be saved\n const changed = dirty || href.trim() !== start || (allowNewTab && newTab !== !!initial.newTab);\n const ok = !linkProblem(href);\n return (\n <form\n className=\"flex flex-col gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n const p = linkProblem(href);\n setProblem(p);\n if (!p) onSave({ href: href.trim(), newTab: allowNewTab && newTab });\n }}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n <span className=\"text-kit-sm font-medium text-kit-fg\">Link</span>\n {start ? (\n <span className=\"flex min-w-0 text-kit-fg-2\">\n <LinkLine v={normLink({ href: start, newTab: !!initial.newTab })} />\n </span>\n ) : (\n <span className=\"text-kit-xs text-kit-fg-3\">No link yet</span>\n )}\n </div>\n <div\n className=\"flex flex-col gap-1.5\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node) && href.trim()) setProblem(linkProblem(href));\n }}\n >\n <label htmlFor={urlId} className=\"text-kit-xs text-kit-fg-2\">\n Link to\n </label>\n <Input\n id={urlId}\n autoFocus\n size=\"sm\"\n value={href}\n onChange={(e) => {\n setHref(e.target.value);\n if (problem) setProblem(null);\n }}\n onKeyDown={(e) => {\n // Apply is disabled while the address cannot be saved, so Enter says why instead of doing nothing\n if (e.key === \"Enter\" && !ok) {\n e.preventDefault();\n setProblem(linkProblem(href));\n }\n }}\n placeholder=\"https://… or /page\"\n leading={<IconLink stroke={1.5} />}\n aria-invalid={problem ? true : undefined}\n aria-describedby={problem ? errId : undefined}\n />\n {problem ? (\n <p id={errId} role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {problem}\n </p>\n ) : null}\n </div>\n {allowNewTab ? (\n <label htmlFor={tabId} className=\"flex min-h-7 items-center justify-between gap-3\">\n <span className=\"text-kit-xs text-kit-fg-2\">Open in new tab</span>\n <Switch id={tabId} size=\"sm\" checked={newTab} onCheckedChange={setNewTab} />\n </label>\n ) : null}\n <div className=\"flex items-center gap-2 border-t border-kit-line pt-2.5\">\n {onDetach ? (\n <Button type=\"button\" size=\"xs\" variant=\"danger-ghost\" onClick={onDetach}>\n Remove link\n </Button>\n ) : null}\n <span className=\"flex-1\" />\n <Button type=\"button\" size=\"xs\" variant=\"ghost\" onClick={onCancel}>\n Cancel\n </Button>\n <Button type=\"submit\" size=\"xs\" variant=\"primary\" disabled={!changed || !ok}>\n Apply\n </Button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
@@ -152,6 +152,12 @@ function insertBasic(root, id) {
|
|
|
152
152
|
case "toggle":
|
|
153
153
|
case "video":
|
|
154
154
|
case "file":
|
|
155
|
+
case "gallery":
|
|
156
|
+
case "embed":
|
|
157
|
+
case "toc":
|
|
158
|
+
case "columns":
|
|
159
|
+
case "related":
|
|
160
|
+
case "author":
|
|
155
161
|
insertIsland(root, id);
|
|
156
162
|
return;
|
|
157
163
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ops.js","names":[],"sources":["../../../src/controls/rich-text/ops.ts"],"sourcesContent":["/**\n * RICH TEXT · pure DOM block operations on the editor surface (V2 HEAD controls/rich-text/ops.ts; no React, no\n * emitting). The editor wraps each one, then serializes and emits the new value.\n */\nimport { closestBlock, cloneModel, createIsland, defaultModel, esc, placeCaret, textBeforeCaret, topBlock, type IslandEl, type IslandKind, type IslandModels } from \"./dom\";\nimport type { RichTextFeature } from \"./codec\";\nimport type { BasicId } from \"./catalog\";\n\nconst emptyP = () => {\n const p = document.createElement(\"p\");\n p.innerHTML = \"<br>\";\n return p;\n};\nconst isWritable = (el: Element | null): el is HTMLElement => !!el && !el.hasAttribute(\"data-rt-island\") && el.tagName !== \"HR\";\n\n/** The writable line after a block, created when the block is last or followed by another rich block. */\nexport function ensureLineAfter(b: Element): HTMLElement {\n const next = b.nextElementSibling;\n if (isWritable(next)) return next;\n const p = emptyP();\n b.after(p);\n return p;\n}\n\nexport function duplicateBlock(b: HTMLElement): HTMLElement {\n let c: HTMLElement;\n if (b.hasAttribute(\"data-rt-island\")) {\n const k = b.getAttribute(\"data-rt-island\") as IslandKind;\n c = createIsland(k, cloneModel(k, (b as IslandEl).__rt as never));\n } else {\n c = b.cloneNode(true) as HTMLElement;\n c.removeAttribute(\"data-hint\");\n }\n b.after(c);\n return c;\n}\n\n/** Moves a block one step, keeping the caret where it was. Returns false at the edge. */\nexport function moveBlock(b: HTMLElement, dir: -1 | 1): boolean {\n const sib = dir < 0 ? b.previousElementSibling : b.nextElementSibling;\n if (!sib) return false;\n const s = window.getSelection();\n // ranges are live (a move collapses them), so keep the raw caret position\n const at = s && s.rangeCount ? { n: s.getRangeAt(0).startContainer, o: s.getRangeAt(0).startOffset } : null;\n const host = document.activeElement as HTMLElement | null;\n if (dir < 0) sib.before(b);\n else sib.after(b);\n if (at && s && b.contains(at.n)) {\n host?.focus({ preventScroll: true });\n const r = document.createRange();\n r.setStart(at.n, Math.min(at.o, at.n.nodeType === 3 ? (at.n as Text).length : at.n.childNodes.length));\n r.collapse(true);\n s.removeAllRanges();\n s.addRange(r);\n }\n return true;\n}\n\n/** Text / Heading / Quote / Code. Returns the new element, or null when nothing changed. */\nexport function turnInto(root: HTMLElement, b: HTMLElement | null, tag: string): HTMLElement | null {\n if (!b || b.parentNode !== root || b.hasAttribute(\"data-rt-island\") || b.tagName === \"HR\") return null;\n if (b.tagName.toLowerCase() === tag) return null;\n const inner =\n b.tagName === \"UL\" || b.tagName === \"OL\"\n ? Array.from(b.children).map((c) => c.innerHTML).join(\"<br>\")\n : b.tagName === \"PRE\"\n ? esc(b.textContent ?? \"\").replace(/\\n/g, \"<br>\")\n : b.innerHTML;\n const n = document.createElement(tag);\n n.innerHTML = tag === \"pre\" ? esc((b.textContent ?? \"\").replace(//g, \"\")) || \"<br>\" : inner || \"<br>\";\n b.replaceWith(n);\n root.focus({ preventScroll: true });\n placeCaret(n);\n return n;\n}\n\n/** An empty paragraph at the caret to build a block from (reuses the current one when it is empty). */\nexport function takeEmptyBlock(root: HTMLElement): HTMLElement {\n const s = window.getSelection();\n const anchor = s && s.rangeCount && root.contains(s.anchorNode) ? s.anchorNode : null;\n const li = anchor ? closestBlock(root, anchor) : null;\n let b = topBlock(root, anchor);\n if (li && li.tagName === \"LI\" && !li.textContent?.trim()) {\n const list = li.parentElement!;\n li.remove();\n if (!list.children.length) {\n const p = emptyP();\n list.replaceWith(p);\n b = p;\n } else b = list;\n }\n root.focus({ preventScroll: true });\n if (b && b.tagName === \"P\" && !b.textContent?.replace(//g, \"\").trim() && !b.querySelector(\"img\")) {\n b.innerHTML = \"<br>\";\n placeCaret(b);\n return b;\n }\n // an empty heading, quote or code line (e.g. \"/\" typed on one) is reused too, so no empty block is left behind\n if (b && /^(H[1-6]|BLOCKQUOTE|PRE)$/.test(b.tagName) && !b.textContent?.replace(//g, \"\").trim()) {\n const p = emptyP();\n b.replaceWith(p);\n placeCaret(p);\n return p;\n }\n const p = emptyP();\n if (b) b.after(p);\n else root.append(p);\n placeCaret(p);\n return p;\n}\n\nexport function insertIsland<K extends IslandKind>(root: HTMLElement, kind: K, model: IslandModels[K] = defaultModel(kind)): IslandEl {\n const target = takeEmptyBlock(root);\n const isl = createIsland(kind, model);\n isl.__rtFocus = true;\n target.before(isl);\n // keep the document tidy: reuse the line that already follows instead of leaving an empty one\n const next = target.nextElementSibling;\n if (isWritable(next)) {\n target.remove();\n placeCaret(next, false);\n } else placeCaret(target);\n return isl;\n}\n\n/** Inserts a basic block at the caret. */\nexport function insertBasic(root: HTMLElement, id: BasicId) {\n switch (id) {\n case \"p\":\n takeEmptyBlock(root);\n return;\n case \"h1\": case \"h2\": case \"h3\": case \"h4\": case \"h5\": case \"h6\":\n turnInto(root, takeEmptyBlock(root), id);\n return;\n case \"quote\":\n turnInto(root, takeEmptyBlock(root), \"blockquote\");\n return;\n case \"code\":\n turnInto(root, takeEmptyBlock(root), \"pre\");\n return;\n case \"ul\": case \"ol\": {\n const t = takeEmptyBlock(root);\n const l = document.createElement(id);\n l.innerHTML = \"<li><br></li>\";\n t.replaceWith(l);\n placeCaret(l.firstElementChild!);\n return;\n }\n case \"hr\": {\n const t = takeEmptyBlock(root);\n t.before(document.createElement(\"hr\"));\n placeCaret(t);\n return;\n }\n case \"image\": case \"table\": case \"pullquote\": case \"takeaways\": case \"callout\": case \"toggle\": case \"video\": case \"file\":\n insertIsland(root, id);\n return;\n }\n}\n\n/**\n * \"# \", \"> \", \"- \", \"1. \" and \"---\" at the start of a paragraph, each only when the field accepts what it makes.\n * Returns true when it converted.\n */\nexport function liveMarkdown(root: HTMLElement, data: string | null, allows: (f: RichTextFeature) => boolean = () => true): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const blk = topBlock(root, s.anchorNode);\n if (!blk || blk.tagName !== \"P\") return false;\n const before = textBeforeCaret(blk)?.replace(/ /g, \" \");\n if (before == null) return false;\n if (data === \"-\" && before === \"---\" && (blk.textContent ?? \"\").replace(/ /g, \" \") === \"---\") {\n if (!allows(\"hr\")) return false;\n const p = emptyP();\n blk.replaceWith(document.createElement(\"hr\"), p);\n placeCaret(p);\n return true;\n }\n if (data !== \" \") return false;\n const h = before.match(/^(#{1,6}) $/);\n let tag = h ? `h${h[1].length}` : before === \"> \" ? \"blockquote\" : null;\n let list = before === \"- \" || before === \"* \" ? \"ul\" : /^1[.)] $/.test(before) ? \"ol\" : null;\n if (tag && !allows(tag as RichTextFeature)) tag = null;\n if (list && !allows(list as RichTextFeature)) list = null;\n if (!tag && !list) return false;\n const r = document.createRange();\n r.selectNodeContents(blk);\n r.setEnd(s.anchorNode!, s.anchorOffset);\n r.deleteContents();\n const html = blk.textContent || blk.querySelector(\"img\") ? blk.innerHTML : \"<br>\";\n let target: HTMLElement;\n if (tag) {\n target = document.createElement(tag);\n target.innerHTML = html;\n blk.replaceWith(target);\n } else {\n const l = document.createElement(list!);\n target = document.createElement(\"li\");\n target.innerHTML = html;\n l.append(target);\n blk.replaceWith(l);\n }\n placeCaret(target, false);\n return true;\n}\n\n/** `text` typed between backticks becomes inline code. */\nexport function liveInlineCode(): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const r = s.getRangeAt(0);\n if (r.startContainer.nodeType !== 3 || r.startContainer.parentElement?.closest(\"code,pre\")) return false;\n const node = r.startContainer as Text;\n const m = node.data.slice(0, r.startOffset).match(/`([^`\\n]+)`$/);\n if (!m) return false;\n const del = document.createRange();\n del.setStart(node, r.startOffset - m[0].length);\n del.setEnd(node, r.startOffset);\n del.deleteContents();\n const code = document.createElement(\"code\");\n code.textContent = m[1];\n del.insertNode(code);\n const next = code.nextSibling;\n if (next && next.nodeType === 3 && next.textContent) {\n const c = document.createRange();\n c.setStart(next, 0);\n c.collapse(true);\n s.removeAllRanges();\n s.addRange(c);\n } else {\n const after = document.createTextNode(\"\");\n code.after(after);\n placeCaret(after);\n }\n return true;\n}\n\n/* ── What a field accepts, applied to HTML that arrives from outside (paste, Markdown) ── */\nconst MARK_FEATURE: Record<string, RichTextFeature> = {\n STRONG: \"bold\", B: \"bold\", EM: \"italic\", I: \"italic\", U: \"underline\", S: \"strike\", STRIKE: \"strike\", DEL: \"strike\", CODE: \"code\", A: \"link\",\n};\nconst BLOCK_FEATURE: Record<string, RichTextFeature> = {\n H1: \"h1\", H2: \"h2\", H3: \"h3\", H4: \"h4\", H5: \"h5\", H6: \"h6\", BLOCKQUOTE: \"blockquote\", UL: \"ul\", OL: \"ol\", PRE: \"pre\", HR: \"hr\", FIGURE: \"image\", IMG: \"image\",\n};\nconst FEATURE_WORD: Partial<Record<RichTextFeature, string>> = {\n image: \"images\", hr: \"dividers\", pre: \"code blocks\", ul: \"bulleted lists\", ol: \"numbered lists\", blockquote: \"quotes\", link: \"links\",\n};\n\n/**\n * HTML with every element the field does not accept turned into what it does: a heading / quote / code block becomes a\n * paragraph, a list becomes one paragraph per item, a mark or link is unwrapped to its text, a divider or image is\n * removed. `dropped` names what was taken out entirely (said to the author, never silent).\n */\nexport function restrictHtml(html: string, allows: (f: RichTextFeature) => boolean): { html: string; dropped: string[] } {\n if (!html) return { html, dropped: [] };\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, \"text/html\");\n const dropped = new Set<string>();\n const walk = (el: Element) => {\n for (const ch of Array.from(el.children)) {\n walk(ch);\n const mark = MARK_FEATURE[ch.tagName];\n const block = BLOCK_FEATURE[ch.tagName];\n if (mark && !allows(mark)) ch.replaceWith(...Array.from(ch.childNodes));\n else if (block && !allows(block)) {\n if (block === \"hr\" || block === \"image\") {\n dropped.add(FEATURE_WORD[block]!);\n ch.remove();\n } else if (block === \"ul\" || block === \"ol\") {\n const ps = Array.from(ch.children).map((li) => {\n const p = doc.createElement(\"p\");\n p.innerHTML = li.innerHTML;\n return p;\n });\n ch.replaceWith(...ps);\n } else {\n const p = doc.createElement(\"p\");\n p.innerHTML = block === \"pre\" ? esc(ch.textContent ?? \"\").replace(/\\n/g, \"<br>\") : ch.innerHTML;\n ch.replaceWith(p);\n }\n }\n }\n };\n walk(doc.body);\n return { html: doc.body.innerHTML, dropped: [...dropped] };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ops.js","names":[],"sources":["../../../src/controls/rich-text/ops.ts"],"sourcesContent":["/**\n * RICH TEXT · pure DOM block operations on the editor surface (V2 HEAD controls/rich-text/ops.ts; no React, no\n * emitting). The editor wraps each one, then serializes and emits the new value.\n */\nimport { closestBlock, cloneModel, createIsland, defaultModel, esc, placeCaret, textBeforeCaret, topBlock, type IslandEl, type IslandKind, type IslandModels } from \"./dom\";\nimport type { RichTextFeature } from \"./codec\";\nimport type { BasicId } from \"./catalog\";\n\nconst emptyP = () => {\n const p = document.createElement(\"p\");\n p.innerHTML = \"<br>\";\n return p;\n};\nconst isWritable = (el: Element | null): el is HTMLElement => !!el && !el.hasAttribute(\"data-rt-island\") && el.tagName !== \"HR\";\n\n/** The writable line after a block, created when the block is last or followed by another rich block. */\nexport function ensureLineAfter(b: Element): HTMLElement {\n const next = b.nextElementSibling;\n if (isWritable(next)) return next;\n const p = emptyP();\n b.after(p);\n return p;\n}\n\nexport function duplicateBlock(b: HTMLElement): HTMLElement {\n let c: HTMLElement;\n if (b.hasAttribute(\"data-rt-island\")) {\n const k = b.getAttribute(\"data-rt-island\") as IslandKind;\n c = createIsland(k, cloneModel(k, (b as IslandEl).__rt as never));\n } else {\n c = b.cloneNode(true) as HTMLElement;\n c.removeAttribute(\"data-hint\");\n }\n b.after(c);\n return c;\n}\n\n/** Moves a block one step, keeping the caret where it was. Returns false at the edge. */\nexport function moveBlock(b: HTMLElement, dir: -1 | 1): boolean {\n const sib = dir < 0 ? b.previousElementSibling : b.nextElementSibling;\n if (!sib) return false;\n const s = window.getSelection();\n // ranges are live (a move collapses them), so keep the raw caret position\n const at = s && s.rangeCount ? { n: s.getRangeAt(0).startContainer, o: s.getRangeAt(0).startOffset } : null;\n const host = document.activeElement as HTMLElement | null;\n if (dir < 0) sib.before(b);\n else sib.after(b);\n if (at && s && b.contains(at.n)) {\n host?.focus({ preventScroll: true });\n const r = document.createRange();\n r.setStart(at.n, Math.min(at.o, at.n.nodeType === 3 ? (at.n as Text).length : at.n.childNodes.length));\n r.collapse(true);\n s.removeAllRanges();\n s.addRange(r);\n }\n return true;\n}\n\n/** Text / Heading / Quote / Code. Returns the new element, or null when nothing changed. */\nexport function turnInto(root: HTMLElement, b: HTMLElement | null, tag: string): HTMLElement | null {\n if (!b || b.parentNode !== root || b.hasAttribute(\"data-rt-island\") || b.tagName === \"HR\") return null;\n if (b.tagName.toLowerCase() === tag) return null;\n const inner =\n b.tagName === \"UL\" || b.tagName === \"OL\"\n ? Array.from(b.children).map((c) => c.innerHTML).join(\"<br>\")\n : b.tagName === \"PRE\"\n ? esc(b.textContent ?? \"\").replace(/\\n/g, \"<br>\")\n : b.innerHTML;\n const n = document.createElement(tag);\n n.innerHTML = tag === \"pre\" ? esc((b.textContent ?? \"\").replace(//g, \"\")) || \"<br>\" : inner || \"<br>\";\n b.replaceWith(n);\n root.focus({ preventScroll: true });\n placeCaret(n);\n return n;\n}\n\n/** An empty paragraph at the caret to build a block from (reuses the current one when it is empty). */\nexport function takeEmptyBlock(root: HTMLElement): HTMLElement {\n const s = window.getSelection();\n const anchor = s && s.rangeCount && root.contains(s.anchorNode) ? s.anchorNode : null;\n const li = anchor ? closestBlock(root, anchor) : null;\n let b = topBlock(root, anchor);\n if (li && li.tagName === \"LI\" && !li.textContent?.trim()) {\n const list = li.parentElement!;\n li.remove();\n if (!list.children.length) {\n const p = emptyP();\n list.replaceWith(p);\n b = p;\n } else b = list;\n }\n root.focus({ preventScroll: true });\n if (b && b.tagName === \"P\" && !b.textContent?.replace(//g, \"\").trim() && !b.querySelector(\"img\")) {\n b.innerHTML = \"<br>\";\n placeCaret(b);\n return b;\n }\n // an empty heading, quote or code line (e.g. \"/\" typed on one) is reused too, so no empty block is left behind\n if (b && /^(H[1-6]|BLOCKQUOTE|PRE)$/.test(b.tagName) && !b.textContent?.replace(//g, \"\").trim()) {\n const p = emptyP();\n b.replaceWith(p);\n placeCaret(p);\n return p;\n }\n const p = emptyP();\n if (b) b.after(p);\n else root.append(p);\n placeCaret(p);\n return p;\n}\n\nexport function insertIsland<K extends IslandKind>(root: HTMLElement, kind: K, model: IslandModels[K] = defaultModel(kind)): IslandEl {\n const target = takeEmptyBlock(root);\n const isl = createIsland(kind, model);\n isl.__rtFocus = true;\n target.before(isl);\n // keep the document tidy: reuse the line that already follows instead of leaving an empty one\n const next = target.nextElementSibling;\n if (isWritable(next)) {\n target.remove();\n placeCaret(next, false);\n } else placeCaret(target);\n return isl;\n}\n\n/** Inserts a basic block at the caret. */\nexport function insertBasic(root: HTMLElement, id: BasicId) {\n switch (id) {\n case \"p\":\n takeEmptyBlock(root);\n return;\n case \"h1\": case \"h2\": case \"h3\": case \"h4\": case \"h5\": case \"h6\":\n turnInto(root, takeEmptyBlock(root), id);\n return;\n case \"quote\":\n turnInto(root, takeEmptyBlock(root), \"blockquote\");\n return;\n case \"code\":\n turnInto(root, takeEmptyBlock(root), \"pre\");\n return;\n case \"ul\": case \"ol\": {\n const t = takeEmptyBlock(root);\n const l = document.createElement(id);\n l.innerHTML = \"<li><br></li>\";\n t.replaceWith(l);\n placeCaret(l.firstElementChild!);\n return;\n }\n case \"hr\": {\n const t = takeEmptyBlock(root);\n t.before(document.createElement(\"hr\"));\n placeCaret(t);\n return;\n }\n case \"image\": case \"table\": case \"pullquote\": case \"takeaways\": case \"callout\": case \"toggle\": case \"video\": case \"file\":\n case \"gallery\": case \"embed\": case \"toc\": case \"columns\": case \"related\": case \"author\":\n insertIsland(root, id);\n return;\n }\n}\n\n/**\n * \"# \", \"> \", \"- \", \"1. \" and \"---\" at the start of a paragraph, each only when the field accepts what it makes.\n * Returns true when it converted.\n */\nexport function liveMarkdown(root: HTMLElement, data: string | null, allows: (f: RichTextFeature) => boolean = () => true): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const blk = topBlock(root, s.anchorNode);\n if (!blk || blk.tagName !== \"P\") return false;\n const before = textBeforeCaret(blk)?.replace(/ /g, \" \");\n if (before == null) return false;\n if (data === \"-\" && before === \"---\" && (blk.textContent ?? \"\").replace(/ /g, \" \") === \"---\") {\n if (!allows(\"hr\")) return false;\n const p = emptyP();\n blk.replaceWith(document.createElement(\"hr\"), p);\n placeCaret(p);\n return true;\n }\n if (data !== \" \") return false;\n const h = before.match(/^(#{1,6}) $/);\n let tag = h ? `h${h[1].length}` : before === \"> \" ? \"blockquote\" : null;\n let list = before === \"- \" || before === \"* \" ? \"ul\" : /^1[.)] $/.test(before) ? \"ol\" : null;\n if (tag && !allows(tag as RichTextFeature)) tag = null;\n if (list && !allows(list as RichTextFeature)) list = null;\n if (!tag && !list) return false;\n const r = document.createRange();\n r.selectNodeContents(blk);\n r.setEnd(s.anchorNode!, s.anchorOffset);\n r.deleteContents();\n const html = blk.textContent || blk.querySelector(\"img\") ? blk.innerHTML : \"<br>\";\n let target: HTMLElement;\n if (tag) {\n target = document.createElement(tag);\n target.innerHTML = html;\n blk.replaceWith(target);\n } else {\n const l = document.createElement(list!);\n target = document.createElement(\"li\");\n target.innerHTML = html;\n l.append(target);\n blk.replaceWith(l);\n }\n placeCaret(target, false);\n return true;\n}\n\n/** `text` typed between backticks becomes inline code. */\nexport function liveInlineCode(): boolean {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !s.isCollapsed) return false;\n const r = s.getRangeAt(0);\n if (r.startContainer.nodeType !== 3 || r.startContainer.parentElement?.closest(\"code,pre\")) return false;\n const node = r.startContainer as Text;\n const m = node.data.slice(0, r.startOffset).match(/`([^`\\n]+)`$/);\n if (!m) return false;\n const del = document.createRange();\n del.setStart(node, r.startOffset - m[0].length);\n del.setEnd(node, r.startOffset);\n del.deleteContents();\n const code = document.createElement(\"code\");\n code.textContent = m[1];\n del.insertNode(code);\n const next = code.nextSibling;\n if (next && next.nodeType === 3 && next.textContent) {\n const c = document.createRange();\n c.setStart(next, 0);\n c.collapse(true);\n s.removeAllRanges();\n s.addRange(c);\n } else {\n const after = document.createTextNode(\"\");\n code.after(after);\n placeCaret(after);\n }\n return true;\n}\n\n/* ── What a field accepts, applied to HTML that arrives from outside (paste, Markdown) ── */\nconst MARK_FEATURE: Record<string, RichTextFeature> = {\n STRONG: \"bold\", B: \"bold\", EM: \"italic\", I: \"italic\", U: \"underline\", S: \"strike\", STRIKE: \"strike\", DEL: \"strike\", CODE: \"code\", A: \"link\",\n};\nconst BLOCK_FEATURE: Record<string, RichTextFeature> = {\n H1: \"h1\", H2: \"h2\", H3: \"h3\", H4: \"h4\", H5: \"h5\", H6: \"h6\", BLOCKQUOTE: \"blockquote\", UL: \"ul\", OL: \"ol\", PRE: \"pre\", HR: \"hr\", FIGURE: \"image\", IMG: \"image\",\n};\nconst FEATURE_WORD: Partial<Record<RichTextFeature, string>> = {\n image: \"images\", hr: \"dividers\", pre: \"code blocks\", ul: \"bulleted lists\", ol: \"numbered lists\", blockquote: \"quotes\", link: \"links\",\n};\n\n/**\n * HTML with every element the field does not accept turned into what it does: a heading / quote / code block becomes a\n * paragraph, a list becomes one paragraph per item, a mark or link is unwrapped to its text, a divider or image is\n * removed. `dropped` names what was taken out entirely (said to the author, never silent).\n */\nexport function restrictHtml(html: string, allows: (f: RichTextFeature) => boolean): { html: string; dropped: string[] } {\n if (!html) return { html, dropped: [] };\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, \"text/html\");\n const dropped = new Set<string>();\n const walk = (el: Element) => {\n for (const ch of Array.from(el.children)) {\n walk(ch);\n const mark = MARK_FEATURE[ch.tagName];\n const block = BLOCK_FEATURE[ch.tagName];\n if (mark && !allows(mark)) ch.replaceWith(...Array.from(ch.childNodes));\n else if (block && !allows(block)) {\n if (block === \"hr\" || block === \"image\") {\n dropped.add(FEATURE_WORD[block]!);\n ch.remove();\n } else if (block === \"ul\" || block === \"ol\") {\n const ps = Array.from(ch.children).map((li) => {\n const p = doc.createElement(\"p\");\n p.innerHTML = li.innerHTML;\n return p;\n });\n ch.replaceWith(...ps);\n } else {\n const p = doc.createElement(\"p\");\n p.innerHTML = block === \"pre\" ? esc(ch.textContent ?? \"\").replace(/\\n/g, \"<br>\") : ch.innerHTML;\n ch.replaceWith(p);\n }\n }\n }\n };\n walk(doc.body);\n return { html: doc.body.innerHTML, dropped: [...dropped] };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type KitRichTextRefRequest } from "../../context";
|
|
2
|
+
import type { TypeSchema } from "../../schema/types";
|
|
3
|
+
import type { IslandCtx } from "./blocks";
|
|
4
|
+
import type { IslandEl } from "./dom";
|
|
5
|
+
export type RelatedMode = "topic" | "newest" | "manual";
|
|
6
|
+
export interface RelatedModel {
|
|
7
|
+
title: string;
|
|
8
|
+
mode: RelatedMode;
|
|
9
|
+
count: number;
|
|
10
|
+
/** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */
|
|
11
|
+
posts: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const RELATED_LABEL = "Related posts";
|
|
14
|
+
/** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */
|
|
15
|
+
export declare const relatedDefault: (topic?: boolean) => RelatedModel;
|
|
16
|
+
/** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */
|
|
17
|
+
export declare const NO_TOPIC_REASON = "Same topic needs a tags or category field on this collection.";
|
|
18
|
+
/**
|
|
19
|
+
* The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. With `topic` false,
|
|
20
|
+
* Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).
|
|
21
|
+
*/
|
|
22
|
+
export declare const relatedSchema: (to: string[], mode: RelatedMode, topic?: boolean, pick?: {
|
|
23
|
+
noun?: string;
|
|
24
|
+
excludeId?: string;
|
|
25
|
+
}) => TypeSchema;
|
|
26
|
+
/** What the preview asks the host for: the picked ids, or the automatic mode's query. */
|
|
27
|
+
export declare function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null;
|
|
28
|
+
export declare function RelatedPreview({ m, onPick, ro, collection, excludeId }: {
|
|
29
|
+
m: RelatedModel;
|
|
30
|
+
onPick: () => void;
|
|
31
|
+
ro: boolean;
|
|
32
|
+
collection?: string;
|
|
33
|
+
excludeId?: string;
|
|
34
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, topic, noun, }: {
|
|
36
|
+
m: RelatedModel;
|
|
37
|
+
set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;
|
|
38
|
+
el: IslandEl;
|
|
39
|
+
ctx: IslandCtx;
|
|
40
|
+
autoFocus: boolean;
|
|
41
|
+
/** the posts collection (the field's config) */
|
|
42
|
+
collection: string;
|
|
43
|
+
/** the entry being edited, left out of the automatic modes */
|
|
44
|
+
excludeId?: string;
|
|
45
|
+
/** false: the collection has no topic field, so Same topic is not offered */
|
|
46
|
+
topic?: boolean;
|
|
47
|
+
/** what one post is called (the collection's singular); absent → the picker's own noun */
|
|
48
|
+
noun?: string;
|
|
49
|
+
}): import("react/jsx-runtime").JSX.Element;
|