UNPKG

@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
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