@pdfme/schemas 4.1.1 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/cjs/src/constants.js +1 -1
  2. package/dist/cjs/src/constants.js.map +1 -1
  3. package/dist/cjs/src/index.js +11 -9
  4. package/dist/cjs/src/index.js.map +1 -1
  5. package/dist/cjs/src/multiVariableText/helper.js +19 -0
  6. package/dist/cjs/src/multiVariableText/helper.js.map +1 -0
  7. package/dist/cjs/src/multiVariableText/index.js +8 -0
  8. package/dist/cjs/src/multiVariableText/index.js.map +1 -0
  9. package/dist/cjs/src/multiVariableText/pdfRender.js +16 -0
  10. package/dist/cjs/src/multiVariableText/pdfRender.js.map +1 -0
  11. package/dist/cjs/src/multiVariableText/propPanel.js +128 -0
  12. package/dist/cjs/src/multiVariableText/propPanel.js.map +1 -0
  13. package/dist/cjs/src/multiVariableText/types.js +3 -0
  14. package/dist/cjs/src/multiVariableText/types.js.map +1 -0
  15. package/dist/cjs/src/multiVariableText/uiRender.js +133 -0
  16. package/dist/cjs/src/multiVariableText/uiRender.js.map +1 -0
  17. package/dist/cjs/src/text/uiRender.js +122 -103
  18. package/dist/cjs/src/text/uiRender.js.map +1 -1
  19. package/dist/esm/src/constants.js +1 -1
  20. package/dist/esm/src/constants.js.map +1 -1
  21. package/dist/esm/src/index.js +2 -1
  22. package/dist/esm/src/index.js.map +1 -1
  23. package/dist/esm/src/multiVariableText/helper.js +15 -0
  24. package/dist/esm/src/multiVariableText/helper.js.map +1 -0
  25. package/dist/esm/src/multiVariableText/index.js +6 -0
  26. package/dist/esm/src/multiVariableText/index.js.map +1 -0
  27. package/dist/esm/src/multiVariableText/pdfRender.js +12 -0
  28. package/dist/esm/src/multiVariableText/pdfRender.js.map +1 -0
  29. package/dist/esm/src/multiVariableText/propPanel.js +125 -0
  30. package/dist/esm/src/multiVariableText/propPanel.js.map +1 -0
  31. package/dist/esm/src/multiVariableText/types.js +2 -0
  32. package/dist/esm/src/multiVariableText/types.js.map +1 -0
  33. package/dist/esm/src/multiVariableText/uiRender.js +129 -0
  34. package/dist/esm/src/multiVariableText/uiRender.js.map +1 -0
  35. package/dist/esm/src/text/uiRender.js +118 -101
  36. package/dist/esm/src/text/uiRender.js.map +1 -1
  37. package/dist/types/src/constants.d.ts +1 -1
  38. package/dist/types/src/index.d.ts +2 -1
  39. package/dist/types/src/multiVariableText/helper.d.ts +1 -0
  40. package/dist/types/src/multiVariableText/index.d.ts +4 -0
  41. package/dist/types/src/multiVariableText/pdfRender.d.ts +3 -0
  42. package/dist/types/src/multiVariableText/propPanel.d.ts +3 -0
  43. package/dist/types/src/multiVariableText/types.d.ts +5 -0
  44. package/dist/types/src/multiVariableText/uiRender.d.ts +3 -0
  45. package/dist/types/src/shapes/rectAndEllipse.d.ts +2 -0
  46. package/dist/types/src/text/uiRender.d.ts +6 -0
  47. package/package.json +1 -1
  48. package/src/constants.ts +1 -1
  49. package/src/index.ts +2 -0
  50. package/src/multiVariableText/helper.ts +18 -0
  51. package/src/multiVariableText/index.ts +8 -0
  52. package/src/multiVariableText/pdfRender.ts +16 -0
  53. package/src/multiVariableText/propPanel.ts +139 -0
  54. package/src/multiVariableText/types.ts +6 -0
  55. package/src/multiVariableText/uiRender.ts +161 -0
  56. package/src/text/uiRender.ts +150 -118
