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