@campxdev/pdfme 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/chunks/{index-dHRmLCnu.js → fontSizePxWidget-Drk8HKGH.js} +138 -9
- package/dist/cjs/chunks/fontSizeTransform-CQQ_O42f.js +37 -0
- package/dist/cjs/chunks/{helper-C2o2tpNj.js → helper-DGH62Z2s.js} +16959 -4
- package/dist/cjs/chunks/{index-qB7eb2BC.js → index-CoNR0xQU.js} +10 -2
- package/dist/cjs/chunks/{index-CXm3doOM.js → index-CsEKt088.js} +750 -535
- package/dist/cjs/chunks/{pluginRegistry-Ba3ANzzx.js → pluginRegistry-D2vr9MUy.js} +1 -1
- package/dist/cjs/common.js +11 -3
- package/dist/cjs/converter.js +1 -1
- package/dist/cjs/generator.js +3 -3
- package/dist/cjs/index.js +27 -16
- package/dist/cjs/print-designer-editor.js +3389 -3383
- package/dist/cjs/schemas.js +500 -38
- package/dist/cjs/ui.js +83339 -72691
- package/dist/esm/chunks/{index-pDt5vVVj.js → fontSizePxWidget-CbzQrSSM.js} +130 -5
- package/dist/esm/chunks/fontSizeTransform-CkTVJdRF.js +34 -0
- package/dist/esm/chunks/{helper-D7XF1bxK.js → helper-DSxGxZ0j.js} +16955 -5
- package/dist/esm/chunks/{index-D1FuD_XZ.js → index-DJkUkUo9.js} +4 -3
- package/dist/esm/chunks/{index-qTsnfbi7.js → index-D_-j4c4P.js} +744 -532
- package/dist/esm/chunks/{pluginRegistry-DEA2P0ud.js → pluginRegistry-Bgrz5qWG.js} +1 -1
- package/dist/esm/common.js +4 -3
- package/dist/esm/converter.js +1 -1
- package/dist/esm/generator.js +3 -3
- package/dist/esm/index.js +7 -6
- package/dist/esm/print-designer-editor.js +3374 -3374
- package/dist/esm/schemas.js +472 -13
- package/dist/esm/ui.js +83339 -72691
- package/dist/types/_vendors/common/constants.d.ts +1 -2
- package/dist/types/_vendors/common/fontSizeTransform.d.ts +5 -0
- package/dist/types/_vendors/common/googleFontUrls.d.ts +9 -0
- package/dist/types/_vendors/common/googleFonts.d.ts +7 -0
- package/dist/types/_vendors/common/helper.d.ts +2 -0
- package/dist/types/_vendors/common/index.d.ts +4 -2
- package/dist/types/_vendors/print-designer-editor/index.d.ts +2 -1
- package/dist/types/_vendors/print-designer-editor/types.d.ts +1 -1
- package/dist/types/_vendors/print-designer-editor/useDesigner.d.ts +1 -1
- package/dist/types/_vendors/schemas/index.d.ts +8 -2
- package/dist/types/_vendors/schemas/richText/helper.d.ts +3 -0
- package/dist/types/_vendors/schemas/richText/index.d.ts +4 -0
- package/dist/types/_vendors/schemas/richText/pdfRender.d.ts +3 -0
- package/dist/types/_vendors/schemas/richText/propPanel.d.ts +3 -0
- package/dist/types/_vendors/schemas/richText/types.d.ts +7 -0
- package/dist/types/_vendors/schemas/richText/uiRender.d.ts +3 -0
- package/dist/types/_vendors/schemas/singleVariableText/index.d.ts +4 -0
- package/dist/types/_vendors/schemas/singleVariableText/propPanel.d.ts +3 -0
- package/dist/types/_vendors/schemas/singleVariableText/types.d.ts +4 -0
- package/dist/types/_vendors/schemas/text/fontCache.d.ts +10 -0
- package/dist/types/_vendors/schemas/text/fontSizePxWidget.d.ts +9 -0
- package/dist/types/_vendors/ui/components/CtlBar.d.ts +1 -1
- package/dist/types/_vendors/ui/components/Paper.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var helper = require('./helper-
|
|
3
|
+
var helper = require('./helper-DGH62Z2s.js');
|
|
4
4
|
var fontkit = require('fontkit');
|
|
5
5
|
var buffer = require('buffer');
|
|
6
6
|
var colors = require('./colors-BeBcxfhb.js');
|
|
@@ -121,6 +121,36 @@ const LINE_END_FORBIDDEN_CHARS = [
|
|
|
121
121
|
'«',
|
|
122
122
|
];
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Global font binary cache for URL-based fonts.
|
|
126
|
+
* Fonts are fetched on demand and cached so subsequent uses are instant.
|
|
127
|
+
*/
|
|
128
|
+
const _binaryCache = new Map();
|
|
129
|
+
const _inflight = new Map();
|
|
130
|
+
/**
|
|
131
|
+
* Fetch font binary data for a URL, using the global cache.
|
|
132
|
+
* Returns cached ArrayBuffer if available, otherwise fetches and caches.
|
|
133
|
+
* Deduplicates concurrent requests for the same URL.
|
|
134
|
+
*/
|
|
135
|
+
const fetchFontBinary = async (url) => {
|
|
136
|
+
const cached = _binaryCache.get(url);
|
|
137
|
+
if (cached)
|
|
138
|
+
return cached;
|
|
139
|
+
// Deduplicate: if a fetch for this URL is already in flight, reuse it
|
|
140
|
+
const inflight = _inflight.get(url);
|
|
141
|
+
if (inflight)
|
|
142
|
+
return inflight;
|
|
143
|
+
const promise = fetch(url)
|
|
144
|
+
.then((res) => res.arrayBuffer())
|
|
145
|
+
.then((buf) => {
|
|
146
|
+
_binaryCache.set(url, buf);
|
|
147
|
+
_inflight.delete(url);
|
|
148
|
+
return buf;
|
|
149
|
+
});
|
|
150
|
+
_inflight.set(url, promise);
|
|
151
|
+
return promise;
|
|
152
|
+
};
|
|
153
|
+
|
|
124
154
|
const getBrowserVerticalFontAdjustments = (fontKitFont, fontSize, lineHeight, verticalAlignment) => {
|
|
125
155
|
const { ascent, descent, unitsPerEm } = fontKitFont;
|
|
126
156
|
// Fonts have a designed line height that the browser renders when using `line-height: normal`
|
|
@@ -189,7 +219,7 @@ const getFontKitFont = async (fontName, font, _cache) => {
|
|
|
189
219
|
if (typeof fontData === 'string') {
|
|
190
220
|
const isUrl = fontData.startsWith('http') || fontData.startsWith('/') || fontData.startsWith('./');
|
|
191
221
|
fontData = isUrl
|
|
192
|
-
? await
|
|
222
|
+
? await fetchFontBinary(fontData)
|
|
193
223
|
: helper.b64toUint8Array(fontData);
|
|
194
224
|
}
|
|
195
225
|
// Convert fontData to Buffer if it's not already a Buffer
|
|
@@ -1615,26 +1645,34 @@ const createSvgStr = (icon, attrs) => {
|
|
|
1615
1645
|
return `<svg ${svgAttrString}>${elementsString}</svg>`;
|
|
1616
1646
|
};
|
|
1617
1647
|
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
if (_cache.has(
|
|
1621
|
-
return _cache.get(
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1648
|
+
const embedPdfFont = async (pdfDoc, fontName, font, _cache) => {
|
|
1649
|
+
const cacheKey = `embedPdfFont-${fontName}`;
|
|
1650
|
+
if (_cache.has(cacheKey))
|
|
1651
|
+
return _cache.get(cacheKey);
|
|
1652
|
+
const fontEntry = font[fontName];
|
|
1653
|
+
if (!fontEntry)
|
|
1654
|
+
throw new Error(`Font "${fontName}" not found`);
|
|
1655
|
+
let fontData = fontEntry.data;
|
|
1656
|
+
if (typeof fontData === 'string') {
|
|
1657
|
+
const isUrl = fontData.startsWith('http') || fontData.startsWith('/') || fontData.startsWith('./');
|
|
1658
|
+
fontData = isUrl ? await fetchFontBinary(fontData) : helper.b64toUint8Array(fontData);
|
|
1659
|
+
}
|
|
1660
|
+
const useSubset = typeof fontEntry.subset === 'undefined' ? true : fontEntry.subset;
|
|
1661
|
+
let pdfFont;
|
|
1662
|
+
try {
|
|
1663
|
+
pdfFont = await pdfDoc.embedFont(fontData, { subset: useSubset });
|
|
1664
|
+
}
|
|
1665
|
+
catch (err) {
|
|
1666
|
+
// CFF/OpenType fonts fail subsetting (glyph._decode is not a function) — retry without subset
|
|
1667
|
+
if (useSubset) {
|
|
1668
|
+
pdfFont = await pdfDoc.embedFont(fontData, { subset: false });
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
throw err;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
_cache.set(cacheKey, pdfFont);
|
|
1675
|
+
return pdfFont;
|
|
1638
1676
|
};
|
|
1639
1677
|
const getFontProp = ({ value, fontKitFont, schema, colorType, }) => {
|
|
1640
1678
|
const fontSize = schema.dynamicFontSize
|
|
@@ -1656,18 +1694,12 @@ const pdfRender$1 = async (arg) => {
|
|
|
1656
1694
|
return;
|
|
1657
1695
|
const { font = helper.getDefaultFont(), colorType } = options;
|
|
1658
1696
|
const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
|
|
1659
|
-
const [
|
|
1660
|
-
|
|
1661
|
-
pdfDoc,
|
|
1662
|
-
font,
|
|
1663
|
-
_cache: _cache,
|
|
1664
|
-
}),
|
|
1697
|
+
const [pdfFontValue, fontKitFont] = await Promise.all([
|
|
1698
|
+
embedPdfFont(pdfDoc, resolvedFontName, font, _cache),
|
|
1665
1699
|
getFontKitFont(resolvedFontName, font, _cache),
|
|
1666
1700
|
]);
|
|
1667
1701
|
const fontProp = getFontProp({ value, fontKitFont, schema, colorType });
|
|
1668
1702
|
const { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing } = fontProp;
|
|
1669
|
-
const fontName = resolvedFontName;
|
|
1670
|
-
const pdfFontValue = pdfFontObj && pdfFontObj[fontName];
|
|
1671
1703
|
const pageHeight = page.getHeight();
|
|
1672
1704
|
const { width, height, rotate, position: { x, y }, opacity, } = convertForPdfLayoutProps({ schema, pageHeight, applyRotateTranslate: false });
|
|
1673
1705
|
const pivotPoint = { x: x + width / 2, y: pageHeight - helper.mm2pt(schema.position.y) - height / 2 };
|
|
@@ -1778,76 +1810,8 @@ const pdfRender$1 = async (arg) => {
|
|
|
1778
1810
|
});
|
|
1779
1811
|
};
|
|
1780
1812
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
return '';
|
|
1784
|
-
}
|
|
1785
|
-
let variables;
|
|
1786
|
-
try {
|
|
1787
|
-
variables =
|
|
1788
|
-
typeof variablesIn === 'string'
|
|
1789
|
-
? JSON.parse(variablesIn || '{}')
|
|
1790
|
-
: variablesIn;
|
|
1791
|
-
}
|
|
1792
|
-
catch {
|
|
1793
|
-
throw new SyntaxError(`[@campxdev/schemas] MVT: invalid JSON string '${variablesIn}'`);
|
|
1794
|
-
}
|
|
1795
|
-
// Merge extra context (e.g. currentPage, totalPages) with user variables
|
|
1796
|
-
// System context takes precedence over user variables
|
|
1797
|
-
const merged = extraContext ? { ...variables, ...extraContext } : variables;
|
|
1798
|
-
// Use the full JS expression evaluator — supports {varName}, {expr * 2}, {str.toUpperCase()}, etc.
|
|
1799
|
-
const result = expression.replacePlaceholders({ content: text, variables: merged, schemas: [] });
|
|
1800
|
-
// Strip any remaining unresolved {placeholders} for clean output
|
|
1801
|
-
return result.replace(/\{[^{}]+\}/g, '');
|
|
1802
|
-
};
|
|
1803
|
-
const validateVariables = (value, schema) => {
|
|
1804
|
-
if (!schema.variables || schema.variables.length === 0) {
|
|
1805
|
-
return true;
|
|
1806
|
-
}
|
|
1807
|
-
let values;
|
|
1808
|
-
try {
|
|
1809
|
-
values = value ? JSON.parse(value) : {};
|
|
1810
|
-
}
|
|
1811
|
-
catch {
|
|
1812
|
-
throw new SyntaxError(`[@campxdev/generator] invalid JSON string '${value}' for variables in field ${schema.name}`);
|
|
1813
|
-
}
|
|
1814
|
-
for (const variable of schema.variables) {
|
|
1815
|
-
if (!values[variable]) {
|
|
1816
|
-
if (schema.required) {
|
|
1817
|
-
throw new Error(`[@campxdev/generator] variable ${variable} is missing for field ${schema.name}`);
|
|
1818
|
-
}
|
|
1819
|
-
return false;
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
return true;
|
|
1823
|
-
};
|
|
1824
|
-
|
|
1825
|
-
const pdfRender = async (arg) => {
|
|
1826
|
-
const { value, schema, pageContext, ...rest } = arg;
|
|
1827
|
-
// Static mode: no template text → render value directly as plain text
|
|
1828
|
-
if (!schema.text) {
|
|
1829
|
-
await pdfRender$1({ value, schema, ...rest });
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
// readOnly: value is already resolved by generate.ts via replacePlaceholders
|
|
1833
|
-
if (schema.readOnly) {
|
|
1834
|
-
await pdfRender$1({ value, schema, ...rest });
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
// Dynamic mode (form): substitute variables in template
|
|
1838
|
-
if (!validateVariables(value, schema)) {
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
1841
|
-
const renderArgs = {
|
|
1842
|
-
value: substituteVariables(schema.text, value || '{}', pageContext),
|
|
1843
|
-
schema,
|
|
1844
|
-
...rest,
|
|
1845
|
-
};
|
|
1846
|
-
await pdfRender$1(renderArgs);
|
|
1847
|
-
};
|
|
1848
|
-
|
|
1849
|
-
const TextBoldIcon = createSvgStr(lucide.Bold);
|
|
1850
|
-
const TextItalicIcon = createSvgStr(lucide.Italic);
|
|
1813
|
+
createSvgStr(lucide.Bold);
|
|
1814
|
+
createSvgStr(lucide.Italic);
|
|
1851
1815
|
const TextStrikethroughIcon = createSvgStr(lucide.Strikethrough);
|
|
1852
1816
|
const TextUnderlineIcon = createSvgStr(lucide.Underline);
|
|
1853
1817
|
const TextAlignLeftIcon = createSvgStr(lucide.AlignLeft);
|
|
@@ -1871,8 +1835,7 @@ exports.Formatter = void 0;
|
|
|
1871
1835
|
})(exports.Formatter || (exports.Formatter = {}));
|
|
1872
1836
|
function getExtraFormatterSchema(i18n) {
|
|
1873
1837
|
const buttons = [
|
|
1874
|
-
|
|
1875
|
-
{ key: exports.Formatter.ITALIC, icon: TextItalicIcon, type: 'boolean' },
|
|
1838
|
+
// TODO: re-enable bold/italic controls when ready
|
|
1876
1839
|
{ key: exports.Formatter.STRIKETHROUGH, icon: TextStrikethroughIcon, type: 'boolean' },
|
|
1877
1840
|
{ key: exports.Formatter.UNDERLINE, icon: TextUnderlineIcon, type: 'boolean' },
|
|
1878
1841
|
{ key: exports.Formatter.ALIGNMENT, icon: TextAlignLeftIcon, type: 'select', value: DEFAULT_ALIGNMENT },
|
|
@@ -1930,120 +1893,111 @@ const UseDynamicFontSize = (props) => {
|
|
|
1930
1893
|
label.appendChild(span);
|
|
1931
1894
|
rootElement.appendChild(label);
|
|
1932
1895
|
};
|
|
1933
|
-
const propPanel$
|
|
1896
|
+
const propPanel$2 = {
|
|
1934
1897
|
schema: ({ options, activeSchema, i18n }) => {
|
|
1935
1898
|
const font = options.font || { [helper.DEFAULT_FONT_NAME]: { data: '', fallback: true } };
|
|
1936
1899
|
const fontNames = Object.keys(font);
|
|
1937
1900
|
const fallbackFontName = helper.getFallbackFontName(font);
|
|
1938
1901
|
const enableDynamicFont = Boolean(activeSchema?.dynamicFontSize);
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
disabled: enableDynamicFont,
|
|
1955
|
-
props: { min: 0 },
|
|
1956
|
-
},
|
|
1957
|
-
characterSpacing: {
|
|
1958
|
-
title: i18n('schemas.text.spacing'),
|
|
1959
|
-
type: 'number',
|
|
1960
|
-
widget: 'inputNumber',
|
|
1961
|
-
span: 6,
|
|
1962
|
-
props: { min: 0 },
|
|
1963
|
-
},
|
|
1964
|
-
formatter: getExtraFormatterSchema(i18n),
|
|
1965
|
-
lineHeight: {
|
|
1966
|
-
title: i18n('schemas.text.lineHeight'),
|
|
1967
|
-
type: 'number',
|
|
1968
|
-
widget: 'inputNumber',
|
|
1969
|
-
props: { step: 0.1, min: 0 },
|
|
1970
|
-
span: 8,
|
|
1902
|
+
const result = {};
|
|
1903
|
+
result.fontName = {
|
|
1904
|
+
title: i18n('schemas.text.fontName'),
|
|
1905
|
+
type: 'string',
|
|
1906
|
+
widget: 'select',
|
|
1907
|
+
default: fallbackFontName,
|
|
1908
|
+
placeholder: fallbackFontName,
|
|
1909
|
+
props: {
|
|
1910
|
+
options: fontNames.map((name) => ({ label: name, value: name })),
|
|
1911
|
+
showSearch: true,
|
|
1912
|
+
virtual: true,
|
|
1913
|
+
filterOption: (input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
|
|
1914
|
+
popupMatchSelectWidth: false,
|
|
1915
|
+
listHeight: 300,
|
|
1916
|
+
style: { width: '100%' },
|
|
1971
1917
|
},
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1918
|
+
span: 24,
|
|
1919
|
+
};
|
|
1920
|
+
result.fontSize = {
|
|
1921
|
+
title: i18n('schemas.text.size'),
|
|
1922
|
+
type: 'number',
|
|
1923
|
+
widget: 'inputNumber',
|
|
1924
|
+
span: 6,
|
|
1925
|
+
disabled: enableDynamicFont,
|
|
1926
|
+
props: { min: 0 },
|
|
1927
|
+
};
|
|
1928
|
+
result.characterSpacing = {
|
|
1929
|
+
title: i18n('schemas.text.spacing'),
|
|
1930
|
+
type: 'number',
|
|
1931
|
+
widget: 'inputNumber',
|
|
1932
|
+
span: 6,
|
|
1933
|
+
props: { min: 0 },
|
|
1934
|
+
};
|
|
1935
|
+
result.formatter = getExtraFormatterSchema(i18n);
|
|
1936
|
+
result.lineHeight = {
|
|
1937
|
+
title: i18n('schemas.text.lineHeight'),
|
|
1938
|
+
type: 'number',
|
|
1939
|
+
widget: 'inputNumber',
|
|
1940
|
+
props: { step: 0.1, min: 0 },
|
|
1941
|
+
span: 8,
|
|
1942
|
+
};
|
|
1943
|
+
result.useDynamicFontSize = { type: 'boolean', widget: 'UseDynamicFontSize', bind: false, span: 16 };
|
|
1944
|
+
result.dynamicFontSize = {
|
|
1945
|
+
type: 'object',
|
|
1946
|
+
widget: 'card',
|
|
1947
|
+
column: 3,
|
|
1948
|
+
properties: {
|
|
1949
|
+
min: {
|
|
1950
|
+
title: i18n('schemas.text.min'),
|
|
1951
|
+
type: 'number',
|
|
1952
|
+
widget: 'inputNumber',
|
|
1953
|
+
hidden: !enableDynamicFont,
|
|
1954
|
+
props: { min: 0 },
|
|
2004
1955
|
},
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
disabledAlpha: true,
|
|
1956
|
+
max: {
|
|
1957
|
+
title: i18n('schemas.text.max'),
|
|
1958
|
+
type: 'number',
|
|
1959
|
+
widget: 'inputNumber',
|
|
1960
|
+
hidden: !enableDynamicFont,
|
|
1961
|
+
props: { min: 0 },
|
|
2012
1962
|
},
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
1963
|
+
fit: {
|
|
1964
|
+
title: i18n('schemas.text.fit'),
|
|
1965
|
+
type: 'string',
|
|
1966
|
+
widget: 'select',
|
|
1967
|
+
hidden: !enableDynamicFont,
|
|
1968
|
+
props: {
|
|
1969
|
+
options: [
|
|
1970
|
+
{ label: i18n('schemas.horizontal'), value: DYNAMIC_FIT_HORIZONTAL },
|
|
1971
|
+
{ label: i18n('schemas.vertical'), value: DYNAMIC_FIT_VERTICAL },
|
|
1972
|
+
],
|
|
2017
1973
|
},
|
|
2018
|
-
],
|
|
2019
|
-
},
|
|
2020
|
-
backgroundColor: {
|
|
2021
|
-
title: i18n('schemas.bgColor'),
|
|
2022
|
-
type: 'string',
|
|
2023
|
-
widget: 'color',
|
|
2024
|
-
props: {
|
|
2025
|
-
disabledAlpha: true,
|
|
2026
1974
|
},
|
|
2027
|
-
rules: [
|
|
2028
|
-
{
|
|
2029
|
-
pattern: HEX_COLOR_PATTERN,
|
|
2030
|
-
message: i18n('validation.hexColor'),
|
|
2031
|
-
},
|
|
2032
|
-
],
|
|
2033
1975
|
},
|
|
2034
1976
|
};
|
|
2035
|
-
|
|
1977
|
+
result.fontColor = {
|
|
1978
|
+
title: i18n('schemas.textColor'),
|
|
1979
|
+
type: 'string',
|
|
1980
|
+
widget: 'color',
|
|
1981
|
+
props: { disabledAlpha: true },
|
|
1982
|
+
rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
|
|
1983
|
+
};
|
|
1984
|
+
result.backgroundColor = {
|
|
1985
|
+
title: i18n('schemas.bgColor'),
|
|
1986
|
+
type: 'string',
|
|
1987
|
+
widget: 'color',
|
|
1988
|
+
props: { disabledAlpha: true },
|
|
1989
|
+
rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
|
|
1990
|
+
};
|
|
1991
|
+
return result;
|
|
2036
1992
|
},
|
|
2037
1993
|
widgets: { UseDynamicFontSize },
|
|
2038
1994
|
defaultSchema: {
|
|
2039
1995
|
name: '',
|
|
2040
1996
|
type: 'text',
|
|
2041
|
-
content: '
|
|
1997
|
+
content: '',
|
|
2042
1998
|
position: { x: 0, y: 0 },
|
|
2043
1999
|
width: 45,
|
|
2044
|
-
height:
|
|
2045
|
-
// If the value of "rotate" is set to undefined or not set at all, rotation will be disabled in the UI.
|
|
2046
|
-
// Check this document: https://pdfme.com//docs/custom-schemas#learning-how-to-create-from-pdfmeschemas-code
|
|
2000
|
+
height: 7,
|
|
2047
2001
|
rotate: 0,
|
|
2048
2002
|
alignment: DEFAULT_ALIGNMENT,
|
|
2049
2003
|
verticalAlignment: DEFAULT_VERTICAL_ALIGNMENT,
|
|
@@ -2062,338 +2016,120 @@ const propPanel$1 = {
|
|
|
2062
2016
|
},
|
|
2063
2017
|
};
|
|
2064
2018
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
console.log('[insertVariableWidget] variables.length:', variables.length);
|
|
2081
|
-
if (variables.length === 0) {
|
|
2082
|
-
console.log('[insertVariableWidget] NO VARIABLES - returning early');
|
|
2083
|
-
return;
|
|
2019
|
+
const _loadedGoogleFonts = new Set();
|
|
2020
|
+
const ensureGoogleFontLoaded = (fontName, fontData) => {
|
|
2021
|
+
if (!fontName || _loadedGoogleFonts.has(fontName) || !helper.isGoogleFont(fontName))
|
|
2022
|
+
return;
|
|
2023
|
+
if (!document?.fonts)
|
|
2024
|
+
return;
|
|
2025
|
+
_loadedGoogleFonts.add(fontName);
|
|
2026
|
+
const fontFace = new FontFace(fontName, typeof fontData === 'string' ? `url(${fontData})` : fontData, { display: 'swap' });
|
|
2027
|
+
fontFace.load().then(() => document.fonts.add(fontFace)).catch(() => { });
|
|
2028
|
+
};
|
|
2029
|
+
const replaceUnsupportedChars = (text, fontKitFont) => {
|
|
2030
|
+
const charSupportCache = {};
|
|
2031
|
+
const isCharSupported = (char) => {
|
|
2032
|
+
if (char in charSupportCache) {
|
|
2033
|
+
return charSupportCache[char];
|
|
2084
2034
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
'display:flex; gap:6px; align-items:center; margin-bottom:10px; z-index:9999; position:relative;';
|
|
2089
|
-
console.log('[insertVariableWidget] rootElement:', rootElement);
|
|
2090
|
-
console.log('[insertVariableWidget] rootElement.parentElement:', rootElement.parentElement);
|
|
2091
|
-
console.log('[insertVariableWidget] rootElement computed style:', window.getComputedStyle(rootElement));
|
|
2092
|
-
const label = document.createElement('span');
|
|
2093
|
-
label.textContent = 'Insert Variable:';
|
|
2094
|
-
label.style.cssText = 'font-size:12px; color:#666; white-space:nowrap;';
|
|
2095
|
-
const select = document.createElement('select');
|
|
2096
|
-
select.style.cssText =
|
|
2097
|
-
'flex:1; height:30px; border:1px solid #E0E0E0; border-radius:4px; padding:0 8px; font-size:13px; background:#fff; cursor:pointer; z-index:9999;';
|
|
2098
|
-
const blank = document.createElement('option');
|
|
2099
|
-
blank.value = '';
|
|
2100
|
-
blank.textContent = '— pick variable —';
|
|
2101
|
-
select.appendChild(blank);
|
|
2102
|
-
for (const v of variables) {
|
|
2103
|
-
const opt = document.createElement('option');
|
|
2104
|
-
opt.value = v.value;
|
|
2105
|
-
opt.textContent = v.label;
|
|
2106
|
-
select.appendChild(opt);
|
|
2107
|
-
}
|
|
2108
|
-
select.onchange = (e) => {
|
|
2109
|
-
const varName = e.target.value;
|
|
2110
|
-
if (!varName)
|
|
2111
|
-
return;
|
|
2112
|
-
const current = String(activeSchema[targetKey] ?? '');
|
|
2113
|
-
changeSchemas([{ key: targetKey, value: current + `{${varName}}`, schemaId: activeSchema.id }]);
|
|
2114
|
-
select.value = '';
|
|
2115
|
-
};
|
|
2116
|
-
container.appendChild(label);
|
|
2117
|
-
container.appendChild(select);
|
|
2118
|
-
rootElement.appendChild(container);
|
|
2119
|
-
console.log('[insertVariableWidget] Successfully created and appended widget');
|
|
2035
|
+
const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
|
|
2036
|
+
charSupportCache[char] = isSupported;
|
|
2037
|
+
return isSupported;
|
|
2120
2038
|
};
|
|
2039
|
+
const segments = text.split(/(\r\n|\n|\r)/);
|
|
2040
|
+
return segments
|
|
2041
|
+
.map((segment) => {
|
|
2042
|
+
if (/\r\n|\n|\r/.test(segment)) {
|
|
2043
|
+
return segment;
|
|
2044
|
+
}
|
|
2045
|
+
return segment
|
|
2046
|
+
.split('')
|
|
2047
|
+
.map((char) => {
|
|
2048
|
+
if (/\s/.test(char) || char.charCodeAt(0) < 32) {
|
|
2049
|
+
return char;
|
|
2050
|
+
}
|
|
2051
|
+
return isCharSupported(char) ? char : '〿';
|
|
2052
|
+
})
|
|
2053
|
+
.join('');
|
|
2054
|
+
})
|
|
2055
|
+
.join('');
|
|
2121
2056
|
};
|
|
2122
|
-
|
|
2123
|
-
const
|
|
2124
|
-
const
|
|
2125
|
-
const
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2057
|
+
const uiRender$2 = async (arg) => {
|
|
2058
|
+
const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
|
|
2059
|
+
const effectivePlaceholder = placeholder || 'Type Something...';
|
|
2060
|
+
const usePlaceholder = isEditable(mode, schema) && !value;
|
|
2061
|
+
const getText = (element) => {
|
|
2062
|
+
let text = element.innerText;
|
|
2063
|
+
if (text.endsWith('\n')) {
|
|
2064
|
+
// contenteditable adds additional newline char retrieved with innerText
|
|
2065
|
+
text = text.slice(0, -1);
|
|
2066
|
+
}
|
|
2067
|
+
return text;
|
|
2068
|
+
};
|
|
2069
|
+
const font = options?.font || helper.getDefaultFont();
|
|
2070
|
+
const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
|
|
2071
|
+
// Lazily load Google Font for CSS rendering if needed
|
|
2072
|
+
if (resolvedFontName && font[resolvedFontName]) {
|
|
2073
|
+
ensureGoogleFontLoaded(resolvedFontName, font[resolvedFontName].data);
|
|
2074
|
+
}
|
|
2075
|
+
// Show a subtle loading state while the font binary is being fetched
|
|
2076
|
+
const isLoading = helper.isGoogleFont(resolvedFontName);
|
|
2077
|
+
if (isLoading) {
|
|
2078
|
+
arg.rootElement.style.opacity = '0.4';
|
|
2079
|
+
arg.rootElement.style.transition = 'opacity 0.15s ease-out';
|
|
2080
|
+
}
|
|
2081
|
+
const fontKitFont = await getFontKitFont(resolvedFontName, font, _cache);
|
|
2082
|
+
const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? effectivePlaceholder : value);
|
|
2083
|
+
// Fade in once the font is ready
|
|
2084
|
+
if (isLoading) {
|
|
2085
|
+
// Force a reflow so the transition triggers from the dimmed state
|
|
2086
|
+
void arg.rootElement.offsetHeight;
|
|
2087
|
+
arg.rootElement.style.opacity = '1';
|
|
2088
|
+
}
|
|
2089
|
+
const processedText = replaceUnsupportedChars(value, fontKitFont);
|
|
2090
|
+
if (!isEditable(mode, schema)) {
|
|
2091
|
+
// Read-only mode
|
|
2092
|
+
textBlock.innerHTML = processedText
|
|
2093
|
+
.split('')
|
|
2094
|
+
.map((l, i) => `<span style="letter-spacing:${String(value).length === i + 1 ? 0 : 'inherit'};">${l}</span>`)
|
|
2095
|
+
.join('');
|
|
2130
2096
|
return;
|
|
2131
2097
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
{ key: 'content', value:
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
type: 'object',
|
|
2163
|
-
widget: 'mapDynamicVariables',
|
|
2164
|
-
bind: false,
|
|
2165
|
-
span: 0,
|
|
2166
|
-
},
|
|
2167
|
-
};
|
|
2168
|
-
},
|
|
2169
|
-
widgets: { ...(propPanel$1.widgets || {}), mapDynamicVariables, insertVariableWidget },
|
|
2170
|
-
defaultSchema: {
|
|
2171
|
-
...propPanel$1.defaultSchema,
|
|
2172
|
-
readOnly: false,
|
|
2173
|
-
type: 'text',
|
|
2174
|
-
text: 'Type Something...',
|
|
2175
|
-
width: 50,
|
|
2176
|
-
height: 15,
|
|
2177
|
-
content: '{}',
|
|
2178
|
-
variables: [],
|
|
2179
|
-
},
|
|
2180
|
-
};
|
|
2181
|
-
/** Known JS globals/keywords that should NOT be treated as user-defined variables */
|
|
2182
|
-
const RESERVED_NAMES$1 = new Set([
|
|
2183
|
-
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
2184
|
-
'void', 'delete', 'new', 'this', 'NaN', 'Infinity',
|
|
2185
|
-
'Math', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'JSON',
|
|
2186
|
-
'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
|
|
2187
|
-
'encodeURI', 'encodeURIComponent', 'date', 'dateTime',
|
|
2188
|
-
'currentPage', 'totalPages',
|
|
2189
|
-
]);
|
|
2190
|
-
/**
|
|
2191
|
-
* Extract full dot-notation paths from an expression string.
|
|
2192
|
-
* E.g. "student.marks.sem1 > 80" → ["student.marks.sem1"]
|
|
2193
|
-
* Handles method calls: "student.name.toUpperCase()" → ["student.name"]
|
|
2194
|
-
* Skips string literals and reserved names.
|
|
2195
|
-
*/
|
|
2196
|
-
const extractDotPaths = (expr) => {
|
|
2197
|
-
// Replace string literals with spaces (preserving positions for nextChar lookup)
|
|
2198
|
-
const cleaned = expr.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, (m) => ' '.repeat(m.length));
|
|
2199
|
-
const pathRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
|
|
2200
|
-
const paths = new Set();
|
|
2201
|
-
let m;
|
|
2202
|
-
while ((m = pathRegex.exec(cleaned)) !== null) {
|
|
2203
|
-
let path = m[0];
|
|
2204
|
-
// If followed by '(', the last segment is a method call — trim it
|
|
2205
|
-
const nextChar = cleaned[m.index + path.length];
|
|
2206
|
-
if (nextChar === '(') {
|
|
2207
|
-
const lastDot = path.lastIndexOf('.');
|
|
2208
|
-
if (lastDot !== -1) {
|
|
2209
|
-
path = path.substring(0, lastDot);
|
|
2210
|
-
}
|
|
2211
|
-
else {
|
|
2212
|
-
// Standalone function call like parseInt(...) — skip
|
|
2213
|
-
continue;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
const root = path.split('.')[0];
|
|
2217
|
-
if (!RESERVED_NAMES$1.has(root))
|
|
2218
|
-
paths.add(path);
|
|
2219
|
-
}
|
|
2220
|
-
return Array.from(paths);
|
|
2221
|
-
};
|
|
2222
|
-
/**
|
|
2223
|
-
* Build a nested default object from dot-paths.
|
|
2224
|
-
* E.g. ["student.name", "student.marks.sem1"] →
|
|
2225
|
-
* { name: "NAME", marks: { sem1: "SEM1" } }
|
|
2226
|
-
* Merges into an existing object, only adding missing leaves.
|
|
2227
|
-
* Returns true if anything was added.
|
|
2228
|
-
*/
|
|
2229
|
-
const buildNestedDefault = (obj, paths) => {
|
|
2230
|
-
let added = false;
|
|
2231
|
-
for (const path of paths) {
|
|
2232
|
-
const parts = path.split('.');
|
|
2233
|
-
if (parts.length <= 1)
|
|
2234
|
-
continue; // no nested parts
|
|
2235
|
-
let current = obj;
|
|
2236
|
-
for (let i = 1; i < parts.length - 1; i++) {
|
|
2237
|
-
if (!(parts[i] in current) || typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
|
|
2238
|
-
current[parts[i]] = {};
|
|
2239
|
-
added = true;
|
|
2240
|
-
}
|
|
2241
|
-
current = current[parts[i]];
|
|
2242
|
-
}
|
|
2243
|
-
const leaf = parts[parts.length - 1];
|
|
2244
|
-
if (!(leaf in current)) {
|
|
2245
|
-
current[leaf] = path.replace(/\./g, '_').toUpperCase();
|
|
2246
|
-
added = true;
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
return added;
|
|
2250
|
-
};
|
|
2251
|
-
const updateVariablesFromText = (text, variables) => {
|
|
2252
|
-
// Find all {...} blocks and extract dot-notation paths from each
|
|
2253
|
-
const blockRegex = /\{([^{}]+)\}/g;
|
|
2254
|
-
const allPaths = new Set();
|
|
2255
|
-
let blockMatch;
|
|
2256
|
-
while ((blockMatch = blockRegex.exec(text)) !== null) {
|
|
2257
|
-
for (const path of extractDotPaths(blockMatch[1])) {
|
|
2258
|
-
allPaths.add(path);
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
// Group paths by root identifier
|
|
2262
|
-
const rootToPaths = new Map();
|
|
2263
|
-
for (const path of allPaths) {
|
|
2264
|
-
const root = path.split('.')[0];
|
|
2265
|
-
if (!rootToPaths.has(root))
|
|
2266
|
-
rootToPaths.set(root, []);
|
|
2267
|
-
rootToPaths.get(root).push(path);
|
|
2268
|
-
}
|
|
2269
|
-
const allRoots = new Set(rootToPaths.keys());
|
|
2270
|
-
let changed = false;
|
|
2271
|
-
for (const [root, paths] of rootToPaths) {
|
|
2272
|
-
const hasNested = paths.some((p) => p.includes('.'));
|
|
2273
|
-
if (hasNested) {
|
|
2274
|
-
// Parse existing value or start fresh
|
|
2275
|
-
let obj = {};
|
|
2276
|
-
if (root in variables) {
|
|
2277
|
-
try {
|
|
2278
|
-
const parsed = JSON.parse(variables[root]);
|
|
2279
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
2280
|
-
obj = parsed;
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
catch {
|
|
2284
|
-
/* not JSON, will rebuild */
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
const added = buildNestedDefault(obj, paths);
|
|
2288
|
-
if (!(root in variables) || added) {
|
|
2289
|
-
variables[root] = JSON.stringify(obj);
|
|
2290
|
-
changed = true;
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
else {
|
|
2294
|
-
if (!(root in variables)) {
|
|
2295
|
-
variables[root] = root.toUpperCase();
|
|
2296
|
-
changed = true;
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
// Remove variables whose root is no longer referenced
|
|
2301
|
-
for (const varName of Object.keys(variables)) {
|
|
2302
|
-
if (!allRoots.has(varName)) {
|
|
2303
|
-
delete variables[varName];
|
|
2304
|
-
changed = true;
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
return changed;
|
|
2308
|
-
};
|
|
2309
|
-
|
|
2310
|
-
const replaceUnsupportedChars = (text, fontKitFont) => {
|
|
2311
|
-
const charSupportCache = {};
|
|
2312
|
-
const isCharSupported = (char) => {
|
|
2313
|
-
if (char in charSupportCache) {
|
|
2314
|
-
return charSupportCache[char];
|
|
2315
|
-
}
|
|
2316
|
-
const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
|
|
2317
|
-
charSupportCache[char] = isSupported;
|
|
2318
|
-
return isSupported;
|
|
2319
|
-
};
|
|
2320
|
-
const segments = text.split(/(\r\n|\n|\r)/);
|
|
2321
|
-
return segments
|
|
2322
|
-
.map((segment) => {
|
|
2323
|
-
if (/\r\n|\n|\r/.test(segment)) {
|
|
2324
|
-
return segment;
|
|
2325
|
-
}
|
|
2326
|
-
return segment
|
|
2327
|
-
.split('')
|
|
2328
|
-
.map((char) => {
|
|
2329
|
-
if (/\s/.test(char) || char.charCodeAt(0) < 32) {
|
|
2330
|
-
return char;
|
|
2331
|
-
}
|
|
2332
|
-
return isCharSupported(char) ? char : '〿';
|
|
2333
|
-
})
|
|
2334
|
-
.join('');
|
|
2335
|
-
})
|
|
2336
|
-
.join('');
|
|
2337
|
-
};
|
|
2338
|
-
const uiRender$1 = async (arg) => {
|
|
2339
|
-
const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
|
|
2340
|
-
const usePlaceholder = isEditable(mode, schema) && placeholder && !value;
|
|
2341
|
-
const getText = (element) => {
|
|
2342
|
-
let text = element.innerText;
|
|
2343
|
-
if (text.endsWith('\n')) {
|
|
2344
|
-
// contenteditable adds additional newline char retrieved with innerText
|
|
2345
|
-
text = text.slice(0, -1);
|
|
2346
|
-
}
|
|
2347
|
-
return text;
|
|
2348
|
-
};
|
|
2349
|
-
const font = options?.font || helper.getDefaultFont();
|
|
2350
|
-
const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
|
|
2351
|
-
const fontKitFont = await getFontKitFont(resolvedFontName, font, _cache);
|
|
2352
|
-
const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? placeholder : value);
|
|
2353
|
-
const processedText = replaceUnsupportedChars(value, fontKitFont);
|
|
2354
|
-
if (!isEditable(mode, schema)) {
|
|
2355
|
-
// Read-only mode
|
|
2356
|
-
textBlock.innerHTML = processedText
|
|
2357
|
-
.split('')
|
|
2358
|
-
.map((l, i) => `<span style="letter-spacing:${String(value).length === i + 1 ? 0 : 'inherit'};">${l}</span>`)
|
|
2359
|
-
.join('');
|
|
2360
|
-
return;
|
|
2361
|
-
}
|
|
2362
|
-
makeElementPlainTextContentEditable(textBlock);
|
|
2363
|
-
textBlock.tabIndex = tabIndex || 0;
|
|
2364
|
-
textBlock.innerText = mode === 'designer' ? value : processedText;
|
|
2365
|
-
textBlock.addEventListener('blur', (e) => {
|
|
2366
|
-
if (onChange)
|
|
2367
|
-
onChange({ key: 'content', value: getText(e.target) });
|
|
2368
|
-
if (stopEditing)
|
|
2369
|
-
stopEditing();
|
|
2370
|
-
});
|
|
2371
|
-
if (schema.dynamicFontSize) {
|
|
2372
|
-
let dynamicFontSize = undefined;
|
|
2373
|
-
textBlock.addEventListener('keyup', () => {
|
|
2374
|
-
setTimeout(() => {
|
|
2375
|
-
// Use a regular function instead of an async one since we don't need await
|
|
2376
|
-
(() => {
|
|
2377
|
-
if (!textBlock.textContent)
|
|
2378
|
-
return;
|
|
2379
|
-
dynamicFontSize = calculateDynamicFontSize({
|
|
2380
|
-
textSchema: schema,
|
|
2381
|
-
fontKitFont,
|
|
2382
|
-
value: getText(textBlock),
|
|
2383
|
-
startingFontSize: dynamicFontSize,
|
|
2384
|
-
});
|
|
2385
|
-
textBlock.style.fontSize = `${dynamicFontSize}pt`;
|
|
2386
|
-
const { topAdj: newTopAdj, bottomAdj: newBottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
|
|
2387
|
-
textBlock.style.paddingTop = `${newTopAdj}px`;
|
|
2388
|
-
textBlock.style.marginBottom = `${newBottomAdj}px`;
|
|
2389
|
-
})();
|
|
2390
|
-
}, 0);
|
|
2391
|
-
});
|
|
2098
|
+
makeElementPlainTextContentEditable(textBlock);
|
|
2099
|
+
textBlock.tabIndex = tabIndex || 0;
|
|
2100
|
+
textBlock.innerText = mode === 'designer' ? value : processedText;
|
|
2101
|
+
textBlock.addEventListener('blur', (e) => {
|
|
2102
|
+
if (onChange)
|
|
2103
|
+
onChange({ key: 'content', value: getText(e.target) });
|
|
2104
|
+
if (stopEditing)
|
|
2105
|
+
stopEditing();
|
|
2106
|
+
});
|
|
2107
|
+
if (schema.dynamicFontSize) {
|
|
2108
|
+
let dynamicFontSize = undefined;
|
|
2109
|
+
textBlock.addEventListener('keyup', () => {
|
|
2110
|
+
setTimeout(() => {
|
|
2111
|
+
// Use a regular function instead of an async one since we don't need await
|
|
2112
|
+
(() => {
|
|
2113
|
+
if (!textBlock.textContent)
|
|
2114
|
+
return;
|
|
2115
|
+
dynamicFontSize = calculateDynamicFontSize({
|
|
2116
|
+
textSchema: schema,
|
|
2117
|
+
fontKitFont,
|
|
2118
|
+
value: getText(textBlock),
|
|
2119
|
+
startingFontSize: dynamicFontSize,
|
|
2120
|
+
});
|
|
2121
|
+
textBlock.style.fontSize = `${dynamicFontSize}pt`;
|
|
2122
|
+
const { topAdj: newTopAdj, bottomAdj: newBottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
|
|
2123
|
+
textBlock.style.paddingTop = `${newTopAdj}px`;
|
|
2124
|
+
textBlock.style.marginBottom = `${newBottomAdj}px`;
|
|
2125
|
+
})();
|
|
2126
|
+
}, 0);
|
|
2127
|
+
});
|
|
2392
2128
|
}
|
|
2393
2129
|
if (usePlaceholder) {
|
|
2394
2130
|
textBlock.style.color = PLACEHOLDER_FONT_COLOR;
|
|
2395
2131
|
textBlock.addEventListener('focus', () => {
|
|
2396
|
-
if (textBlock.innerText ===
|
|
2132
|
+
if (textBlock.innerText === effectivePlaceholder) {
|
|
2397
2133
|
textBlock.innerText = '';
|
|
2398
2134
|
textBlock.style.color = schema.fontColor ?? DEFAULT_FONT_COLOR;
|
|
2399
2135
|
}
|
|
@@ -2523,11 +2259,301 @@ const getBackgroundColor = (value, schema) => {
|
|
|
2523
2259
|
return schema.backgroundColor;
|
|
2524
2260
|
};
|
|
2525
2261
|
|
|
2526
|
-
const
|
|
2262
|
+
const substituteVariables = (text, variablesIn, extraContext) => {
|
|
2263
|
+
if (!text) {
|
|
2264
|
+
return '';
|
|
2265
|
+
}
|
|
2266
|
+
let variables;
|
|
2267
|
+
try {
|
|
2268
|
+
variables =
|
|
2269
|
+
typeof variablesIn === 'string'
|
|
2270
|
+
? JSON.parse(variablesIn || '{}')
|
|
2271
|
+
: variablesIn;
|
|
2272
|
+
}
|
|
2273
|
+
catch {
|
|
2274
|
+
throw new SyntaxError(`[@campxdev/schemas] richText: invalid JSON string '${variablesIn}'`);
|
|
2275
|
+
}
|
|
2276
|
+
// Merge extra context (e.g. currentPage, totalPages) with user variables
|
|
2277
|
+
// System context takes precedence over user variables
|
|
2278
|
+
const merged = extraContext ? { ...variables, ...extraContext } : variables;
|
|
2279
|
+
// Use the full JS expression evaluator — supports {varName}, {expr * 2}, {str.toUpperCase()}, etc.
|
|
2280
|
+
const result = expression.replacePlaceholders({ content: text, variables: merged, schemas: [] });
|
|
2281
|
+
// Strip any remaining unresolved {placeholders} for clean output
|
|
2282
|
+
return result.replace(/\{[^{}]+\}/g, '');
|
|
2283
|
+
};
|
|
2284
|
+
const validateVariables = (value, schema) => {
|
|
2285
|
+
if (!schema.variables || schema.variables.length === 0) {
|
|
2286
|
+
return true;
|
|
2287
|
+
}
|
|
2288
|
+
let values;
|
|
2289
|
+
try {
|
|
2290
|
+
values = value ? JSON.parse(value) : {};
|
|
2291
|
+
}
|
|
2292
|
+
catch {
|
|
2293
|
+
throw new SyntaxError(`[@campxdev/generator] invalid JSON string '${value}' for variables in field ${schema.name}`);
|
|
2294
|
+
}
|
|
2295
|
+
for (const variable of schema.variables) {
|
|
2296
|
+
if (!values[variable]) {
|
|
2297
|
+
if (schema.required) {
|
|
2298
|
+
throw new Error(`[@campxdev/generator] variable ${variable} is missing for field ${schema.name}`);
|
|
2299
|
+
}
|
|
2300
|
+
return false;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return true;
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
const pdfRender = async (arg) => {
|
|
2307
|
+
const { value, schema, pageContext, ...rest } = arg;
|
|
2308
|
+
// Static mode: no template text → render value directly as plain text
|
|
2309
|
+
if (!schema.text) {
|
|
2310
|
+
await pdfRender$1({ value, schema, ...rest });
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
// readOnly: value is already resolved by generate.ts via replacePlaceholders
|
|
2314
|
+
if (schema.readOnly) {
|
|
2315
|
+
await pdfRender$1({ value, schema, ...rest });
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
// Dynamic mode (form): substitute variables in template
|
|
2319
|
+
if (!validateVariables(value, schema)) {
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const renderArgs = {
|
|
2323
|
+
value: substituteVariables(schema.text, value || '{}', pageContext),
|
|
2324
|
+
schema,
|
|
2325
|
+
...rest,
|
|
2326
|
+
};
|
|
2327
|
+
await pdfRender$1(renderArgs);
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
const variablePrefixSuffixWidget = (props) => {
|
|
2331
|
+
const { rootElement, changeSchemas, activeSchema, options } = props;
|
|
2332
|
+
const schema = activeSchema;
|
|
2333
|
+
const variables = options.variables?.textVariables ?? [];
|
|
2334
|
+
if (variables.length === 0) {
|
|
2335
|
+
const hint = document.createElement('div');
|
|
2336
|
+
hint.textContent = 'Type {variableName} directly in the text field';
|
|
2337
|
+
hint.style.cssText = 'font-size:11px;color:#aaa;margin-bottom:8px;font-style:italic;';
|
|
2338
|
+
rootElement.appendChild(hint);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const container = document.createElement('div');
|
|
2342
|
+
container.style.cssText = 'display:flex;flex-direction:column;gap:4px;margin-bottom:8px;';
|
|
2343
|
+
const title = document.createElement('div');
|
|
2344
|
+
title.textContent = 'Insert Variable:';
|
|
2345
|
+
title.style.cssText = 'font-size:12px;color:#666;margin-bottom:2px;';
|
|
2346
|
+
container.appendChild(title);
|
|
2347
|
+
for (const v of variables) {
|
|
2348
|
+
const row = document.createElement('div');
|
|
2349
|
+
row.style.cssText = 'display:flex;align-items:center;gap:6px;';
|
|
2350
|
+
const makeBtn = (side) => {
|
|
2351
|
+
const btn = document.createElement('button');
|
|
2352
|
+
btn.textContent = '+';
|
|
2353
|
+
btn.title = side === 'prefix' ? `Add ${v.label} as prefix` : `Add ${v.label} as suffix`;
|
|
2354
|
+
btn.style.cssText =
|
|
2355
|
+
'width:22px;height:22px;cursor:pointer;border:1px solid #d9d9d9;border-radius:4px;' +
|
|
2356
|
+
'background:#fff;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;padding:0;';
|
|
2357
|
+
btn.onclick = () => {
|
|
2358
|
+
const current = String(schema.text ?? '');
|
|
2359
|
+
const next = side === 'prefix' ? `{${v.value}} ${current}` : `${current} {${v.value}}`;
|
|
2360
|
+
changeSchemas([{ key: 'text', value: next, schemaId: activeSchema.id }]);
|
|
2361
|
+
};
|
|
2362
|
+
return btn;
|
|
2363
|
+
};
|
|
2364
|
+
const label = document.createElement('span');
|
|
2365
|
+
label.textContent = v.label;
|
|
2366
|
+
label.style.cssText = 'font-size:12px;flex:1;color:#333;';
|
|
2367
|
+
row.appendChild(makeBtn('prefix'));
|
|
2368
|
+
row.appendChild(label);
|
|
2369
|
+
row.appendChild(makeBtn('suffix'));
|
|
2370
|
+
container.appendChild(row);
|
|
2371
|
+
}
|
|
2372
|
+
rootElement.appendChild(container);
|
|
2373
|
+
};
|
|
2374
|
+
const mapDynamicVariables$1 = (props) => {
|
|
2375
|
+
const { rootElement, changeSchemas, activeSchema } = props;
|
|
2376
|
+
const rtSchema = activeSchema;
|
|
2377
|
+
const text = rtSchema.text ?? '';
|
|
2378
|
+
if (!text) {
|
|
2379
|
+
rootElement.style.display = 'none';
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
const variables = JSON.parse((rtSchema.content && rtSchema.content !== '' ? rtSchema.content : '{}'));
|
|
2383
|
+
const variablesChanged = updateVariablesFromText(text, variables);
|
|
2384
|
+
const varNames = Object.keys(variables);
|
|
2385
|
+
if (variablesChanged) {
|
|
2386
|
+
changeSchemas([
|
|
2387
|
+
{ key: 'content', value: JSON.stringify(variables), schemaId: activeSchema.id },
|
|
2388
|
+
{ key: 'variables', value: varNames, schemaId: activeSchema.id },
|
|
2389
|
+
{ key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
|
|
2390
|
+
]);
|
|
2391
|
+
}
|
|
2392
|
+
rootElement.style.display = 'none';
|
|
2393
|
+
};
|
|
2394
|
+
const propPanel$1 = {
|
|
2395
|
+
schema: (propPanelProps) => {
|
|
2396
|
+
if (typeof propPanel$2.schema !== 'function') {
|
|
2397
|
+
throw new Error('Oops, is text schema no longer a function?');
|
|
2398
|
+
}
|
|
2399
|
+
const parentSchema = typeof propPanel$2.schema === 'function' ? propPanel$2.schema(propPanelProps) : {};
|
|
2400
|
+
return {
|
|
2401
|
+
insertVariablePicker: {
|
|
2402
|
+
type: 'void',
|
|
2403
|
+
widget: 'variablePrefixSuffixWidget',
|
|
2404
|
+
bind: false,
|
|
2405
|
+
span: 24,
|
|
2406
|
+
},
|
|
2407
|
+
'----': { type: 'void', widget: 'Divider' },
|
|
2408
|
+
...parentSchema,
|
|
2409
|
+
dynamicVariables: {
|
|
2410
|
+
type: 'object',
|
|
2411
|
+
widget: 'mapDynamicVariables',
|
|
2412
|
+
bind: false,
|
|
2413
|
+
span: 0,
|
|
2414
|
+
},
|
|
2415
|
+
};
|
|
2416
|
+
},
|
|
2417
|
+
widgets: { ...(propPanel$2.widgets || {}), mapDynamicVariables: mapDynamicVariables$1, variablePrefixSuffixWidget },
|
|
2418
|
+
defaultSchema: {
|
|
2419
|
+
...propPanel$2.defaultSchema,
|
|
2420
|
+
readOnly: false,
|
|
2421
|
+
type: 'richText',
|
|
2422
|
+
text: '',
|
|
2423
|
+
width: 50,
|
|
2424
|
+
height: 7,
|
|
2425
|
+
content: '',
|
|
2426
|
+
variables: [],
|
|
2427
|
+
},
|
|
2428
|
+
};
|
|
2429
|
+
/** Known JS globals/keywords that should NOT be treated as user-defined variables */
|
|
2430
|
+
const RESERVED_NAMES$1 = new Set([
|
|
2431
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
2432
|
+
'void', 'delete', 'new', 'this', 'NaN', 'Infinity',
|
|
2433
|
+
'Math', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'JSON',
|
|
2434
|
+
'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
|
|
2435
|
+
'encodeURI', 'encodeURIComponent', 'date', 'dateTime',
|
|
2436
|
+
'currentPage', 'totalPages',
|
|
2437
|
+
]);
|
|
2438
|
+
const extractDotPaths = (expr) => {
|
|
2439
|
+
const cleaned = expr.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, (m) => ' '.repeat(m.length));
|
|
2440
|
+
const pathRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
|
|
2441
|
+
const paths = new Set();
|
|
2442
|
+
let m;
|
|
2443
|
+
while ((m = pathRegex.exec(cleaned)) !== null) {
|
|
2444
|
+
let path = m[0];
|
|
2445
|
+
const nextChar = cleaned[m.index + path.length];
|
|
2446
|
+
if (nextChar === '(') {
|
|
2447
|
+
const lastDot = path.lastIndexOf('.');
|
|
2448
|
+
if (lastDot !== -1) {
|
|
2449
|
+
path = path.substring(0, lastDot);
|
|
2450
|
+
}
|
|
2451
|
+
else {
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const root = path.split('.')[0];
|
|
2456
|
+
if (!RESERVED_NAMES$1.has(root))
|
|
2457
|
+
paths.add(path);
|
|
2458
|
+
}
|
|
2459
|
+
return Array.from(paths);
|
|
2460
|
+
};
|
|
2461
|
+
const buildNestedDefault = (obj, paths) => {
|
|
2462
|
+
let added = false;
|
|
2463
|
+
for (const path of paths) {
|
|
2464
|
+
const parts = path.split('.');
|
|
2465
|
+
if (parts.length <= 1)
|
|
2466
|
+
continue;
|
|
2467
|
+
let current = obj;
|
|
2468
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
2469
|
+
if (!(parts[i] in current) || typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
|
|
2470
|
+
current[parts[i]] = {};
|
|
2471
|
+
added = true;
|
|
2472
|
+
}
|
|
2473
|
+
current = current[parts[i]];
|
|
2474
|
+
}
|
|
2475
|
+
const leaf = parts[parts.length - 1];
|
|
2476
|
+
const isOldLeafFormat = leaf in current && (/^[A-Z][A-Z0-9_]+$/.test(String(current[leaf])) || /^\{\{.*\}\}$/.test(String(current[leaf])));
|
|
2477
|
+
if (!(leaf in current) || isOldLeafFormat) {
|
|
2478
|
+
current[leaf] = path.replace(/\./g, ' ');
|
|
2479
|
+
added = true;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return added;
|
|
2483
|
+
};
|
|
2484
|
+
const updateVariablesFromText = (text, variables) => {
|
|
2485
|
+
const blockRegex = /\{([^{}]+)\}/g;
|
|
2486
|
+
const allPaths = new Set();
|
|
2487
|
+
let blockMatch;
|
|
2488
|
+
while ((blockMatch = blockRegex.exec(text)) !== null) {
|
|
2489
|
+
for (const path of extractDotPaths(blockMatch[1])) {
|
|
2490
|
+
allPaths.add(path);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
const rootToPaths = new Map();
|
|
2494
|
+
for (const path of allPaths) {
|
|
2495
|
+
const root = path.split('.')[0];
|
|
2496
|
+
if (!rootToPaths.has(root))
|
|
2497
|
+
rootToPaths.set(root, []);
|
|
2498
|
+
rootToPaths.get(root).push(path);
|
|
2499
|
+
}
|
|
2500
|
+
const allRoots = new Set(rootToPaths.keys());
|
|
2501
|
+
let changed = false;
|
|
2502
|
+
for (const [root, paths] of rootToPaths) {
|
|
2503
|
+
const hasNested = paths.some((p) => p.includes('.'));
|
|
2504
|
+
if (hasNested) {
|
|
2505
|
+
let obj = {};
|
|
2506
|
+
if (root in variables) {
|
|
2507
|
+
try {
|
|
2508
|
+
const parsed = JSON.parse(variables[root]);
|
|
2509
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
2510
|
+
obj = parsed;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
catch {
|
|
2514
|
+
/* not JSON, will rebuild */
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const added = buildNestedDefault(obj, paths);
|
|
2518
|
+
if (!(root in variables) || added) {
|
|
2519
|
+
variables[root] = JSON.stringify(obj);
|
|
2520
|
+
changed = true;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
else {
|
|
2524
|
+
const existingVal = variables[root];
|
|
2525
|
+
const isStaleObject = typeof existingVal === 'string' && (() => {
|
|
2526
|
+
try {
|
|
2527
|
+
const p = JSON.parse(existingVal);
|
|
2528
|
+
return typeof p === 'object' && p !== null;
|
|
2529
|
+
}
|
|
2530
|
+
catch {
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
})();
|
|
2534
|
+
const isOldFormat = typeof existingVal === 'string' && (/^[A-Z][A-Z0-9_]*$/.test(existingVal) || /^\{\{.*\}\}$/.test(existingVal));
|
|
2535
|
+
if (!(root in variables) || isStaleObject || isOldFormat) {
|
|
2536
|
+
variables[root] = root;
|
|
2537
|
+
changed = true;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
for (const varName of Object.keys(variables)) {
|
|
2542
|
+
if (!allRoots.has(varName)) {
|
|
2543
|
+
delete variables[varName];
|
|
2544
|
+
changed = true;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return changed;
|
|
2548
|
+
};
|
|
2549
|
+
|
|
2550
|
+
/** Format {expr} → {{ expr with dots as spaces }} for display */
|
|
2551
|
+
const formatTemplateDisplay$1 = (text) => text.replace(/\{([^{}]+)\}/g, (_, expr) => '{{ ' + expr.trim().replace(/\./g, ' ') + ' }}');
|
|
2552
|
+
const uiRender$1 = async (arg) => {
|
|
2527
2553
|
const { value, schema, rootElement, mode, onChange, pageContext, ...rest } = arg;
|
|
2528
2554
|
// Static mode: no template text → delegate to plain text behavior
|
|
2529
2555
|
if (!schema.text) {
|
|
2530
|
-
await uiRender$
|
|
2556
|
+
await uiRender$2({ ...arg, value: value || '' });
|
|
2531
2557
|
return;
|
|
2532
2558
|
}
|
|
2533
2559
|
// Dynamic mode: template with optional variables
|
|
@@ -2537,8 +2563,8 @@ const uiRender = async (arg) => {
|
|
|
2537
2563
|
await formUiRender(arg);
|
|
2538
2564
|
return;
|
|
2539
2565
|
}
|
|
2540
|
-
await uiRender$
|
|
2541
|
-
value: isEditable(mode, schema) ? text :
|
|
2566
|
+
await uiRender$2({
|
|
2567
|
+
value: isEditable(mode, schema) ? text : formatTemplateDisplay$1(text),
|
|
2542
2568
|
schema,
|
|
2543
2569
|
mode: mode === 'form' ? 'viewer' : mode, // if no variables for form it's just a viewer
|
|
2544
2570
|
rootElement,
|
|
@@ -2559,11 +2585,34 @@ const uiRender = async (arg) => {
|
|
|
2559
2585
|
throw new Error('Text block not found. Ensure the text block has an id of "text-" + schema.id');
|
|
2560
2586
|
}
|
|
2561
2587
|
if (mode === 'designer') {
|
|
2562
|
-
|
|
2588
|
+
// Show formatted display initially: {value} → {{ value }}
|
|
2589
|
+
if (text) {
|
|
2590
|
+
textBlock.textContent = formatTemplateDisplay$1(text);
|
|
2591
|
+
}
|
|
2592
|
+
textBlock.addEventListener('focus', () => {
|
|
2593
|
+
// Switch to raw template for editing
|
|
2594
|
+
textBlock.textContent = text;
|
|
2595
|
+
const sel = window.getSelection();
|
|
2596
|
+
const range = document.createRange();
|
|
2597
|
+
range.selectNodeContents(textBlock);
|
|
2598
|
+
range.collapse(false);
|
|
2599
|
+
sel?.removeAllRanges();
|
|
2600
|
+
sel?.addRange(range);
|
|
2601
|
+
});
|
|
2602
|
+
textBlock.addEventListener('blur', () => {
|
|
2563
2603
|
text = textBlock.textContent || '';
|
|
2604
|
+
if (onChange) {
|
|
2605
|
+
onChange({ key: 'text', value: text });
|
|
2606
|
+
}
|
|
2607
|
+
// Show formatted display again
|
|
2608
|
+
textBlock.textContent = formatTemplateDisplay$1(text);
|
|
2609
|
+
});
|
|
2610
|
+
textBlock.addEventListener('keyup', (event) => {
|
|
2611
|
+
const currentText = textBlock.textContent || '';
|
|
2564
2612
|
if (keyPressShouldBeChecked(event)) {
|
|
2565
|
-
const newNumVariables = countUniqueVariableNames(
|
|
2613
|
+
const newNumVariables = countUniqueVariableNames(currentText);
|
|
2566
2614
|
if (numVariables !== newNumVariables) {
|
|
2615
|
+
text = currentText;
|
|
2567
2616
|
if (onChange) {
|
|
2568
2617
|
onChange({ key: 'text', value: text });
|
|
2569
2618
|
}
|
|
@@ -2748,7 +2797,6 @@ const countUniqueVariableNames = (content) => {
|
|
|
2748
2797
|
/**
|
|
2749
2798
|
* An optimisation to try to minimise jank while typing.
|
|
2750
2799
|
* Only check whether variables were modified based on certain key presses.
|
|
2751
|
-
* Regex would otherwise be performed on every key press (which isn't terrible, but this code helps).
|
|
2752
2800
|
*/
|
|
2753
2801
|
const keyPressShouldBeChecked = (event) => {
|
|
2754
2802
|
if (event.key === 'ArrowUp' ||
|
|
@@ -2770,15 +2818,179 @@ const keyPressShouldBeChecked = (event) => {
|
|
|
2770
2818
|
return true;
|
|
2771
2819
|
};
|
|
2772
2820
|
|
|
2821
|
+
const schema$1 = {
|
|
2822
|
+
pdf: pdfRender,
|
|
2823
|
+
ui: uiRender$1,
|
|
2824
|
+
propPanel: propPanel$1,
|
|
2825
|
+
icon: createSvgStr(lucide.FileText),
|
|
2826
|
+
uninterruptedEditMode: true,
|
|
2827
|
+
};
|
|
2828
|
+
|
|
2829
|
+
/** Replaces any existing {var} in the template with {newVar}, or appends if none exists */
|
|
2830
|
+
const replaceOrAppendVariable = (currentText, varName) => {
|
|
2831
|
+
// Replace any existing {identifier} block with the new variable
|
|
2832
|
+
const replaced = currentText.replace(/\{[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\}/g, `{${varName}}`);
|
|
2833
|
+
if (replaced !== currentText)
|
|
2834
|
+
return replaced;
|
|
2835
|
+
// No existing variable — append with a space separator
|
|
2836
|
+
return currentText ? `${currentText} {${varName}}` : `{${varName}}`;
|
|
2837
|
+
};
|
|
2838
|
+
const singleVariablePickerWidget = (props) => {
|
|
2839
|
+
const { rootElement, changeSchemas, activeSchema, options } = props;
|
|
2840
|
+
const schema = activeSchema;
|
|
2841
|
+
const variables = options.variables?.textVariables ?? [];
|
|
2842
|
+
if (variables.length === 0)
|
|
2843
|
+
return;
|
|
2844
|
+
const container = document.createElement('div');
|
|
2845
|
+
container.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:10px;';
|
|
2846
|
+
const label = document.createElement('span');
|
|
2847
|
+
label.textContent = 'Variable:';
|
|
2848
|
+
label.style.cssText = 'font-size:12px;color:#666;white-space:nowrap;';
|
|
2849
|
+
const select = document.createElement('select');
|
|
2850
|
+
select.style.cssText =
|
|
2851
|
+
'flex:1;height:30px;border:1px solid #E0E0E0;border-radius:4px;padding:0 8px;font-size:13px;background:#fff;cursor:pointer;';
|
|
2852
|
+
const blank = document.createElement('option');
|
|
2853
|
+
blank.value = '';
|
|
2854
|
+
blank.textContent = '— pick variable —';
|
|
2855
|
+
select.appendChild(blank);
|
|
2856
|
+
for (const v of variables) {
|
|
2857
|
+
const opt = document.createElement('option');
|
|
2858
|
+
opt.value = v.value;
|
|
2859
|
+
opt.textContent = v.label;
|
|
2860
|
+
select.appendChild(opt);
|
|
2861
|
+
}
|
|
2862
|
+
select.onchange = (e) => {
|
|
2863
|
+
const varName = e.target.value;
|
|
2864
|
+
if (!varName)
|
|
2865
|
+
return;
|
|
2866
|
+
const current = String(schema.text ?? '');
|
|
2867
|
+
const next = replaceOrAppendVariable(current, varName);
|
|
2868
|
+
changeSchemas([{ key: 'text', value: next, schemaId: activeSchema.id }]);
|
|
2869
|
+
select.value = '';
|
|
2870
|
+
};
|
|
2871
|
+
container.appendChild(label);
|
|
2872
|
+
container.appendChild(select);
|
|
2873
|
+
rootElement.appendChild(container);
|
|
2874
|
+
};
|
|
2875
|
+
const mapDynamicVariables = (props) => {
|
|
2876
|
+
const { rootElement, changeSchemas, activeSchema } = props;
|
|
2877
|
+
const svtSchema = activeSchema;
|
|
2878
|
+
const text = svtSchema.text ?? '';
|
|
2879
|
+
if (!text) {
|
|
2880
|
+
rootElement.style.display = 'none';
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
// Extract all simple variable references from the template
|
|
2884
|
+
const varMatches = [...text.matchAll(/\{([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\}/g)].map(m => m[1]);
|
|
2885
|
+
// Keep only the first unique variable (enforce single-variable restriction)
|
|
2886
|
+
const uniqueVars = [...new Set(varMatches)];
|
|
2887
|
+
const variable = uniqueVars[0];
|
|
2888
|
+
const varNames = variable ? [variable] : [];
|
|
2889
|
+
const variables = {};
|
|
2890
|
+
if (variable) {
|
|
2891
|
+
const existing = JSON.parse(svtSchema.content && svtSchema.content !== '' ? svtSchema.content : '{}');
|
|
2892
|
+
const existingVal = existing[variable];
|
|
2893
|
+
const isOldFormat = typeof existingVal === 'string' && (/^[A-Z][A-Z0-9_]*$/.test(existingVal) || /^\{\{.*\}\}$/.test(existingVal));
|
|
2894
|
+
const isStaleObject = typeof existingVal === 'string' && (() => {
|
|
2895
|
+
try {
|
|
2896
|
+
const p = JSON.parse(existingVal);
|
|
2897
|
+
return typeof p === 'object' && p !== null;
|
|
2898
|
+
}
|
|
2899
|
+
catch {
|
|
2900
|
+
return false;
|
|
2901
|
+
}
|
|
2902
|
+
})();
|
|
2903
|
+
variables[variable] = (!existingVal || isOldFormat || isStaleObject) ? variable.replace(/\./g, ' ') : existingVal;
|
|
2904
|
+
}
|
|
2905
|
+
const content = varNames.length > 0 ? JSON.stringify(variables) : '';
|
|
2906
|
+
const changed = JSON.stringify(svtSchema.variables ?? []) !== JSON.stringify(varNames) ||
|
|
2907
|
+
(svtSchema.content ?? '') !== content;
|
|
2908
|
+
if (changed) {
|
|
2909
|
+
changeSchemas([
|
|
2910
|
+
{ key: 'content', value: content, schemaId: activeSchema.id },
|
|
2911
|
+
{ key: 'variables', value: varNames, schemaId: activeSchema.id },
|
|
2912
|
+
{ key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
|
|
2913
|
+
]);
|
|
2914
|
+
}
|
|
2915
|
+
rootElement.style.display = 'none';
|
|
2916
|
+
};
|
|
2917
|
+
const propPanel = {
|
|
2918
|
+
schema: (propPanelProps) => {
|
|
2919
|
+
if (typeof propPanel$1.schema !== 'function') {
|
|
2920
|
+
throw new Error('richText propPanel schema is not a function');
|
|
2921
|
+
}
|
|
2922
|
+
const parentSchema = propPanel$1.schema(propPanelProps);
|
|
2923
|
+
// Strip out richText-specific structural keys; we'll re-add our own versions
|
|
2924
|
+
const { insertVariablePicker: _ip, '----': _div, dynamicVariables: _dv, ...textFields } = parentSchema;
|
|
2925
|
+
return {
|
|
2926
|
+
insertVariablePicker: {
|
|
2927
|
+
type: 'void',
|
|
2928
|
+
widget: 'singleVariablePickerWidget',
|
|
2929
|
+
bind: false,
|
|
2930
|
+
span: 24,
|
|
2931
|
+
},
|
|
2932
|
+
'----': { type: 'void', widget: 'Divider' },
|
|
2933
|
+
...textFields,
|
|
2934
|
+
dynamicVariables: {
|
|
2935
|
+
type: 'object',
|
|
2936
|
+
widget: 'mapDynamicVariables',
|
|
2937
|
+
bind: false,
|
|
2938
|
+
span: 0,
|
|
2939
|
+
},
|
|
2940
|
+
};
|
|
2941
|
+
},
|
|
2942
|
+
widgets: {
|
|
2943
|
+
...(propPanel$1.widgets || {}),
|
|
2944
|
+
singleVariablePickerWidget,
|
|
2945
|
+
mapDynamicVariables,
|
|
2946
|
+
},
|
|
2947
|
+
defaultSchema: {
|
|
2948
|
+
...propPanel$1.defaultSchema,
|
|
2949
|
+
type: 'singleVariableText',
|
|
2950
|
+
text: '',
|
|
2951
|
+
content: '',
|
|
2952
|
+
height: 7,
|
|
2953
|
+
variables: [],
|
|
2954
|
+
},
|
|
2955
|
+
};
|
|
2956
|
+
|
|
2957
|
+
/** Format {expr} → {{ expr with dots as spaces }} */
|
|
2958
|
+
const formatTemplateDisplay = (text) => text.replace(/\{([^{}]+)\}/g, (_, expr) => '{{ ' + expr.trim().replace(/\./g, ' ') + ' }}');
|
|
2959
|
+
const uiRender = async (arg) => {
|
|
2960
|
+
const { mode, schema, options, _cache } = arg;
|
|
2961
|
+
if (mode === 'designer') {
|
|
2962
|
+
// ReadOnly preview — show {{ variable }} formatted display
|
|
2963
|
+
const text = schema.text ?? '';
|
|
2964
|
+
const displayText = text ? formatTemplateDisplay(text) : '';
|
|
2965
|
+
const font = options?.font || helper.getDefaultFont();
|
|
2966
|
+
const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
|
|
2967
|
+
const textBlock = buildStyledTextContainer(arg, fontKitFont, displayText);
|
|
2968
|
+
textBlock.textContent = displayText;
|
|
2969
|
+
textBlock.style.cursor = 'default';
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
// Viewer/form modes: delegate to richText render
|
|
2973
|
+
await uiRender$1(arg);
|
|
2974
|
+
};
|
|
2773
2975
|
const schema = {
|
|
2774
2976
|
pdf: pdfRender,
|
|
2775
2977
|
ui: uiRender,
|
|
2776
2978
|
propPanel,
|
|
2777
|
-
icon: createSvgStr(lucide.
|
|
2778
|
-
uninterruptedEditMode: true,
|
|
2979
|
+
icon: createSvgStr(lucide.BetweenHorizontalStart),
|
|
2779
2980
|
};
|
|
2780
2981
|
|
|
2781
|
-
const
|
|
2982
|
+
const textSchema = {
|
|
2983
|
+
pdf: pdfRender$1,
|
|
2984
|
+
ui: uiRender$2,
|
|
2985
|
+
propPanel: propPanel$2,
|
|
2986
|
+
icon: createSvgStr(lucide.TextCursorInput),
|
|
2987
|
+
};
|
|
2988
|
+
|
|
2989
|
+
const builtInPlugins = {
|
|
2990
|
+
Text: textSchema,
|
|
2991
|
+
'Dynamic Text': schema,
|
|
2992
|
+
'Rich Text': schema$1,
|
|
2993
|
+
};
|
|
2782
2994
|
|
|
2783
2995
|
exports.DEFAULT_ALIGNMENT = DEFAULT_ALIGNMENT;
|
|
2784
2996
|
exports.DEFAULT_CHARACTER_SPACING = DEFAULT_CHARACTER_SPACING;
|
|
@@ -2789,10 +3001,10 @@ exports.DEFAULT_OPACITY = DEFAULT_OPACITY;
|
|
|
2789
3001
|
exports.HEX_COLOR_PATTERN = HEX_COLOR_PATTERN;
|
|
2790
3002
|
exports.VERTICAL_ALIGN_MIDDLE = VERTICAL_ALIGN_MIDDLE;
|
|
2791
3003
|
exports.addAlphaToHex = addAlphaToHex;
|
|
3004
|
+
exports.buildStyledTextContainer = buildStyledTextContainer;
|
|
2792
3005
|
exports.builtInPlugins = builtInPlugins;
|
|
2793
3006
|
exports.convertForPdfLayoutProps = convertForPdfLayoutProps;
|
|
2794
3007
|
exports.createErrorElm = createErrorElm;
|
|
2795
|
-
exports.createInsertVariableWidget = createInsertVariableWidget;
|
|
2796
3008
|
exports.createSingleTable = createSingleTable;
|
|
2797
3009
|
exports.createSvgStr = createSvgStr;
|
|
2798
3010
|
exports.getBody = getBody;
|
|
@@ -2802,13 +3014,16 @@ exports.getColumnStylesPropPanelSchema = getColumnStylesPropPanelSchema;
|
|
|
2802
3014
|
exports.getDefaultCellStyles = getDefaultCellStyles;
|
|
2803
3015
|
exports.getDynamicHeightsForTable = getDynamicHeightsForTable;
|
|
2804
3016
|
exports.getExtraFormatterSchema = getExtraFormatterSchema;
|
|
3017
|
+
exports.getFontKitFont = getFontKitFont;
|
|
2805
3018
|
exports.hex2PrintingColor = hex2PrintingColor;
|
|
2806
3019
|
exports.isEditable = isEditable;
|
|
3020
|
+
exports.makeElementPlainTextContentEditable = makeElementPlainTextContentEditable;
|
|
2807
3021
|
exports.mapVerticalAlignToFlex = mapVerticalAlignToFlex;
|
|
2808
3022
|
exports.pdfRender = pdfRender$1;
|
|
2809
|
-
exports.propPanel = propPanel$
|
|
3023
|
+
exports.propPanel = propPanel$2;
|
|
2810
3024
|
exports.readFile = readFile;
|
|
2811
3025
|
exports.rotatePoint = rotatePoint;
|
|
2812
|
-
exports.schema = schema;
|
|
2813
|
-
exports.
|
|
2814
|
-
exports.
|
|
3026
|
+
exports.schema = schema$1;
|
|
3027
|
+
exports.schema$1 = schema;
|
|
3028
|
+
exports.textSchema = textSchema;
|
|
3029
|
+
exports.uiRender = uiRender$2;
|