@unlayer/react-elements 0.1.17 → 0.1.19
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/README.md +32 -1
- package/dist/index.cjs +122 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +127 -2
- package/dist/index.d.ts +127 -2
- package/dist/index.js +121 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -331,7 +331,38 @@ The document shell matches the mode: `<Email>` gets the XHTML transitional docty
|
|
|
331
331
|
- `title` — document `<title>`
|
|
332
332
|
- `fonts` — web-font stylesheet URLs to load via `<link>` tags, e.g. `[{ url: "https://fonts.googleapis.com/css2?family=Inter" }]`
|
|
333
333
|
|
|
334
|
-
If your app owns the document shell (an existing page template, an ESP template, an iframe `srcdoc`), use `renderToHtmlParts` instead — it returns `{ head, body }`
|
|
334
|
+
If your app owns the document shell (an existing page template, an ESP template, an iframe `srcdoc`), use `renderToHtmlParts` instead — it returns `{ head, body }` plus the granular pieces (`css`, `js`, `tags`) for pipelines that need them separately, e.g. CSS inlining. This mirrors the editor's [chunk parameters](https://docs.unlayer.com/builder/latest/export-html#chunk-parameters) with two deliberate differences: `tags` is extra (components can contribute head tags, and reassembling from `css` + `js` alone would drop them), and there is no `fonts` chunk (Elements has no font registry — pass font stylesheet URLs via `renderToHtml`'s `fonts` option instead).
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const { head, body, css, js, tags } = renderToHtmlParts(<Email>...</Email>);
|
|
338
|
+
// head = css/js/tags pre-assembled; css = raw stylesheet (no <style> wrapper)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Custom Tools
|
|
342
|
+
|
|
343
|
+
**A custom tool is the custom component you create.** We keep the editor's term — Custom Tools — because it's the same API across the Builder and Elements, and that shared vocabulary is where the power is: the same definition you pass to the editor's [`unlayer.registerTool`](https://docs.unlayer.com/builder/tools/custom) renders from code too. Elements uses the `renderer.exporters` and `renderer.head` halves of the definition; the editor-only parts (`label`, `icon`, `options`, `Viewer`, `validator`) are accepted and ignored:
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import { registerTool, Email, Row, Column, renderToHtml, renderToJson } from '@unlayer/react-elements';
|
|
347
|
+
import { countdownTool } from './countdown-tool'; // the same object you register in the editor
|
|
348
|
+
|
|
349
|
+
const Countdown = registerTool(countdownTool); // same term, same config as the editor
|
|
350
|
+
|
|
351
|
+
const tree = (
|
|
352
|
+
<Email>
|
|
353
|
+
<Row><Column>
|
|
354
|
+
<Countdown endTime="2026-08-01T00:00:00Z" digitColor="#e11d48" />
|
|
355
|
+
</Column></Row>
|
|
356
|
+
</Email>
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
renderToHtml(tree); // renders via YOUR exporters — identical output to editor exports
|
|
360
|
+
renderToJson(tree); // emits { type: "custom", slug: "countdown", values } — opens in the Builder as the real tool
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
- Props are the tool's `values` (flat), merged over the definition's defaults; `values={{...}}` works as an escape hatch.
|
|
364
|
+
- Modes fall back like the editor: `document` uses the `web` exporter; an email-only tool renders via its `email` exporter everywhere.
|
|
365
|
+
- **Sanitization:** when your config provides `toSafeHtml`, the tool's HTML output is passed through it (matching editor exports). The default config has no sanitizer — configure one if tool values can contain untrusted content.
|
|
335
366
|
|
|
336
367
|
## UnlayerProvider
|
|
337
368
|
|
package/dist/index.cjs
CHANGED
|
@@ -620,16 +620,6 @@ function nextHtmlId(config, prefix) {
|
|
|
620
620
|
ids[prefix] = (ids[prefix] || 0) + 1;
|
|
621
621
|
return `${prefix}_${ids[prefix]}`;
|
|
622
622
|
}
|
|
623
|
-
function ensureMeta(values, type, index = 0) {
|
|
624
|
-
return {
|
|
625
|
-
...values,
|
|
626
|
-
_meta: {
|
|
627
|
-
htmlID: `u_content_${type.toLowerCase()}_${index + 1}`,
|
|
628
|
-
htmlClassNames: `u_content_${type.toLowerCase()}`,
|
|
629
|
-
...values._meta || {}
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
623
|
function renderComponent(config) {
|
|
634
624
|
const { type, values, className, style, args = [], innerHTML, _config, exporter, metaContext } = config;
|
|
635
625
|
try {
|
|
@@ -682,6 +672,7 @@ function createItemComponent(config) {
|
|
|
682
672
|
style,
|
|
683
673
|
// Internal props
|
|
684
674
|
index = 0,
|
|
675
|
+
_metaHtmlId,
|
|
685
676
|
colIndex = 0,
|
|
686
677
|
cells = [],
|
|
687
678
|
bodyValues = {},
|
|
@@ -699,11 +690,15 @@ function createItemComponent(config) {
|
|
|
699
690
|
...restProps
|
|
700
691
|
});
|
|
701
692
|
const finalValues = mergeValues(config.defaultValues, mappedValues);
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
693
|
+
const metaName = config.metaName ?? config.name.toLowerCase();
|
|
694
|
+
const valuesWithMeta = {
|
|
695
|
+
...finalValues,
|
|
696
|
+
_meta: {
|
|
697
|
+
htmlID: _metaHtmlId ?? `u_content_${metaName}_${index + 1}`,
|
|
698
|
+
htmlClassNames: `u_content_${metaName}`,
|
|
699
|
+
...finalValues._meta || {}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
707
702
|
const safeBodyValues = {
|
|
708
703
|
contentWidth: "500px",
|
|
709
704
|
...bodyValues
|
|
@@ -752,7 +747,11 @@ function createItemComponent(config) {
|
|
|
752
747
|
ItemComponent[UNLAYER_CONFIG_KEY] = {
|
|
753
748
|
name: config.name,
|
|
754
749
|
defaultValues: config.defaultValues,
|
|
755
|
-
propMapper: config.propMapper
|
|
750
|
+
propMapper: config.propMapper,
|
|
751
|
+
contentType: config.contentType,
|
|
752
|
+
slug: config.slug,
|
|
753
|
+
metaName: config.metaName,
|
|
754
|
+
head: config.head
|
|
756
755
|
};
|
|
757
756
|
return ItemComponent;
|
|
758
757
|
}
|
|
@@ -1374,9 +1373,12 @@ var Column = (props) => {
|
|
|
1374
1373
|
if (typeof child.type === "function") {
|
|
1375
1374
|
const ComponentType = child.type;
|
|
1376
1375
|
const renderFn = ComponentType[UNLAYER_RENDER_KEY] || ComponentType;
|
|
1376
|
+
const componentName = ComponentType?.[UNLAYER_CONFIG_KEY]?.metaName ?? (ComponentType?.displayName || ComponentType?.name || "component").toLowerCase();
|
|
1377
|
+
const contentHtmlId = nextHtmlId(_config, `u_content_${componentName}`);
|
|
1377
1378
|
const rendered = renderFn({
|
|
1378
1379
|
...child.props,
|
|
1379
1380
|
_config,
|
|
1381
|
+
_metaHtmlId: contentHtmlId,
|
|
1380
1382
|
colIndex: index,
|
|
1381
1383
|
cells,
|
|
1382
1384
|
bodyValues,
|
|
@@ -1385,15 +1387,13 @@ var Column = (props) => {
|
|
|
1385
1387
|
});
|
|
1386
1388
|
if (rendered && typeof rendered === "object" && rendered.props && rendered.props.dangerouslySetInnerHTML) {
|
|
1387
1389
|
const componentHTML = rendered.props.dangerouslySetInnerHTML.__html;
|
|
1388
|
-
const componentType = child.type;
|
|
1389
|
-
const componentName = (componentType?.displayName || componentType?.name || "component").toLowerCase();
|
|
1390
1390
|
const childProps = child.props;
|
|
1391
1391
|
const rawContainerPadding = childProps.containerPadding ?? childProps.values?.containerPadding ?? DEFAULT_CONTAINER_PADDING2;
|
|
1392
1392
|
const containerPadding = typeof rawContainerPadding === "number" ? `${rawContainerPadding}px` : rawContainerPadding;
|
|
1393
1393
|
const contentValues = {
|
|
1394
1394
|
containerPadding,
|
|
1395
1395
|
_meta: {
|
|
1396
|
-
htmlID:
|
|
1396
|
+
htmlID: contentHtmlId,
|
|
1397
1397
|
htmlClassNames: `u_content_${componentName}`
|
|
1398
1398
|
}
|
|
1399
1399
|
};
|
|
@@ -1564,7 +1564,7 @@ UnlayerProvider.displayName = "UnlayerProvider";
|
|
|
1564
1564
|
function useUnlayerConfig() {
|
|
1565
1565
|
return React.useContext(getUnlayerContext());
|
|
1566
1566
|
}
|
|
1567
|
-
function
|
|
1567
|
+
function ensureMeta(values, type, index = 0) {
|
|
1568
1568
|
return {
|
|
1569
1569
|
...values,
|
|
1570
1570
|
_meta: {
|
|
@@ -1645,17 +1645,17 @@ function walkItem(element, bodyValues, displayMode, headConfig, counters, styles
|
|
|
1645
1645
|
const { children, ...restProps } = element.props;
|
|
1646
1646
|
const mappedValues = propMapper({ children, ...restProps });
|
|
1647
1647
|
const finalValues = mergeValues(defaultValues, mappedValues);
|
|
1648
|
-
const contentType = name.toLowerCase();
|
|
1648
|
+
const contentType = config.metaName ?? name.toLowerCase();
|
|
1649
1649
|
const count = nextCounter(counters, `u_content_${contentType}`);
|
|
1650
|
-
const valuesWithMeta =
|
|
1651
|
-
const head = exporters.heads[name];
|
|
1650
|
+
const valuesWithMeta = ensureMeta(finalValues, contentType, count - 1);
|
|
1651
|
+
const head = config.head ?? exporters.heads[name];
|
|
1652
1652
|
callHead(head, valuesWithMeta, bodyValues, displayMode, headConfig, styles, scripts, tags);
|
|
1653
1653
|
}
|
|
1654
1654
|
function walkColumn(element, bodyValues, rowValues, displayMode, headConfig, counters, styles, scripts, tags) {
|
|
1655
1655
|
const semanticProps = extractSemanticProps(element.props);
|
|
1656
1656
|
const mapped = mapSemanticProps(semanticProps, COLUMN_DEFAULTS, "Column");
|
|
1657
1657
|
const count = nextCounter(counters, "u_column");
|
|
1658
|
-
const columnValues =
|
|
1658
|
+
const columnValues = ensureMeta(mergeValues(COLUMN_DEFAULTS, mapped), "column", count - 1);
|
|
1659
1659
|
const columnHead = exporters.heads["Column"];
|
|
1660
1660
|
callHead(columnHead, columnValues, bodyValues, displayMode, headConfig, styles, scripts, tags);
|
|
1661
1661
|
const children = collectChildren(element.props.children);
|
|
@@ -1667,7 +1667,7 @@ function walkRow(element, bodyValues, displayMode, headConfig, counters, styles,
|
|
|
1667
1667
|
const semanticProps = extractSemanticProps(element.props, ["layout"]);
|
|
1668
1668
|
const mapped = mapSemanticProps(semanticProps, ROW_DEFAULTS, "Row");
|
|
1669
1669
|
const count = nextCounter(counters, "u_row");
|
|
1670
|
-
const rowValues =
|
|
1670
|
+
const rowValues = ensureMeta(mergeValues(ROW_DEFAULTS, mapped), "row", count - 1);
|
|
1671
1671
|
const rowHead = exporters.heads["Row"];
|
|
1672
1672
|
callHead(rowHead, rowValues, bodyValues, displayMode, headConfig, styles, scripts, tags);
|
|
1673
1673
|
const children = collectChildren(element.props.children);
|
|
@@ -1690,7 +1690,7 @@ function extractHeadFromTree(element, options) {
|
|
|
1690
1690
|
const counters = {};
|
|
1691
1691
|
const semanticProps = extractSemanticProps(element.props);
|
|
1692
1692
|
const mapped = mapSemanticProps(semanticProps, BODY_DEFAULTS, "Body");
|
|
1693
|
-
const bodyValues =
|
|
1693
|
+
const bodyValues = ensureMeta(mergeValues(BODY_DEFAULTS, mapped), "body", 0);
|
|
1694
1694
|
const bodyHead = exporters.heads["Body"];
|
|
1695
1695
|
callHead(bodyHead, bodyValues, bodyValues, displayMode, headConfig, styles, scripts, tags);
|
|
1696
1696
|
const children = collectChildren(element.props.children);
|
|
@@ -1857,7 +1857,10 @@ function renderToHtmlParts(element, config) {
|
|
|
1857
1857
|
}
|
|
1858
1858
|
return {
|
|
1859
1859
|
head: headParts.join("\n"),
|
|
1860
|
-
body
|
|
1860
|
+
body,
|
|
1861
|
+
css,
|
|
1862
|
+
js,
|
|
1863
|
+
tags
|
|
1861
1864
|
};
|
|
1862
1865
|
}
|
|
1863
1866
|
var schemaVersion = exporters.schemaVersion ?? 24;
|
|
@@ -1963,10 +1966,11 @@ function processItem(element, counters, layout = {}) {
|
|
|
1963
1966
|
`[Unlayer] renderToJson: <${name2}> is not a recognized Unlayer item component. Only components created with createItemComponent are supported.`
|
|
1964
1967
|
);
|
|
1965
1968
|
}
|
|
1966
|
-
const { name, defaultValues, propMapper } = config;
|
|
1967
|
-
const contentType = toContentType(name);
|
|
1968
|
-
const
|
|
1969
|
-
const
|
|
1969
|
+
const { name, defaultValues, propMapper, slug } = config;
|
|
1970
|
+
const contentType = config.contentType ?? toContentType(name);
|
|
1971
|
+
const metaBase = `u_content_${config.metaName ?? contentType}`;
|
|
1972
|
+
const count = nextCounter2(counters, metaBase);
|
|
1973
|
+
const id = makeId(metaBase, count);
|
|
1970
1974
|
const { children, ...restProps } = element.props;
|
|
1971
1975
|
const mappedValues = propMapper({ children, ...restProps });
|
|
1972
1976
|
const finalValues = mergeValues(defaultValues, mappedValues);
|
|
@@ -1979,7 +1983,7 @@ function processItem(element, counters, layout = {}) {
|
|
|
1979
1983
|
...finalValues,
|
|
1980
1984
|
_meta: {
|
|
1981
1985
|
htmlID: id,
|
|
1982
|
-
htmlClassNames:
|
|
1986
|
+
htmlClassNames: metaBase,
|
|
1983
1987
|
...finalValues._meta || {}
|
|
1984
1988
|
},
|
|
1985
1989
|
selectable: true,
|
|
@@ -1996,7 +2000,7 @@ function processItem(element, counters, layout = {}) {
|
|
|
1996
2000
|
});
|
|
1997
2001
|
values.src = pinImageSrc(itemSrc, availableWidth);
|
|
1998
2002
|
}
|
|
1999
|
-
return { type: contentType, values };
|
|
2003
|
+
return slug ? { type: contentType, slug, values } : { type: contentType, values };
|
|
2000
2004
|
}
|
|
2001
2005
|
function processColumn(element, counters, layout = {}) {
|
|
2002
2006
|
const count = nextCounter2(counters, "u_column");
|
|
@@ -2140,6 +2144,89 @@ function renderToJson(element) {
|
|
|
2140
2144
|
};
|
|
2141
2145
|
}
|
|
2142
2146
|
|
|
2147
|
+
// src/utils/register-tool.tsx
|
|
2148
|
+
function initialValuesFromOptions(options) {
|
|
2149
|
+
const result = {};
|
|
2150
|
+
for (const group of Object.values(options ?? {})) {
|
|
2151
|
+
for (const [property, option] of Object.entries(group?.options ?? {})) {
|
|
2152
|
+
if (!option || option.enabled === false) continue;
|
|
2153
|
+
if (typeof option.defaultValue !== "undefined") {
|
|
2154
|
+
result[property] = option.defaultValue;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return result;
|
|
2159
|
+
}
|
|
2160
|
+
function withSanitizer(exporter) {
|
|
2161
|
+
return (values, ...args) => {
|
|
2162
|
+
const html = String(exporter(values, ...args) ?? "");
|
|
2163
|
+
const meta = args[args.length - 1];
|
|
2164
|
+
const toSafeHtml = meta?.exporterConfig?.toSafeHtml;
|
|
2165
|
+
return typeof toSafeHtml === "function" ? toSafeHtml(html) : html;
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
function withLinkNormalization(exporter, linkKeys) {
|
|
2169
|
+
if (linkKeys.length === 0) return exporter;
|
|
2170
|
+
return (values, ...args) => {
|
|
2171
|
+
const out = { ...values };
|
|
2172
|
+
for (const key of linkKeys) {
|
|
2173
|
+
const normalized = normalizeLinkValue(out[key]);
|
|
2174
|
+
if (normalized !== void 0) out[key] = normalized;
|
|
2175
|
+
}
|
|
2176
|
+
return exporter(out, ...args);
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
function linkWidgetKeys(options) {
|
|
2180
|
+
const keys = [];
|
|
2181
|
+
for (const group of Object.values(options ?? {})) {
|
|
2182
|
+
for (const [key, option] of Object.entries(group?.options ?? {})) {
|
|
2183
|
+
if (option && option.widget === "link") keys.push(key);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return keys;
|
|
2187
|
+
}
|
|
2188
|
+
function registerTool(tool) {
|
|
2189
|
+
if (!tool?.name || typeof tool.name !== "string") {
|
|
2190
|
+
throw new Error("[Unlayer] registerTool: `name` is required");
|
|
2191
|
+
}
|
|
2192
|
+
const exporters = tool.renderer?.exporters;
|
|
2193
|
+
if (!exporters || !exporters.web && !exporters.email) {
|
|
2194
|
+
throw new Error(
|
|
2195
|
+
`[Unlayer] registerTool("${tool.name}"): \`renderer.exporters\` needs at least a \`web\` or \`email\` exporter`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
const linkKeys = linkWidgetKeys(tool.options);
|
|
2199
|
+
const sanitized = {};
|
|
2200
|
+
for (const mode of ["web", "email", "document"]) {
|
|
2201
|
+
const exporter = exporters[mode];
|
|
2202
|
+
if (exporter) {
|
|
2203
|
+
sanitized[mode] = withSanitizer(withLinkNormalization(exporter, linkKeys));
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (!sanitized.web && sanitized.email) sanitized.web = sanitized.email;
|
|
2207
|
+
return createItemComponent({
|
|
2208
|
+
name: tool.name,
|
|
2209
|
+
displayName: tool.displayName ?? tool.name,
|
|
2210
|
+
// Option-group widget defaults seed the values; tool-level `values`
|
|
2211
|
+
// win — the editor's precedence.
|
|
2212
|
+
defaultValues: {
|
|
2213
|
+
...initialValuesFromOptions(tool.options),
|
|
2214
|
+
...tool.values ?? {}
|
|
2215
|
+
},
|
|
2216
|
+
// Flat props are the tool's values; `values` is the escape hatch.
|
|
2217
|
+
propMapper: ({ children: _children, values, ...rest }) => ({
|
|
2218
|
+
...values ?? {},
|
|
2219
|
+
...rest
|
|
2220
|
+
}),
|
|
2221
|
+
exporters: sanitized,
|
|
2222
|
+
contentType: "custom",
|
|
2223
|
+
slug: tool.name,
|
|
2224
|
+
metaName: `custom_${tool.name}`,
|
|
2225
|
+
head: tool.renderer.head
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
var registerElementsTool = registerTool;
|
|
2229
|
+
|
|
2143
2230
|
exports.Body = Body_default;
|
|
2144
2231
|
exports.Button = Button_default;
|
|
2145
2232
|
exports.Column = Column_default;
|
|
@@ -2160,6 +2247,8 @@ exports.Table = Table_default;
|
|
|
2160
2247
|
exports.UnlayerProvider = UnlayerProvider;
|
|
2161
2248
|
exports.Video = Video_default;
|
|
2162
2249
|
exports.htmlToTextJson = htmlToTextJson;
|
|
2250
|
+
exports.registerElementsTool = registerElementsTool;
|
|
2251
|
+
exports.registerTool = registerTool;
|
|
2163
2252
|
exports.renderRowToJson = renderRowToJson;
|
|
2164
2253
|
exports.renderToHtml = renderToHtml;
|
|
2165
2254
|
exports.renderToHtmlParts = renderToHtmlParts;
|