@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!
217 lines • 9.22 kB
JavaScript
import { getDefaultFont } from '@pdfme/common';
import { DEFAULT_FONT_SIZE, DEFAULT_ALIGNMENT, VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM, DEFAULT_VERTICAL_ALIGNMENT, DEFAULT_LINE_HEIGHT, DEFAULT_CHARACTER_SPACING, DEFAULT_FONT_COLOR, PLACEHOLDER_FONT_COLOR, } from './constants.js';
import { calculateDynamicFontSize, getFontKitFont, getBrowserVerticalFontAdjustments, isFirefox, } from './helper.js';
import { isEditable } from '../utils.js';
const replaceUnsupportedChars = (text, fontKitFont) => {
const charSupportCache = {};
const isCharSupported = (char) => {
if (char in charSupportCache) {
return charSupportCache[char];
}
const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
charSupportCache[char] = isSupported;
return isSupported;
};
const segments = text.split(/(\r\n|\n|\r)/);
return segments
.map((segment) => {
if (/\r\n|\n|\r/.test(segment)) {
return segment;
}
return segment
.split('')
.map((char) => {
if (/\s/.test(char) || char.charCodeAt(0) < 32) {
return char;
}
return isCharSupported(char) ? char : '〿';
})
.join('');
})
.join('');
};
export const uiRender = async (arg) => {
const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
const usePlaceholder = isEditable(mode, schema) && placeholder && !value;
const getText = (element) => {
let text = element.innerText;
if (text.endsWith('\n')) {
// contenteditable adds additional newline char retrieved with innerText
text = text.slice(0, -1);
}
return text;
};
const font = options?.font || getDefaultFont();
const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? placeholder : value);
const processedText = replaceUnsupportedChars(value, fontKitFont);
if (!isEditable(mode, schema)) {
// Read-only mode
textBlock.innerHTML = processedText
.split('')
.map((l, i) => `<span style="letter-spacing:${String(value).length === i + 1 ? 0 : 'inherit'};">${l}</span>`)
.join('');
return;
}
makeElementPlainTextContentEditable(textBlock);
textBlock.tabIndex = tabIndex || 0;
textBlock.innerText = mode === 'designer' ? value : processedText;
textBlock.addEventListener('blur', (e) => {
if (onChange)
onChange({ key: 'content', value: getText(e.target) });
if (stopEditing)
stopEditing();
});
if (schema.dynamicFontSize) {
let dynamicFontSize = undefined;
textBlock.addEventListener('keyup', () => {
setTimeout(() => {
// Use a regular function instead of an async one since we don't need await
(() => {
if (!textBlock.textContent)
return;
dynamicFontSize = calculateDynamicFontSize({
textSchema: schema,
fontKitFont,
value: getText(textBlock),
startingFontSize: dynamicFontSize,
});
textBlock.style.fontSize = `${dynamicFontSize}pt`;
const { topAdj: newTopAdj, bottomAdj: newBottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
textBlock.style.paddingTop = `${newTopAdj}px`;
textBlock.style.marginBottom = `${newBottomAdj}px`;
})();
}, 0);
});
}
if (usePlaceholder) {
textBlock.style.color = PLACEHOLDER_FONT_COLOR;
textBlock.addEventListener('focus', () => {
if (textBlock.innerText === placeholder) {
textBlock.innerText = '';
textBlock.style.color = schema.fontColor ?? DEFAULT_FONT_COLOR;
}
});
}
if (mode === 'designer') {
setTimeout(() => {
textBlock.focus();
// Set the focus to the end of the editable element when you focus, as we would for a textarea
const selection = window.getSelection();
const range = document.createRange();
if (selection && range) {
range.selectNodeContents(textBlock);
range.collapse(false); // Collapse range to the end
selection?.removeAllRanges();
selection?.addRange(range);
}
});
}
};
export const buildStyledTextContainer = (arg, fontKitFont, value) => {
const { schema, rootElement, mode } = arg;
let dynamicFontSize = undefined;
if (schema.dynamicFontSize && value) {
dynamicFontSize = calculateDynamicFontSize({
textSchema: schema,
fontKitFont,
value,
startingFontSize: dynamicFontSize,
});
}
// Depending on vertical alignment, we need to move the top or bottom of the font to keep
// it within it's defined box and align it with the generated pdf.
const { topAdj, bottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
const topAdjustment = topAdj.toString();
const bottomAdjustment = bottomAdj.toString();
const container = document.createElement('div');
const containerStyle = {
padding: 0,
resize: 'none',
backgroundColor: getBackgroundColor(value, schema),
border: 'none',
display: 'flex',
flexDirection: 'column',
justifyContent: mapVerticalAlignToFlex(schema.verticalAlignment),
width: '100%',
height: '100%',
cursor: isEditable(mode, schema) ? 'text' : 'default',
};
Object.assign(container.style, containerStyle);
rootElement.innerHTML = '';
rootElement.appendChild(container);
// text decoration
const textDecorations = [];
if (schema.strikethrough)
textDecorations.push('line-through');
if (schema.underline)
textDecorations.push('underline');
const textBlockStyle = {
// Font formatting styles
fontFamily: schema.fontName ? `'${schema.fontName}'` : 'inherit',
color: schema.fontColor ? schema.fontColor : DEFAULT_FONT_COLOR,
fontSize: `${dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE}pt`,
letterSpacing: `${schema.characterSpacing ?? DEFAULT_CHARACTER_SPACING}pt`,
lineHeight: `${schema.lineHeight ?? DEFAULT_LINE_HEIGHT}em`,
textAlign: schema.alignment ?? DEFAULT_ALIGNMENT,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
// Block layout styles
resize: 'none',
border: 'none',
outline: 'none',
marginBottom: `${bottomAdjustment}px`,
paddingTop: `${topAdjustment}px`,
backgroundColor: 'transparent',
textDecoration: textDecorations.join(' '),
};
const textBlock = document.createElement('div');
textBlock.id = 'text-' + String(schema.id);
Object.assign(textBlock.style, textBlockStyle);
container.appendChild(textBlock);
return textBlock;
};
/**
* Firefox doesn't support 'plaintext-only' contentEditable mode, which we want to avoid mark-up.
* This function adds a workaround for Firefox to make the contentEditable element behave like 'plaintext-only'.
*/
export const makeElementPlainTextContentEditable = (element) => {
if (!isFirefox()) {
element.contentEditable = 'plaintext-only';
return;
}
element.contentEditable = 'true';
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.execCommand('insertLineBreak', false, undefined);
}
});
element.addEventListener('paste', (e) => {
e.preventDefault();
const paste = e.clipboardData?.getData('text');
const selection = window.getSelection();
if (!selection?.rangeCount)
return;
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(document.createTextNode(paste || ''));
selection.collapseToEnd();
});
};
export const mapVerticalAlignToFlex = (verticalAlignmentValue) => {
switch (verticalAlignmentValue) {
case VERTICAL_ALIGN_TOP:
return 'flex-start';
case VERTICAL_ALIGN_MIDDLE:
return 'center';
case VERTICAL_ALIGN_BOTTOM:
return 'flex-end';
}
return 'flex-start';
};
const getBackgroundColor = (value, schema) => {
if (!value || !schema.backgroundColor)
return 'transparent';
return schema.backgroundColor;
};
//# sourceMappingURL=uiRender.js.map