@@ -0,0 +1,161 @@
1
+ import { UIRenderProps } from '@pdfme/common';
2
+ import { MultiVariableTextSchema } from './types';
3
+ import {
4
+ uiRender as parentUiRender,
5
+ buildStyledTextContainer,
6
+ makeElementPlainTextContentEditable
7
+ } from '../text/uiRender';
8
+ import { isEditable } from '../utils';
9
+ import { substituteVariables } from './helper';
10
+
11
+ export const uiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
12
+ const { value, schema, rootElement, mode, onChange, ...rest } = arg;
13
+
14
+ let text = schema.text;
15
+ let numVariables = schema.variables.length;
16
+
17
+ if (mode === 'form' && numVariables > 0) {
18
+ await formUiRender(arg);
19
+ return;
20
+ }
21
+
22
+ await parentUiRender({
23
+ value: isEditable(mode, schema) ? text : substituteVariables(text, value),
24
+ schema,
25
+ mode: mode == 'form' ? 'viewer' : mode, // if no variables for form it's just a viewer
26
+ rootElement,
27
+ onChange: (arg: { key: string; value: any; } | { key: string; value: any; }[]) => {
28
+ if (!Array.isArray(arg)) {
29
+ onChange && onChange({key: 'text', value: arg.value});
30
+ } else {
31
+ throw new Error('onChange is not an array, the parent text plugin has changed...');
32
+ }
33
+ },
34
+ ...rest,
35
+ });
36
+
37
+ const textBlock = rootElement.querySelector('#text-' + schema.id) as HTMLDivElement;
38
+ if (!textBlock) {
39
+ throw new Error('Text block not found. Ensure the text block has an id of "text-" + schema.id');
40
+ }
41
+
42
+ if (mode === 'designer') {
43
+ textBlock.addEventListener('keyup', (event: KeyboardEvent) => {
44
+ text = textBlock.textContent || '';
45
+ if (keyPressShouldBeChecked(event)) {
46
+ const newNumVariables = countUniqueVariableNames(text);
47
+ if (numVariables !== newNumVariables) {
48
+ // If variables were modified during this keypress, we trigger a change
49
+ if (onChange) {
50
+ onChange({key: 'text', value: text});
51
+ }
52
+ numVariables = newNumVariables;
53
+ }
54
+ }
55
+ });
56
+ }
57
+ };
58
+
59
+ const formUiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
60
+ const {
61
+ value,
62
+ schema,
63
+ rootElement,
64
+ onChange,
65
+ stopEditing,
66
+ theme,
67
+ } = arg;
68
+ const rawText = schema.text;
69
+
70
+ if (rootElement.parentElement) {
71
+ // remove the outline for the whole schema, we'll apply outlines on each individual variable field instead
72
+ rootElement.parentElement.style.outline = '';
73
+ }
74
+
75
+ const variables: Record<string, string> = JSON.parse(value) || {}
76
+ const variableIndices = getVariableIndices(rawText);
77
+ const substitutedText = substituteVariables(rawText, variables);
78
+
79
+ const textBlock = await buildStyledTextContainer(arg, substitutedText);
80
+
81
+ // Construct content-editable spans for each variable within the string
82
+ let inVarString = false;
83
+
84
+ for (let i = 0; i < rawText.length; i++) {
85
+ if (variableIndices[i]) {
86
+ inVarString = true;
87
+ let span = document.createElement('span');
88
+ span.style.outline = `${theme.colorPrimary} dashed 1px`;
89
+ makeElementPlainTextContentEditable(span)
90
+ span.textContent = variables[variableIndices[i]];
91
+ span.addEventListener('blur', (e: Event) => {
92
+ const newValue = (e.target as HTMLSpanElement).innerText;
93
+ if (newValue !== variables[variableIndices[i]]) {
94
+ variables[variableIndices[i]] = newValue;
95
+ onChange && onChange({ key: 'content', value: JSON.stringify(variables) });
96
+ stopEditing && stopEditing();
97
+ }
98
+ });
99
+ textBlock.appendChild(span);
100
+ } else if (inVarString) {
101
+ if (rawText[i] === '}') {
102
+ inVarString = false;
103
+ }
104
+ } else {
105
+ let span = document.createElement('span');
106
+ span.style.letterSpacing = rawText.length === i + 1 ? '0' : 'inherit';
107
+ span.textContent = rawText[i];
108
+ textBlock.appendChild(span);
109
+ }
110
+ }
111
+ }
112
+
113
+ const getVariableIndices = (content: string) => {
114
+ const regex = /\{([^}]+)}/g;
115
+ const indices = [];
116
+ let match;
117
+
118
+ while ((match = regex.exec(content)) !== null) {
119
+ indices[match.index] = match[1];
120
+ }
121
+
122
+ return indices;
123
+ };
124
+
125
+ const countUniqueVariableNames = (content: string) => {
126
+ const regex = /\{([^}]+)}/g;
127
+ const uniqueMatchesSet = new Set();
128
+ let match;
129
+
130
+ while ((match = regex.exec(content)) !== null) {
131
+ uniqueMatchesSet.add(match[1]);
132
+ }
133
+
134
+ return uniqueMatchesSet.size;
135
+ };
136
+
137
+ /**
138
+ * An optimisation to try to minimise jank while typing.
139
+ * Only check whether variables were modified based on certain key presses.
140
+ * Regex would otherwise be performed on every key press (which isn't terrible, but this code helps).
141
+ */
142
+ const keyPressShouldBeChecked = (event: KeyboardEvent) => {
143
+ if (event.key == "ArrowUp" || event.key == "ArrowDown" || event.key == "ArrowLeft" || event.key == "ArrowRight") {
144
+ return false;
145
+ }
146
+
147
+ const selection = window.getSelection();
148
+ const contenteditable = event.target as HTMLDivElement;
149
+
150
+ const isCursorAtEnd = selection?.focusOffset === contenteditable?.textContent?.length;
151
+ if (isCursorAtEnd) {
152
+ return event.key === '}' || event.key === 'Backspace' || event.key === 'Delete';
153
+ }
154
+
155
+ const isCursorAtStart = selection?.anchorOffset === 0;
156
+ if (isCursorAtStart) {
157
+ return event.key === '{' || event.key === 'Backspace' || event.key === 'Delete';
158
+ }
159
+
160
+ return true;
161
+ }
@@ -1,5 +1,5 @@
1
1
  import type * as CSS from 'csstype';
