@pdfme/schemas
Version:
TypeScript base PDF generator and React base UI. Open source, developed by the community, and completely free to use under the MIT license!
171 lines (147 loc) • 5.6 kB
text/typescript
import { getDefaultFont, UIRenderProps } from '@pdfme/common';
import { MultiVariableTextSchema } from './types.js';
import {
uiRender as parentUiRender,
buildStyledTextContainer,
makeElementPlainTextContentEditable,
} from '../text/uiRender.js';
import { isEditable } from '../utils.js';
import { getFontKitFont } from '../text/helper.js';
import { substituteVariables } from './helper.js';
export const uiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
const { value, schema, rootElement, mode, onChange, ...rest } = arg;
let text = schema.text;
let numVariables = schema.variables.length;
if (mode === 'form' && numVariables > 0) {
await formUiRender(arg);
return;
}
await parentUiRender({
value: isEditable(mode, schema) ? text : substituteVariables(text, value),
schema,
mode: mode === 'form' ? 'viewer' : mode, // if no variables for form it's just a viewer
rootElement,
onChange: (arg: { key: string; value: unknown } | { key: string; value: unknown }[]) => {
if (!Array.isArray(arg)) {
if (onChange) {
onChange({ key: 'text', value: arg.value });
}
} else {
throw new Error('onChange is not an array, the parent text plugin has changed...');
}
},
...rest,
});
const textBlock = rootElement.querySelector('#text-' + String(schema.id)) as HTMLDivElement;
if (!textBlock) {
throw new Error('Text block not found. Ensure the text block has an id of "text-" + schema.id');
}
if (mode === 'designer') {
textBlock.addEventListener('keyup', (event: KeyboardEvent) => {
text = textBlock.textContent || '';
if (keyPressShouldBeChecked(event)) {
const newNumVariables = countUniqueVariableNames(text);
if (numVariables !== newNumVariables) {
// If variables were modified during this keypress, we trigger a change
if (onChange) {
onChange({ key: 'text', value: text });
}
numVariables = newNumVariables;
}
}
});
}
};
const formUiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
const { value, schema, rootElement, onChange, stopEditing, theme, _cache, options } = arg;
const rawText = schema.text;
if (rootElement.parentElement) {
// remove the outline for the whole schema, we'll apply outlines on each individual variable field instead
rootElement.parentElement.style.outline = '';
}
const variables: Record<string, string> = value
? (JSON.parse(value) as Record<string, string>) || {}
: {};
const variableIndices = getVariableIndices(rawText);
const substitutedText = substituteVariables(rawText, variables);
const font = options?.font || getDefaultFont();
const fontKitFont = await getFontKitFont(
schema.fontName,
font,
_cache as Map<string, import('fontkit').Font>,
);
const textBlock = buildStyledTextContainer(arg, fontKitFont, substitutedText);
// Construct content-editable spans for each variable within the string
let inVarString = false;
for (let i = 0; i < rawText.length; i++) {
if (variableIndices[i]) {
inVarString = true;
let span = document.createElement('span');
span.style.outline = `${theme.colorPrimary} dashed 1px`;
makeElementPlainTextContentEditable(span);
span.textContent = variables[variableIndices[i]];
span.addEventListener('blur', (e: Event) => {
const newValue = (e.target as HTMLSpanElement).textContent || '';
if (newValue !== variables[variableIndices[i]]) {
variables[variableIndices[i]] = newValue;
if (onChange) onChange({ key: 'content', value: JSON.stringify(variables) });
if (stopEditing) stopEditing();
}
});
textBlock.appendChild(span);
} else if (inVarString) {
if (rawText[i] === '}') {
inVarString = false;
}
} else {
let span = document.createElement('span');
span.style.letterSpacing = rawText.length === i + 1 ? '0' : 'inherit';
span.textContent = rawText[i];
textBlock.appendChild(span);
}
}
};
const getVariableIndices = (content: string) => {
const regex = /\{([^}]+)}/g;
const indices = [];
let match;
while ((match = regex.exec(content)) !== null) {
indices[match.index] = match[1];
}
return indices;
};
const countUniqueVariableNames = (content: string) => {
const regex = /\{([^}]+)}/g;
const uniqueMatchesSet = new Set();
let match;
while ((match = regex.exec(content)) !== null) {
uniqueMatchesSet.add(match[1]);
}
return uniqueMatchesSet.size;
};
/**
* An optimisation to try to minimise jank while typing.
* Only check whether variables were modified based on certain key presses.
* Regex would otherwise be performed on every key press (which isn't terrible, but this code helps).
*/
const keyPressShouldBeChecked = (event: KeyboardEvent) => {
if (
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight'
) {
return false;
}
const selection = window.getSelection();
const contenteditable = event.target as HTMLDivElement;
const isCursorAtEnd = selection?.focusOffset === contenteditable?.textContent?.length;
if (isCursorAtEnd) {
return event.key === '}' || event.key === 'Backspace' || event.key === 'Delete';
}
const isCursorAtStart = selection?.anchorOffset === 0;
if (isCursorAtStart) {
return event.key === '{' || event.key === 'Backspace' || event.key === 'Delete';
}
return true;
};