@unlayer/react-elements 0.1.17 → 0.1.18
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 +26 -0
- package/dist/index.cjs +118 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -1
- package/dist/index.d.ts +107 -1
- package/dist/index.js +117 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -333,6 +333,32 @@ The document shell matches the mode: `<Email>` gets the XHTML transitional docty
|
|
|
333
333
|
|
|
334
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 }` chunks for you to place yourself.
|
|
335
335
|
|
|
336
|
+
## Custom Tools
|
|
337
|
+
|
|
338
|
+
**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:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { registerTool, Email, Row, Column, renderToHtml, renderToJson } from '@unlayer/react-elements';
|
|
342
|
+
import { countdownTool } from './countdown-tool'; // the same object you register in the editor
|
|
343
|
+
|
|
344
|
+
const Countdown = registerTool(countdownTool); // same term, same config as the editor
|
|
345
|
+
|
|
346
|
+
const tree = (
|
|
347
|
+
<Email>
|
|
348
|
+
<Row><Column>
|
|
349
|
+
<Countdown endTime="2026-08-01T00:00:00Z" digitColor="#e11d48" />
|
|
350
|
+
</Column></Row>
|
|
351
|
+
</Email>
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
renderToHtml(tree); // renders via YOUR exporters — identical output to editor exports
|
|
355
|
+
renderToJson(tree); // emits { type: "custom", slug: "countdown", values } — opens in the Builder as the real tool
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- Props are the tool's `values` (flat), merged over the definition's defaults; `values={{...}}` works as an escape hatch.
|
|
359
|
+
- Modes fall back like the editor: `document` uses the `web` exporter; an email-only tool renders via its `email` exporter everywhere.
|
|
360
|
+
- **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.
|
|
361
|
+
|
|
336
362
|
## UnlayerProvider
|
|
337
363
|
|
|
338
364
|
Configure global settings like CDN base URL, merge tags, text direction, and rendering mode:
|
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);
|
|
@@ -1963,10 +1963,11 @@ function processItem(element, counters, layout = {}) {
|
|
|
1963
1963
|
`[Unlayer] renderToJson: <${name2}> is not a recognized Unlayer item component. Only components created with createItemComponent are supported.`
|
|
1964
1964
|
);
|
|
1965
1965
|
}
|
|
1966
|
-
const { name, defaultValues, propMapper } = config;
|
|
1967
|
-
const contentType = toContentType(name);
|
|
1968
|
-
const
|
|
1969
|
-
const
|
|
1966
|
+
const { name, defaultValues, propMapper, slug } = config;
|
|
1967
|
+
const contentType = config.contentType ?? toContentType(name);
|
|
1968
|
+
const metaBase = `u_content_${config.metaName ?? contentType}`;
|
|
1969
|
+
const count = nextCounter2(counters, metaBase);
|
|
1970
|
+
const id = makeId(metaBase, count);
|
|
1970
1971
|
const { children, ...restProps } = element.props;
|
|
1971
1972
|
const mappedValues = propMapper({ children, ...restProps });
|
|
1972
1973
|
const finalValues = mergeValues(defaultValues, mappedValues);
|
|
@@ -1979,7 +1980,7 @@ function processItem(element, counters, layout = {}) {
|
|
|
1979
1980
|
...finalValues,
|
|
1980
1981
|
_meta: {
|
|
1981
1982
|
htmlID: id,
|
|
1982
|
-
htmlClassNames:
|
|
1983
|
+
htmlClassNames: metaBase,
|
|
1983
1984
|
...finalValues._meta || {}
|
|
1984
1985
|
},
|
|
1985
1986
|
selectable: true,
|
|
@@ -1996,7 +1997,7 @@ function processItem(element, counters, layout = {}) {
|
|
|
1996
1997
|
});
|
|
1997
1998
|
values.src = pinImageSrc(itemSrc, availableWidth);
|
|
1998
1999
|
}
|
|
1999
|
-
return { type: contentType, values };
|
|
2000
|
+
return slug ? { type: contentType, slug, values } : { type: contentType, values };
|
|
2000
2001
|
}
|
|
2001
2002
|
function processColumn(element, counters, layout = {}) {
|
|
2002
2003
|
const count = nextCounter2(counters, "u_column");
|
|
@@ -2140,6 +2141,89 @@ function renderToJson(element) {
|
|
|
2140
2141
|
};
|
|
2141
2142
|
}
|
|
2142
2143
|
|
|
2144
|
+
// src/utils/register-tool.tsx
|
|
2145
|
+
function initialValuesFromOptions(options) {
|
|
2146
|
+
const result = {};
|
|
2147
|
+
for (const group of Object.values(options ?? {})) {
|
|
2148
|
+
for (const [property, option] of Object.entries(group?.options ?? {})) {
|
|
2149
|
+
if (!option || option.enabled === false) continue;
|
|
2150
|
+
if (typeof option.defaultValue !== "undefined") {
|
|
2151
|
+
result[property] = option.defaultValue;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return result;
|
|
2156
|
+
}
|
|
2157
|
+
function withSanitizer(exporter) {
|
|
2158
|
+
return (values, ...args) => {
|
|
2159
|
+
const html = String(exporter(values, ...args) ?? "");
|
|
2160
|
+
const meta = args[args.length - 1];
|
|
2161
|
+
const toSafeHtml = meta?.exporterConfig?.toSafeHtml;
|
|
2162
|
+
return typeof toSafeHtml === "function" ? toSafeHtml(html) : html;
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
function withLinkNormalization(exporter, linkKeys) {
|
|
2166
|
+
if (linkKeys.length === 0) return exporter;
|
|
2167
|
+
return (values, ...args) => {
|
|
2168
|
+
const out = { ...values };
|
|
2169
|
+
for (const key of linkKeys) {
|
|
2170
|
+
const normalized = normalizeLinkValue(out[key]);
|
|
2171
|
+
if (normalized !== void 0) out[key] = normalized;
|
|
2172
|
+
}
|
|
2173
|
+
return exporter(out, ...args);
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
function linkWidgetKeys(options) {
|
|
2177
|
+
const keys = [];
|
|
2178
|
+
for (const group of Object.values(options ?? {})) {
|
|
2179
|
+
for (const [key, option] of Object.entries(group?.options ?? {})) {
|
|
2180
|
+
if (option && option.widget === "link") keys.push(key);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return keys;
|
|
2184
|
+
}
|
|
2185
|
+
function registerTool(tool) {
|
|
2186
|
+
if (!tool?.name || typeof tool.name !== "string") {
|
|
2187
|
+
throw new Error("[Unlayer] registerTool: `name` is required");
|
|
2188
|
+
}
|
|
2189
|
+
const exporters = tool.renderer?.exporters;
|
|
2190
|
+
if (!exporters || !exporters.web && !exporters.email) {
|
|
2191
|
+
throw new Error(
|
|
2192
|
+
`[Unlayer] registerTool("${tool.name}"): \`renderer.exporters\` needs at least a \`web\` or \`email\` exporter`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
const linkKeys = linkWidgetKeys(tool.options);
|
|
2196
|
+
const sanitized = {};
|
|
2197
|
+
for (const mode of ["web", "email", "document"]) {
|
|
2198
|
+
const exporter = exporters[mode];
|
|
2199
|
+
if (exporter) {
|
|
2200
|
+
sanitized[mode] = withSanitizer(withLinkNormalization(exporter, linkKeys));
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (!sanitized.web && sanitized.email) sanitized.web = sanitized.email;
|
|
2204
|
+
return createItemComponent({
|
|
2205
|
+
name: tool.name,
|
|
2206
|
+
displayName: tool.displayName ?? tool.name,
|
|
2207
|
+
// Option-group widget defaults seed the values; tool-level `values`
|
|
2208
|
+
// win — the editor's precedence.
|
|
2209
|
+
defaultValues: {
|
|
2210
|
+
...initialValuesFromOptions(tool.options),
|
|
2211
|
+
...tool.values ?? {}
|
|
2212
|
+
},
|
|
2213
|
+
// Flat props are the tool's values; `values` is the escape hatch.
|
|
2214
|
+
propMapper: ({ children: _children, values, ...rest }) => ({
|
|
2215
|
+
...values ?? {},
|
|
2216
|
+
...rest
|
|
2217
|
+
}),
|
|
2218
|
+
exporters: sanitized,
|
|
2219
|
+
contentType: "custom",
|
|
2220
|
+
slug: tool.name,
|
|
2221
|
+
metaName: `custom_${tool.name}`,
|
|
2222
|
+
head: tool.renderer.head
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
var registerElementsTool = registerTool;
|
|
2226
|
+
|
|
2143
2227
|
exports.Body = Body_default;
|
|
2144
2228
|
exports.Button = Button_default;
|
|
2145
2229
|
exports.Column = Column_default;
|
|
@@ -2160,6 +2244,8 @@ exports.Table = Table_default;
|
|
|
2160
2244
|
exports.UnlayerProvider = UnlayerProvider;
|
|
2161
2245
|
exports.Video = Video_default;
|
|
2162
2246
|
exports.htmlToTextJson = htmlToTextJson;
|
|
2247
|
+
exports.registerElementsTool = registerElementsTool;
|
|
2248
|
+
exports.registerTool = registerTool;
|
|
2163
2249
|
exports.renderRowToJson = renderRowToJson;
|
|
2164
2250
|
exports.renderToHtml = renderToHtml;
|
|
2165
2251
|
exports.renderToHtmlParts = renderToHtmlParts;
|