2
- import { UIRenderProps, Schema, getDefaultFont } from '@pdfme/common';
2
+ import { UIRenderProps, getDefaultFont } from '@pdfme/common';
3
3
  import type { TextSchema } from './types';
4
4
  import {
5
5
  DEFAULT_FONT_SIZE,
@@ -21,28 +21,10 @@ import {
21
21
  } from './helper.js';
22
22
  import { isEditable } from '../utils.js';
23
23
 
24
- const mapVerticalAlignToFlex = (verticalAlignmentValue: string | undefined) => {
25
- switch (verticalAlignmentValue) {
26
- case VERTICAL_ALIGN_TOP:
27
- return 'flex-start';
28
- case VERTICAL_ALIGN_MIDDLE:
29
- return 'center';
30
- case VERTICAL_ALIGN_BOTTOM:
31
- return 'flex-end';
32
- }
33
- return 'flex-start';
34
- };
35
-
36
- const getBackgroundColor = (value: string, schema: Schema) => {
37
- if (!value || !schema.backgroundColor) return 'transparent';
38
- return schema.backgroundColor as string;
39
- };
40
-
41
24
  export const uiRender = async (arg: UIRenderProps<TextSchema>) => {
42
25
  const {
43
26
  value,
44
27
  schema,
45
- rootElement,
46
28
  mode,
47
29
  onChange,
48
30
  stopEditing,
@@ -51,28 +33,120 @@ export const uiRender = async (arg: UIRenderProps<TextSchema>) => {
51
33
  options,
52
34
  _cache,
53
35
  } = arg;
36
+ const usePlaceholder = isEditable(mode, schema) && placeholder && !value;
37
+
38
+ const textBlock = await buildStyledTextContainer(arg, usePlaceholder ? placeholder : value);
39
+
40
+ if (!isEditable(mode, schema)) {
41
+ // Read-only mode
42
+ textBlock.innerHTML = value
43
+ .split('')
44
+ .map(
45
+ (l: string, i: number) =>
46
+ `<span style="letter-spacing:${
47
+ String(value).length === i + 1 ? 0 : 'inherit'
48
+ };">${l}</span>`
49
+ )
50
+ .join('');
51
+ return;
52
+ }
53
+
54
+ makeElementPlainTextContentEditable(textBlock);
55
+ textBlock.tabIndex = tabIndex || 0;
56
+ textBlock.innerText = value;
57
+ textBlock.addEventListener('blur', (e: Event) => {
58
+ onChange && onChange({ key: 'content', value: (e.target as HTMLDivElement).innerText });
59
+ stopEditing && stopEditing();
60
+ });
61
+
62
+ if (schema.dynamicFontSize) {
63
+ let dynamicFontSize: undefined | number = undefined;
64
+ const font = options?.font || getDefaultFont();
65
+ const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
66
+
67
+ textBlock.addEventListener('keyup', () => {
68
+ setTimeout(() => {
69
+ void (async () => {
70
+ if (!textBlock.textContent) return;
71
+ dynamicFontSize = await calculateDynamicFontSize({
72
+ textSchema: schema,
73
+ font,
74
+ value: textBlock.textContent,
75
+ startingFontSize: dynamicFontSize,
76
+ _cache,
77
+ });
78
+ textBlock.style.fontSize = `${dynamicFontSize}pt`;
79
+
80
+ const { topAdj: newTopAdj, bottomAdj: newBottomAdj } =
81
+ getBrowserVerticalFontAdjustments(
82
+ fontKitFont,
83
+ dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE,
84
+ schema.lineHeight ?? DEFAULT_LINE_HEIGHT,
85
+ schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT
86
+ );
87
+ textBlock.style.paddingTop = `${newTopAdj}px`;
88
+ textBlock.style.marginBottom = `${newBottomAdj}px`;
89
+ })();
90
+ }, 0);
91
+ });
92
+ }
93
+
94
+ if (usePlaceholder) {
95
+ textBlock.style.color = PLACEHOLDER_FONT_COLOR;
96
+ textBlock.addEventListener('focus', () => {
97
+ if (textBlock.innerText === placeholder) {
98
+ textBlock.innerText = '';
99
+ textBlock.style.color = schema.fontColor ?? DEFAULT_FONT_COLOR;
100
+ }
101
+ });
102
+ }
103
+
104
+ if (mode === 'designer') {
105
+ setTimeout(() => {
106
+ textBlock.focus();
107
+ // Set the focus to the end of the editable element when you focus, as we would for a textarea
108
+ const selection = window.getSelection();
109
+ const range = document.createRange();
110
+ if (selection && range) {
111
+ range.selectNodeContents(textBlock);
112
+ range.collapse(false); // Collapse range to the end
113
+ selection?.removeAllRanges();
114
+ selection?.addRange(range);
115
+ }
116
+ });
117
+ }
118
+ };
119
+
120
+ export const buildStyledTextContainer = async (arg: UIRenderProps<TextSchema>, value: string) => {
121
+ const {
122
+ schema,
123
+ rootElement,
124
+ mode,
125
+ options,
126
+ _cache,
127
+ } = arg;
54
128
  const font = options?.font || getDefaultFont();
55
129
 
56
130
  let dynamicFontSize: undefined | number = undefined;
57
- const getCdfArg = (v: string) => ({
58
- textSchema: schema,
59
- font,
60
- value: v,
61
- startingFontSize: dynamicFontSize,
62
- _cache,
63
- });
131
+
64
132
  if (schema.dynamicFontSize && value) {
65
- dynamicFontSize = await calculateDynamicFontSize(getCdfArg(value));
133
+ dynamicFontSize = await calculateDynamicFontSize({
134
+ textSchema: schema,
135
+ font,
136
+ value,
137
+ startingFontSize: dynamicFontSize,
138
+ _cache,
139
+ });
66
140
  }
67
141
 
68
142
  const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
69
143
  // Depending on vertical alignment, we need to move the top or bottom of the font to keep
70
144
  // it within it's defined box and align it with the generated pdf.
71
145
  const { topAdj, bottomAdj } = getBrowserVerticalFontAdjustments(
72
- fontKitFont,
73
- dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE,
74
- schema.lineHeight ?? DEFAULT_LINE_HEIGHT,
75
- schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT
146
+ fontKitFont,
147
+ dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE,
148
+ schema.lineHeight ?? DEFAULT_LINE_HEIGHT,
149
+ schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT
76
150
  );
77
151
 
78
152
  const topAdjustment = topAdj.toString();
@@ -120,100 +194,58 @@ export const uiRender = async (arg: UIRenderProps<TextSchema>) => {
120
194
  backgroundColor: 'transparent',
121
195
  textDecoration: textDecorations.join(' '),
122
196
  };
197
+
123
198
  const textBlock = document.createElement('div');
199
+ textBlock.id = 'text-' + schema.id;
124
200
  Object.assign(textBlock.style, textBlockStyle);
125
201
 
126
- if (isEditable(mode, schema)) {
127
- if (!isFirefox()) {
128
- textBlock.contentEditable = 'plaintext-only';
129
- } else {
130
- textBlock.contentEditable = 'true';
131
- textBlock.addEventListener('keydown', (e: KeyboardEvent) => {
132
- if (e.key === 'Enter' && !e.shiftKey) {
133
- e.preventDefault();
134
- document.execCommand('insertLineBreak', false, undefined);
135
- }
136
- });
137
-
138
- textBlock.addEventListener('paste', (e: ClipboardEvent) => {
139
- e.preventDefault();
140
- const paste = e.clipboardData?.getData('text');
141
- const selection = window.getSelection();
142
- if (!selection?.rangeCount) return;
143
- selection.deleteFromDocument();
144
- selection.getRangeAt(0).insertNode(document.createTextNode(paste || ''));
145
- selection.collapseToEnd();
146
- });
147
- }
148
- textBlock.tabIndex = tabIndex || 0;
149
- textBlock.innerText = value;
150
- textBlock.addEventListener('blur', (e: Event) => {
151
- onChange && onChange({ key: 'content', value: (e.target as HTMLDivElement).innerText });
152
- stopEditing && stopEditing();
153
- });
202
+ container.appendChild(textBlock);
154
203
 
155
- if (schema.dynamicFontSize) {
156
- textBlock.addEventListener('keyup', () => {
157
- setTimeout(() => {
158
- void (async () => {
159
- if (!textBlock.textContent) return;
160
- dynamicFontSize = await calculateDynamicFontSize(getCdfArg(textBlock.textContent));
161
- textBlock.style.fontSize = `${dynamicFontSize}pt`;
162
-
163
- const { topAdj: newTopAdj, bottomAdj: newBottomAdj } =
164
- getBrowserVerticalFontAdjustments(
165
- fontKitFont,
166
- dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE,
167
- schema.lineHeight ?? DEFAULT_LINE_HEIGHT,
168
- schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT
169
- );
170
- textBlock.style.paddingTop = `${newTopAdj}px`;
171
- textBlock.style.marginBottom = `${newBottomAdj}px`;
172
- })();
173
- }, 0);
174
- });
175
- }
204
+ return textBlock;
205
+ };
176
206
 
177
- if (placeholder && !value) {
178
- textBlock.innerText = placeholder;
179
- textBlock.style.color = PLACEHOLDER_FONT_COLOR;
180
- if (schema.dynamicFontSize) {
181
- const fontSize = await calculateDynamicFontSize(getCdfArg(placeholder));
182
- textBlock.style.fontSize = `${fontSize}pt`;
183
- }
184
- textBlock.addEventListener('focus', () => {
185
- if (textBlock.innerText === placeholder) {
186
- textBlock.innerText = '';
187
- textBlock.style.color = schema.fontColor ?? DEFAULT_FONT_COLOR;
188
- }
189
- });
207
+ /**
208
+ * Firefox doesn't support 'plaintext-only' contentEditable mode, which we want to avoid mark-up.
209
+ * This function adds a workaround for Firefox to make the contentEditable element behave like 'plaintext-only'.
210
+ */
211
+ export const makeElementPlainTextContentEditable = (element: HTMLElement) => {
212
+ if (!isFirefox()) {
213
+ element.contentEditable = 'plaintext-only';
214
+ return;
215
+ }
216
+
217
+ element.contentEditable = 'true';
218
+ element.addEventListener('keydown', (e: KeyboardEvent) => {
219
+ if (e.key === 'Enter' && !e.shiftKey) {
220
+ e.preventDefault();
221
+ document.execCommand('insertLineBreak', false, undefined);
190
222
  }
223
+ });
191
224
 
192
- container.appendChild(textBlock);
225
+ element.addEventListener('paste', (e: ClipboardEvent) => {
226
+ e.preventDefault();
227
+ const paste = e.clipboardData?.getData('text');
228
+ const selection = window.getSelection();
229
+ if (!selection?.rangeCount) return;
230
+ selection.deleteFromDocument();
231
+ selection.getRangeAt(0).insertNode(document.createTextNode(paste || ''));
232
+ selection.collapseToEnd();
233
+ });
234
+ }
193
235
 
194
- if (mode === 'designer') {
195
- setTimeout(() => {
196
- textBlock.focus();
197
- // Set the focus to the end of the editable element when you focus, as we would for a textarea
198
- const selection = window.getSelection();
199
- const range = document.createRange();
200
- range.selectNodeContents(textBlock);
201
- range.collapse(false); // Collapse range to the end
202
- selection?.removeAllRanges();
203
- selection?.addRange(range);
204
- });
205
- }
206
- } else {
207
- textBlock.innerHTML = value
208
- .split('')
209
- .map(
210
- (l: string, i: number) =>
211
- `<span style="letter-spacing:${
212
- String(value).length === i + 1 ? 0 : 'inherit'
213
- };">${l}</span>`
214
- )
215
- .join('');
216
-
217
- container.appendChild(textBlock);
236
+ const mapVerticalAlignToFlex = (verticalAlignmentValue: string | undefined) => {
237
+ switch (verticalAlignmentValue) {
238
+ case VERTICAL_ALIGN_TOP:
239
+ return 'flex-start';
240
+ case VERTICAL_ALIGN_MIDDLE:
241
+ return 'center';
242
+ case VERTICAL_ALIGN_BOTTOM:
243
+ return 'flex-end';
218
244
  }
245
+ return 'flex-start';
246
+ };
247
+
248
+ const getBackgroundColor = (value: string, schema: TextSchema) => {
249
+ if (!value || !schema.backgroundColor) return 'transparent';
250
+ return schema.backgroundColor as string;
219
251
  };