@1urso/generic-editor 0.1.37 → 0.1.38
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/README.md +52 -27
- package/dist/generic-editor.js +1 -1
- package/dist/generic-editor.umd.cjs +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,20 +79,46 @@ import "@radix-ui/themes/styles.css";
|
|
|
79
79
|
|
|
80
80
|
This section describes the features available to the **end user** who will use the editor on your platform.
|
|
81
81
|
|
|
82
|
-
### Grouping and Layers Panel
|
|
83
|
-
|
|
84
|
-
- **Group Elements**:
|
|
85
|
-
- Select multiple elements and use the context menu option **Agrupar Seleção**.
|
|
86
|
-
- Groups act as a bounding box; moving the group moves all children.
|
|
87
|
-
- Clicking a child selects its parent group for consistent interactions.
|
|
88
|
-
- **Rename Layers**:
|
|
89
|
-
- Right-click any element or group and choose **Renomear...**.
|
|
90
|
-
- **Layers Panel (Left Sidebar)**:
|
|
91
|
-
- Shows a tree of Groups and their children.
|
|
92
|
-
- Drag any element in the list onto a Group to add it.
|
|
93
|
-
- Drop an element onto the “Soltar aqui para remover do grupo” area to ungroup it.
|
|
94
|
-
- Collapse/expand groups to focus on what matters.
|
|
95
|
-
|
|
82
|
+
### Grouping and Layers Panel
|
|
83
|
+
|
|
84
|
+
- **Group Elements**:
|
|
85
|
+
- Select multiple elements and use the context menu option **Agrupar Seleção**.
|
|
86
|
+
- Groups act as a bounding box; moving the group moves all children.
|
|
87
|
+
- Clicking a child selects its parent group for consistent interactions.
|
|
88
|
+
- **Rename Layers**:
|
|
89
|
+
- Right-click any element or group and choose **Renomear...**.
|
|
90
|
+
- **Layers Panel (Left Sidebar)**:
|
|
91
|
+
- Shows a tree of Groups and their children.
|
|
92
|
+
- Drag any element in the list onto a Group to add it.
|
|
93
|
+
- Drop an element onto the “Soltar aqui para remover do grupo” area to ungroup it.
|
|
94
|
+
- Collapse/expand groups to focus on what matters.
|
|
95
|
+
|
|
96
|
+
### Vínculos de Estilo (Cores por Variável)
|
|
97
|
+
|
|
98
|
+
- **O que é**: vincule propriedades de estilo (cor do texto, cor de fundo, cor da borda) diretamente a variáveis de dados, sem precisar de regras condicionais.
|
|
99
|
+
- **Como usar**:
|
|
100
|
+
- Abra **Configurações Avançadas** do elemento.
|
|
101
|
+
- Na seção **Vínculos de Estilo (Data Binding)**, escolha uma variável para cada propriedade desejada.
|
|
102
|
+
- Propriedades suportadas: `color`, `backgroundColor`, `borderColor`.
|
|
103
|
+
- **Formato dos valores**:
|
|
104
|
+
- Aceita cores CSS válidas: `#RRGGBB`, `#RRGGBBAA` (convertido para `rgba`), nomes CSS (`red`, `blue`), `rgb()`, `rgba()`.
|
|
105
|
+
- **Exemplo (JSON)**:
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"type": "text",
|
|
109
|
+
"content": "Preço: {{price}}",
|
|
110
|
+
"x": 50,
|
|
111
|
+
"y": 50,
|
|
112
|
+
"width": 200,
|
|
113
|
+
"height": 40,
|
|
114
|
+
"styleBindings": {
|
|
115
|
+
"color": "priceColor",
|
|
116
|
+
"backgroundColor": "tagBg"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
Se `data = { "price": 10, "priceColor": "#FF0000", "tagBg": "rgba(0,0,0,0.1)" }`, o texto será renderizado com `color: #FF0000` e `backgroundColor: rgba(0,0,0,0.1)`.
|
|
121
|
+
|
|
96
122
|
### Basic Manipulation
|
|
97
123
|
|
|
98
124
|
The editor offers an experience similar to design tools like Canva or Figma:
|
|
@@ -124,7 +150,6 @@ The editor offers an experience similar to design tools like Canva or Figma:
|
|
|
124
150
|
**Right-click** and select **Advanced Settings** to access powerful data formatting and conditional styling options.
|
|
125
151
|
|
|
126
152
|
- **Data Formatting**:
|
|
127
|
-
|
|
128
153
|
- _Type_: Choose between Text (Default), Boolean (True/False), Date, or Number/Currency.
|
|
129
154
|
- _Boolean_: Define custom labels for true/false values (e.g., "Active" / "Inactive").
|
|
130
155
|
- _Date_: Custom date patterns (e.g., "DD/MM/YYYY HH:mm").
|
|
@@ -172,18 +197,18 @@ When right-clicking on an **Image** element:
|
|
|
172
197
|
- _Stretch (Fill)_: The image fills the entire box, potentially being cropped or distorted depending on the aspect ratio.
|
|
173
198
|
- **Bind Data**: Connects the image to a dynamic variable (e.g., Product Photo).
|
|
174
199
|
|
|
175
|
-
### Export and Import
|
|
176
|
-
|
|
177
|
-
- **Export**: Saves the full layout (elements, list settings, mock data, canvas height).
|
|
178
|
-
- **Import**: Loads a previously exported JSON to restore the editor state.
|
|
179
|
-
- Images are serialized as **Base64** inside the layout to avoid external dependencies.
|
|
180
|
-
|
|
181
|
-
### Preview and Simulation
|
|
182
|
-
|
|
183
|
-
- **Preview Panel**: See a live rendering of your layout.
|
|
184
|
-
- **Simulation Mode (Lists)**: Visualize entry animations and list behavior for incoming items.
|
|
185
|
-
- **Animations**: Configure entry animations (fade, slide, zoom, etc.) for list items.
|
|
186
|
-
|
|
200
|
+
### Export and Import
|
|
201
|
+
|
|
202
|
+
- **Export**: Saves the full layout (elements, list settings, mock data, canvas height).
|
|
203
|
+
- **Import**: Loads a previously exported JSON to restore the editor state.
|
|
204
|
+
- Images are serialized as **Base64** inside the layout to avoid external dependencies.
|
|
205
|
+
|
|
206
|
+
### Preview and Simulation
|
|
207
|
+
|
|
208
|
+
- **Preview Panel**: See a live rendering of your layout.
|
|
209
|
+
- **Simulation Mode (Lists)**: Visualize entry animations and list behavior for incoming items.
|
|
210
|
+
- **Animations**: Configure entry animations (fade, slide, zoom, etc.) for list items.
|
|
211
|
+
|
|
187
212
|
---
|
|
188
213
|
|
|
189
214
|
## Developer Guide (Integration)
|
package/dist/generic-editor.js
CHANGED
|
@@ -18067,5 +18067,5 @@ const GenericEditor = (n) => /* @__PURE__ */ jsx(EditorProvider, {
|
|
|
18067
18067
|
availableProps: n.layout.props,
|
|
18068
18068
|
theme: n.theme,
|
|
18069
18069
|
children: /* @__PURE__ */ jsx(EditorContent, { ...n })
|
|
18070
|
-
}), generateHTML = (n, _, E = {}) => Function("elements", "data", "options", getRendererCode() + "\nreturn renderTemplate(elements, data, options);")(n, _, E), getRendererCode = () => "\n/**\n * Render Template\n * @param {Array} elements - The JSON configuration of elements\n * @param {Object|Array} data - The data object to inject (Object for single, Array for list)\n * @param {Object} options - { isList: boolean, listSettings: { sortProp: string, sortOrder: 'asc'|'desc', newestPosition: 'top'|'bottom', scrollDirection: 'up'|'down', containerHeight: number }, canvasHeight: number }\n * @returns {string} - The generated HTML string\n */\nfunction renderTemplate(elements, data, options = {}) {\n const { isList, listSettings, canvasHeight } = options;\n\n const measureTextHeight = (text, width, fontFamily, fontSize, lineHeightMultiplier = 1.2) => {\n if (!text) return 0;\n try {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) return 0;\n context.font = `${fontSize}px ${fontFamily}`;\n const words = String(text).split(' ');\n let line = '';\n let lineCount = 1;\n for (let i = 0; i < words.length; i++) {\n const testLine = line + words[i] + ' ';\n const metrics = context.measureText(testLine);\n const testWidth = metrics.width;\n if (testWidth > width && i > 0) {\n line = words[i] + ' ';\n lineCount++;\n } else {\n line = testLine;\n }\n }\n const explicitLines = String(text).split('\\n').length - 1;\n lineCount += explicitLines;\n return Math.ceil(lineCount * fontSize * lineHeightMultiplier);\n } catch (_) {\n return 0;\n }\n };\n\n const computeItemHeight = (elements, itemData, fallbackHeight) => {\n if (fallbackHeight) return fallbackHeight;\n let maxY = 0;\n elements.forEach(el => {\n let height = el.height;\n if (el.type === 'text' && el.autoGrow) {\n let content = el.content;\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n const fontSize = parseInt(String((el.style && el.style.fontSize) || 16));\n const fontFamily = String((el.style && el.style.fontFamily) || 'Arial');\n const measured = measureTextHeight(content, el.width, fontFamily, fontSize);\n height = Math.max(height, measured);\n }\n const bottom = el.y + height;\n if (bottom > maxY) maxY = bottom;\n });\n return maxY;\n };\n\n const formatValue = (value, formatting) => {\n if (!formatting || formatting.type === 'text') return value !== undefined && value !== null ? String(value) : '';\n if (value === undefined || value === null) return '';\n\n if (formatting.type === 'boolean') {\n const isTrue = String(value) === 'true' || value === true || (typeof value === 'number' && value > 0);\n return isTrue ? (formatting.trueLabel || 'Sim') : (formatting.falseLabel || 'Não');\n }\n\n if (formatting.type === 'date') {\n try {\n const date = new Date(value);\n if (isNaN(date.getTime())) return String(value);\n \n if (formatting.dateFormat) {\n const d = date.getDate().toString().padStart(2, '0');\n const m = (date.getMonth() + 1).toString().padStart(2, '0');\n const y = date.getFullYear();\n const H = date.getHours().toString().padStart(2, '0');\n const M = date.getMinutes().toString().padStart(2, '0');\n const S = date.getSeconds().toString().padStart(2, '0');\n \n return formatting.dateFormat\n .replace('DD', d)\n .replace('MM', m)\n .replace('YYYY', String(y))\n .replace('HH', H)\n .replace('mm', M)\n .replace('ss', S);\n }\n return date.toLocaleDateString();\n } catch (e) { return String(value); }\n }\n\n if (formatting.type === 'number') {\n const num = parseFloat(value);\n if (isNaN(num)) return String(value);\n \n if (formatting.numberFormat === 'currency') {\n return (formatting.currencySymbol || 'R$') + ' ' + num.toFixed(formatting.decimalPlaces || 2);\n }\n if (formatting.numberFormat === 'percent') {\n return num.toFixed(formatting.decimalPlaces || 0) + '%';\n }\n if (formatting.decimalPlaces !== undefined) {\n return num.toFixed(formatting.decimalPlaces);\n }\n return num.toFixed(formatting.decimalPlaces || 0);\n }\n \n return String(value);\n };\n\n const checkCondition = (propValue, operator, ruleValue) => {\n const val = String(propValue).toLowerCase();\n const target = String(ruleValue).toLowerCase();\n \n switch (operator) {\n case 'equals': return val === target;\n case 'notEquals': return val !== target;\n case 'contains': return val.includes(target);\n case 'greaterThan': return parseFloat(val) > parseFloat(target);\n case 'lessThan': return parseFloat(val) < parseFloat(target);\n case 'truthy': return !!propValue;\n case 'falsy': return !propValue;\n default: return false;\n }\n };\n\n const camelToKebab = (string) => {\n return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();\n };\n\n const hex8ToRgba = (hex) => {\n const m = /^#([0-9a-fA-F]{8})$/.exec(hex);\n if (!m) return hex;\n const h = m[1];\n const r = parseInt(h.slice(0, 2), 16);\n const g = parseInt(h.slice(2, 4), 16);\n const b = parseInt(h.slice(4, 6), 16);\n const a = parseInt(h.slice(6, 8), 16) / 255;\n return `rgba(${r}, ${g}, ${b}, ${a})`;\n };\n\n const styleObjectToString = (style) => {\n if (!style) return '';\n const pxProps = [\n 'width', 'height', 'top', 'left', 'right', 'bottom', \n 'fontSize', 'borderRadius', 'padding', 'margin', 'borderWidth',\n 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'\n ];\n \n return Object.entries(style)\n .map(([key, value]) => {\n if (value === undefined || value === null) return '';\n const cssKey = camelToKebab(key);\n let cssValue = (typeof value === 'number' && pxProps.includes(key)) ? value + 'px' : value;\n if (typeof cssValue === 'string') {\n if (/^#([0-9a-fA-F]{8})$/.test(cssValue)) {\n cssValue = hex8ToRgba(cssValue);\n }\n }\n return `${cssKey}: ${cssValue}`;\n })\n .filter(Boolean)\n .join('; ');\n };\n\n const getAnimationStyles = (anim) => {\n if (!anim || anim.type === 'none') return {};\n return {\n animationName: anim.type,\n animationDuration: (anim.duration || 1) + 's',\n animationDelay: (anim.delay || 0) + 's',\n animationIterationCount: anim.iterationCount || 1,\n animationTimingFunction: anim.timingFunction || 'ease',\n animationFillMode: 'both'\n };\n };\n\n const keyframesCss = `\n @keyframes slideIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n }\n @keyframes fadeIn { \n from { opacity: 0; } \n to { opacity: 1; } \n }\n @keyframes slideInLeft { \n from { opacity: 0; transform: translateX(-50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInRight { \n from { opacity: 0; transform: translateX(50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInUp { \n from { opacity: 0; transform: translateY(50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes slideInDown { \n from { opacity: 0; transform: translateY(-50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes zoomIn { \n from { opacity: 0; transform: scale(0.5); } \n to { opacity: 1; transform: scale(1); } \n }\n @keyframes bounceIn {\n 0% { opacity: 0; transform: scale(0.3); }\n 50% { opacity: 1; transform: scale(1.05); }\n 70% { transform: scale(0.9); }\n 100% { transform: scale(1); }\n }\n @keyframes pulse {\n 0% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n 100% { transform: scale(1); }\n }\n @keyframes shake {\n 0%, 100% { transform: translateX(0); }\n 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }\n 20%, 40%, 60%, 80% { transform: translateX(5px); }\n }\n @keyframes spin { \n from { transform: rotate(0deg); } \n to { transform: rotate(360deg); } \n }\n \n /* Improved / Smoother Animations */\n @keyframes smoothSlideUp {\n 0% { opacity: 0; transform: translateY(30px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n @keyframes popIn {\n 0% { opacity: 0; transform: scale(0.8) translateY(10px); }\n 100% { opacity: 1; transform: scale(1) translateY(0); }\n }\n @keyframes blurIn {\n 0% { opacity: 0; filter: blur(10px); }\n 100% { opacity: 1; filter: blur(0); }\n }\n `;\n\n const renderItem = (itemData, index = 0, offsetY = 0) => {\n return elements.map(element => {\n let content = element.content;\n let imgSrc = '';\n\n // Resolve Content & Formatting\n if (element.type === 'text') {\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n if (val === undefined || val === null) return match;\n if (element.formatting) {\n return formatValue(val, element.formatting);\n }\n return String(val);\n });\n } else if (element.type === 'image') {\n if (element.dataBinding) {\n const val = itemData[element.dataBinding];\n if (val !== undefined && val !== null) {\n imgSrc = String(val);\n } else {\n imgSrc = content;\n }\n } else {\n imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n }\n }\n\n // Resolve Conditional Styles\n let conditionalStyles = {};\n if (element.conditions) {\n element.conditions.forEach(rule => {\n const propVal = itemData[rule.property];\n if (checkCondition(propVal, rule.operator, rule.value)) {\n conditionalStyles = { ...conditionalStyles, ...rule.style };\n }\n });\n }\n\n const baseStyle = {\n position: 'absolute',\n left: element.x,\n top: element.y + offsetY,\n width: element.width,\n height: element.autoGrow ? 'auto' : element.height,\n transform: element.rotation ? `rotate(${element.rotation}deg)` : undefined,\n overflow: element.autoGrow ? 'visible' : 'hidden',\n whiteSpace: element.autoGrow ? 'pre-wrap' : undefined,\n wordBreak: element.autoGrow ? 'break-word' : undefined,\n ...element.style,\n ...conditionalStyles\n };\n \n // Fix: remove padding if it's not explicitly set, or handle it for text\n if (element.type === 'text' && !baseStyle.padding) {\n // baseStyle.padding = '8px'; // Removed default padding to respect resize box\n }\n \n const styleString = styleObjectToString(baseStyle);\n\n if (element.type === 'text') {\n return `<div style=\"${styleString}\">${content}</div>`;\n } else if (element.type === 'image') {\n const imgStyle = styleObjectToString({\n width: '100%',\n height: '100%',\n objectFit: element.style?.objectFit || 'cover',\n display: 'block'\n });\n return `<div style=\"${styleString}\"><img src=\"${imgSrc}\" alt=\"Element\" style=\"${imgStyle}\" /></div>`;\n } else if (element.type === 'box') {\n return `<div style=\"${styleString}\"></div>`;\n }\n return '';\n }).join('\\n');\n };\n\n if (isList && Array.isArray(data)) {\n // Calculate per-item height respecting autoGrow\n // Sort data\n let listData = [...data];\n if (listSettings && listSettings.sortProp) {\n const prop = listSettings.sortProp;\n const order = listSettings.sortOrder === 'asc' ? 1 : -1;\n listData.sort((a, b) => {\n const valA = a[prop];\n const valB = b[prop];\n if (valA < valB) return -1 * order;\n if (valA > valB) return 1 * order;\n return 0;\n });\n }\n \n // Handle newest position\n if (listSettings && listSettings.newestPosition === 'top') {\n listData.reverse();\n }\n\n // Generate HTML for all items\n const itemsHtml = listData.map((item, index) => {\n const itemHtml = renderItem(item, index, 0); \n const itemHeight = computeItemHeight(elements, item, canvasHeight);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n height: itemHeight,\n width: '100%',\n marginBottom: 0\n });\n \n return `<div class=\"list-item\" style=\"${itemContainerStyle}\">${itemHtml}</div>`;\n }).join('\\n');\n\n // Animation Styles based on settings\n const scrollDirection = (listSettings && listSettings.scrollDirection) || 'down';\n const containerHeight = (listSettings && listSettings.containerHeight) ? listSettings.containerHeight + 'px' : '100%';\n \n const justify = (listSettings && listSettings.newestPosition === 'top') ? 'flex-start' : 'flex-end';\n\n // Entry Animation from settings\n const entryAnim = listSettings && listSettings.entryAnimation ? listSettings.entryAnimation : { type: 'slideIn', duration: 0.3, timingFunction: 'ease-out' };\n const entryAnimName = entryAnim.type === 'none' ? 'none' : entryAnim.type;\n const entryAnimDuration = entryAnim.duration + 's';\n const entryAnimTiming = entryAnim.timingFunction || 'ease-out';\n\n const animationCss = `\n ${keyframesCss}\n\n .list-wrapper {\n display: flex;\n flex-direction: column;\n justify-content: ${justify};\n height: ${containerHeight};\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n box-sizing: border-box;\n padding: 10px;\n }\n .list-item {\n flex-shrink: 0;\n animation: ${entryAnimName} ${entryAnimDuration} ${entryAnimTiming};\n margin-bottom: 10px;\n width: 100%;\n position: relative;\n }\n `;\n \n const scrollScript = scrollDirection === 'up' \n ? `<script>\n document.addEventListener('DOMContentLoaded', () => {\n const wrapper = document.querySelector('.list-wrapper');\n if(wrapper) wrapper.scrollTop = wrapper.scrollHeight;\n });\n <\/script>`\n : '';\n\n // Inject Smart Script for Dynamic Updates\n const injectionScript = `\n <script>\n (function() {\n try {\n const elements = ${JSON.stringify(elements)};\n const formatValue = ${formatValue.toString()};\n const checkCondition = ${checkCondition.toString()};\n const camelToKebab = ${camelToKebab.toString()};\n const hex8ToRgba = ${hex8ToRgba.toString()};\n const styleObjectToString = ${styleObjectToString.toString()};\n const getAnimationStyles = ${getAnimationStyles.toString()};\n const renderItem = ${renderItem.toString()};\n\n const measureTextHeight = ${measureTextHeight.toString()};\n const computeItemHeight = ${computeItemHeight.toString()};\n const itemHeightFallback = ${canvasHeight || 0};\n const newestPosition = \"${(listSettings && listSettings.newestPosition) || 'bottom'}\";\n const scrollDirection = \"${(listSettings && listSettings.scrollDirection) || 'down'}\";\n\n window.addItem = function(data) {\n const wrapper = document.querySelector('.list-wrapper');\n if (!wrapper) return;\n\n const itemHtml = renderItem(data, 0, 0);\n const itemHeight = computeItemHeight(elements, data, itemHeightFallback);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n height: itemHeight,\n width: '100%',\n marginBottom: 0\n });\n\n const div = document.createElement('div');\n div.className = 'list-item';\n div.setAttribute('style', itemContainerStyle);\n div.innerHTML = itemHtml;\n\n if (newestPosition === 'top') {\n wrapper.insertBefore(div, wrapper.firstChild);\n } else {\n wrapper.appendChild(div);\n }\n \n if (scrollDirection === 'up') {\n wrapper.scrollTop = wrapper.scrollHeight;\n }\n };\n } catch(e) { console.error(\"Smart List Init Error\", e); }\n })();\n <\/script>\n `;\n\n return `\n <style>${animationCss}</style>\n <div class=\"list-wrapper\">\n ${itemsHtml}\n </div>\n ${scrollScript}\n ${injectionScript}\n `;\n }\n\n // Single Item\n const contentHtml = renderItem(data);\n return `<div style=\"position: relative; width: 100%; height: 100%; overflow: hidden;\">${contentHtml}</div>`;\n}\n";
|
|
18070
|
+
}), generateHTML = (n, _, E = {}) => Function("elements", "data", "options", getRendererCode() + "\nreturn renderTemplate(elements, data, options);")(n, _, E), getRendererCode = () => "\n/**\n * Render Template\n * @param {Array} elements - The JSON configuration of elements\n * @param {Object|Array} data - The data object to inject (Object for single, Array for list)\n * @param {Object} options - { isList: boolean, listSettings: { sortProp: string, sortOrder: 'asc'|'desc', newestPosition: 'top'|'bottom', scrollDirection: 'up'|'down', containerHeight: number }, canvasHeight: number }\n * @returns {string} - The generated HTML string\n */\nfunction renderTemplate(elements, data, options = {}) {\n const { isList, listSettings, canvasHeight } = options;\n\n const measureTextHeight = (text, width, fontFamily, fontSize, lineHeightMultiplier = 1.2) => {\n if (!text) return 0;\n try {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) return 0;\n context.font = `${fontSize}px ${fontFamily}`;\n const words = String(text).split(' ');\n let line = '';\n let lineCount = 1;\n for (let i = 0; i < words.length; i++) {\n const testLine = line + words[i] + ' ';\n const metrics = context.measureText(testLine);\n const testWidth = metrics.width;\n if (testWidth > width && i > 0) {\n line = words[i] + ' ';\n lineCount++;\n } else {\n line = testLine;\n }\n }\n const explicitLines = String(text).split('\\n').length - 1;\n lineCount += explicitLines;\n return Math.ceil(lineCount * fontSize * lineHeightMultiplier);\n } catch (_) {\n return 0;\n }\n };\n\n const computeItemHeight = (elements, itemData, fallbackHeight) => {\n let maxY = 0;\n elements.forEach(el => {\n let height = el.height;\n if (el.type === 'text' && el.autoGrow) {\n let content = el.content;\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n const fontSize = parseInt(String((el.style && el.style.fontSize) || 16));\n const fontFamily = String((el.style && el.style.fontFamily) || 'Arial');\n const measured = measureTextHeight(content, el.width, fontFamily, fontSize);\n height = Math.max(height, measured);\n }\n const bottom = el.y + height;\n if (bottom > maxY) maxY = bottom;\n });\n return fallbackHeight ? Math.max(maxY, fallbackHeight) : maxY;\n };\n\n const formatValue = (value, formatting) => {\n if (!formatting || formatting.type === 'text') return value !== undefined && value !== null ? String(value) : '';\n if (value === undefined || value === null) return '';\n\n if (formatting.type === 'boolean') {\n const isTrue = String(value) === 'true' || value === true || (typeof value === 'number' && value > 0);\n return isTrue ? (formatting.trueLabel || 'Sim') : (formatting.falseLabel || 'Não');\n }\n\n if (formatting.type === 'date') {\n try {\n const date = new Date(value);\n if (isNaN(date.getTime())) return String(value);\n \n if (formatting.dateFormat) {\n const d = date.getDate().toString().padStart(2, '0');\n const m = (date.getMonth() + 1).toString().padStart(2, '0');\n const y = date.getFullYear();\n const H = date.getHours().toString().padStart(2, '0');\n const M = date.getMinutes().toString().padStart(2, '0');\n const S = date.getSeconds().toString().padStart(2, '0');\n \n return formatting.dateFormat\n .replace('DD', d)\n .replace('MM', m)\n .replace('YYYY', String(y))\n .replace('HH', H)\n .replace('mm', M)\n .replace('ss', S);\n }\n return date.toLocaleDateString();\n } catch (e) { return String(value); }\n }\n\n if (formatting.type === 'number') {\n const num = parseFloat(value);\n if (isNaN(num)) return String(value);\n \n if (formatting.numberFormat === 'currency') {\n return (formatting.currencySymbol || 'R$') + ' ' + num.toFixed(formatting.decimalPlaces || 2);\n }\n if (formatting.numberFormat === 'percent') {\n return num.toFixed(formatting.decimalPlaces || 0) + '%';\n }\n if (formatting.decimalPlaces !== undefined) {\n return num.toFixed(formatting.decimalPlaces);\n }\n return num.toFixed(formatting.decimalPlaces || 0);\n }\n \n return String(value);\n };\n\n const checkCondition = (propValue, operator, ruleValue) => {\n const val = String(propValue).toLowerCase();\n const target = String(ruleValue).toLowerCase();\n \n switch (operator) {\n case 'equals': return val === target;\n case 'notEquals': return val !== target;\n case 'contains': return val.includes(target);\n case 'greaterThan': return parseFloat(val) > parseFloat(target);\n case 'lessThan': return parseFloat(val) < parseFloat(target);\n case 'truthy': return !!propValue;\n case 'falsy': return !propValue;\n default: return false;\n }\n };\n\n const camelToKebab = (string) => {\n return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();\n };\n\n const hex8ToRgba = (hex) => {\n const m = /^#([0-9a-fA-F]{8})$/.exec(hex);\n if (!m) return hex;\n const h = m[1];\n const r = parseInt(h.slice(0, 2), 16);\n const g = parseInt(h.slice(2, 4), 16);\n const b = parseInt(h.slice(4, 6), 16);\n const a = parseInt(h.slice(6, 8), 16) / 255;\n return `rgba(${r}, ${g}, ${b}, ${a})`;\n };\n\n const styleObjectToString = (style) => {\n if (!style) return '';\n const pxProps = [\n 'width', 'height', 'top', 'left', 'right', 'bottom', \n 'fontSize', 'borderRadius', 'padding', 'margin', 'borderWidth',\n 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'\n ];\n \n return Object.entries(style)\n .map(([key, value]) => {\n if (value === undefined || value === null) return '';\n const cssKey = camelToKebab(key);\n let cssValue = (typeof value === 'number' && pxProps.includes(key)) ? value + 'px' : value;\n if (typeof cssValue === 'string') {\n if (/^#([0-9a-fA-F]{8})$/.test(cssValue)) {\n cssValue = hex8ToRgba(cssValue);\n }\n }\n return `${cssKey}: ${cssValue}`;\n })\n .filter(Boolean)\n .join('; ');\n };\n\n const getAnimationStyles = (anim) => {\n if (!anim || anim.type === 'none') return {};\n return {\n animationName: anim.type,\n animationDuration: (anim.duration || 1) + 's',\n animationDelay: (anim.delay || 0) + 's',\n animationIterationCount: anim.iterationCount || 1,\n animationTimingFunction: anim.timingFunction || 'ease',\n animationFillMode: 'both'\n };\n };\n\n const keyframesCss = `\n @keyframes slideIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n }\n @keyframes fadeIn { \n from { opacity: 0; } \n to { opacity: 1; } \n }\n @keyframes slideInLeft { \n from { opacity: 0; transform: translateX(-50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInRight { \n from { opacity: 0; transform: translateX(50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInUp { \n from { opacity: 0; transform: translateY(50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes slideInDown { \n from { opacity: 0; transform: translateY(-50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes zoomIn { \n from { opacity: 0; transform: scale(0.5); } \n to { opacity: 1; transform: scale(1); } \n }\n @keyframes bounceIn {\n 0% { opacity: 0; transform: scale(0.3); }\n 50% { opacity: 1; transform: scale(1.05); }\n 70% { transform: scale(0.9); }\n 100% { transform: scale(1); }\n }\n @keyframes pulse {\n 0% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n 100% { transform: scale(1); }\n }\n @keyframes shake {\n 0%, 100% { transform: translateX(0); }\n 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }\n 20%, 40%, 60%, 80% { transform: translateX(5px); }\n }\n @keyframes spin { \n from { transform: rotate(0deg); } \n to { transform: rotate(360deg); } \n }\n \n /* Improved / Smoother Animations */\n @keyframes smoothSlideUp {\n 0% { opacity: 0; transform: translateY(30px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n @keyframes popIn {\n 0% { opacity: 0; transform: scale(0.8) translateY(10px); }\n 100% { opacity: 1; transform: scale(1) translateY(0); }\n }\n @keyframes blurIn {\n 0% { opacity: 0; filter: blur(10px); }\n 100% { opacity: 1; filter: blur(0); }\n }\n `;\n\n const renderItem = (itemData, index = 0, offsetY = 0) => {\n return elements.map(element => {\n let content = element.content;\n let imgSrc = '';\n\n // Resolve Content & Formatting\n if (element.type === 'text') {\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n if (val === undefined || val === null) return match;\n if (element.formatting) {\n return formatValue(val, element.formatting);\n }\n return String(val);\n });\n } else if (element.type === 'image') {\n if (element.dataBinding) {\n const val = itemData[element.dataBinding];\n if (val !== undefined && val !== null) {\n imgSrc = String(val);\n } else {\n imgSrc = content;\n }\n } else {\n imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n }\n }\n\n // Resolve Conditional Styles\n let conditionalStyles = {};\n if (element.conditions) {\n element.conditions.forEach(rule => {\n const propVal = itemData[rule.property];\n if (checkCondition(propVal, rule.operator, rule.value)) {\n conditionalStyles = { ...conditionalStyles, ...rule.style };\n }\n });\n }\n\n // Resolve Style Bindings\n let bindingStyles = {};\n if (element.styleBindings) {\n Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {\n const val = itemData[variableName];\n if (val !== undefined && val !== null) {\n bindingStyles[styleProp] = String(val);\n }\n });\n }\n\n const baseStyle = {\n position: 'absolute',\n left: element.x,\n top: element.y + offsetY,\n width: element.width,\n height: element.autoGrow ? 'auto' : element.height,\n transform: element.rotation ? `rotate(${element.rotation}deg)` : undefined,\n overflow: element.autoGrow ? 'visible' : 'hidden',\n whiteSpace: element.autoGrow ? 'pre-wrap' : undefined,\n wordBreak: element.autoGrow ? 'break-word' : undefined,\n ...element.style,\n ...conditionalStyles,\n ...bindingStyles\n };\n \n // Fix: remove padding if it's not explicitly set, or handle it for text\n if (element.type === 'text' && !baseStyle.padding) {\n // baseStyle.padding = '8px'; // Removed default padding to respect resize box\n }\n \n const styleString = styleObjectToString(baseStyle);\n\n if (element.type === 'text') {\n return `<div style=\"${styleString}\">${content}</div>`;\n } else if (element.type === 'image') {\n const imgStyle = styleObjectToString({\n width: '100%',\n height: '100%',\n objectFit: element.style?.objectFit || 'cover',\n display: 'block'\n });\n return `<div style=\"${styleString}\"><img src=\"${imgSrc}\" alt=\"Element\" style=\"${imgStyle}\" /></div>`;\n } else if (element.type === 'box') {\n return `<div style=\"${styleString}\"></div>`;\n }\n return '';\n }).join('\\n');\n };\n\n if (isList && Array.isArray(data)) {\n // Calculate per-item height respecting autoGrow\n // Sort data\n let listData = [...data];\n if (listSettings && listSettings.sortProp) {\n const prop = listSettings.sortProp;\n const order = listSettings.sortOrder === 'asc' ? 1 : -1;\n listData.sort((a, b) => {\n const valA = a[prop];\n const valB = b[prop];\n if (valA < valB) return -1 * order;\n if (valA > valB) return 1 * order;\n return 0;\n });\n }\n \n // Handle newest position\n if (listSettings && listSettings.newestPosition === 'top') {\n listData.reverse();\n }\n\n // Generate HTML for all items\n const itemsHtml = listData.map((item, index) => {\n const itemHtml = renderItem(item, index, 0); \n const itemHeight = computeItemHeight(elements, item, canvasHeight);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n height: itemHeight,\n width: '100%'\n });\n \n return `<div class=\"list-item\" style=\"${itemContainerStyle}\">${itemHtml}</div>`;\n }).join('\\n');\n\n // Animation Styles based on settings\n const scrollDirection = (listSettings && listSettings.scrollDirection) || 'down';\n const containerHeight = (listSettings && listSettings.containerHeight) ? listSettings.containerHeight + 'px' : '100%';\n \n const justify = (listSettings && listSettings.newestPosition === 'top') ? 'flex-start' : 'flex-end';\n\n // Entry Animation from settings\n const entryAnim = listSettings && listSettings.entryAnimation ? listSettings.entryAnimation : { type: 'slideIn', duration: 0.3, timingFunction: 'ease-out' };\n const entryAnimName = entryAnim.type === 'none' ? 'none' : entryAnim.type;\n const entryAnimDuration = entryAnim.duration + 's';\n const entryAnimTiming = entryAnim.timingFunction || 'ease-out';\n\n const animationCss = `\n ${keyframesCss}\n\n .list-wrapper {\n display: flex;\n flex-direction: column;\n justify-content: ${justify};\n height: ${containerHeight};\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n box-sizing: border-box;\n padding: 10px;\n }\n .list-item {\n flex-shrink: 0;\n animation: ${entryAnimName} ${entryAnimDuration} ${entryAnimTiming};\n margin-bottom: 10px;\n width: 100%;\n position: relative;\n }\n `;\n \n const scrollScript = scrollDirection === 'up' \n ? `<script>\n document.addEventListener('DOMContentLoaded', () => {\n const wrapper = document.querySelector('.list-wrapper');\n if(wrapper) wrapper.scrollTop = wrapper.scrollHeight;\n });\n <\/script>`\n : '';\n\n // Inject Smart Script for Dynamic Updates\n const injectionScript = `\n <script>\n (function() {\n try {\n const elements = ${JSON.stringify(elements)};\n const formatValue = ${formatValue.toString()};\n const checkCondition = ${checkCondition.toString()};\n const camelToKebab = ${camelToKebab.toString()};\n const hex8ToRgba = ${hex8ToRgba.toString()};\n const styleObjectToString = ${styleObjectToString.toString()};\n const getAnimationStyles = ${getAnimationStyles.toString()};\n const renderItem = ${renderItem.toString()};\n\n const measureTextHeight = ${measureTextHeight.toString()};\n const computeItemHeight = ${computeItemHeight.toString()};\n const itemHeightFallback = ${canvasHeight || 0};\n const newestPosition = \"${(listSettings && listSettings.newestPosition) || 'bottom'}\";\n const scrollDirection = \"${(listSettings && listSettings.scrollDirection) || 'down'}\";\n\n window.addItem = function(data) {\n const wrapper = document.querySelector('.list-wrapper');\n if (!wrapper) return;\n\n const itemHtml = renderItem(data, 0, 0);\n const itemHeight = computeItemHeight(elements, data, itemHeightFallback);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n height: itemHeight,\n width: '100%'\n });\n\n const div = document.createElement('div');\n div.className = 'list-item';\n div.setAttribute('style', itemContainerStyle);\n div.innerHTML = itemHtml;\n\n if (newestPosition === 'top') {\n wrapper.insertBefore(div, wrapper.firstChild);\n } else {\n wrapper.appendChild(div);\n }\n \n if (scrollDirection === 'up') {\n wrapper.scrollTop = wrapper.scrollHeight;\n }\n };\n } catch(e) { console.error(\"Smart List Init Error\", e); }\n })();\n <\/script>\n `;\n\n return `\n <style>${animationCss}</style>\n <div class=\"list-wrapper\">\n ${itemsHtml}\n </div>\n ${scrollScript}\n ${injectionScript}\n `;\n }\n\n // Single Item\n const contentHtml = renderItem(data);\n return `<div style=\"position: relative; width: 100%; height: 100%; overflow: hidden;\">${contentHtml}</div>`;\n}\n";
|
|
18071
18071
|
export { GenericEditor as EditorContent, generateHTML };
|
|
@@ -96,7 +96,6 @@ function renderTemplate(elements, data, options = {}) {
|
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
const computeItemHeight = (elements, itemData, fallbackHeight) => {
|
|
99
|
-
if (fallbackHeight) return fallbackHeight;
|
|
100
99
|
let maxY = 0;
|
|
101
100
|
elements.forEach(el => {
|
|
102
101
|
let height = el.height;
|
|
@@ -114,7 +113,7 @@ function renderTemplate(elements, data, options = {}) {
|
|
|
114
113
|
const bottom = el.y + height;
|
|
115
114
|
if (bottom > maxY) maxY = bottom;
|
|
116
115
|
});
|
|
117
|
-
return maxY;
|
|
116
|
+
return fallbackHeight ? Math.max(maxY, fallbackHeight) : maxY;
|
|
118
117
|
};
|
|
119
118
|
|
|
120
119
|
const formatValue = (value, formatting) => {
|
|
@@ -344,6 +343,17 @@ function renderTemplate(elements, data, options = {}) {
|
|
|
344
343
|
});
|
|
345
344
|
}
|
|
346
345
|
|
|
346
|
+
// Resolve Style Bindings
|
|
347
|
+
let bindingStyles = {};
|
|
348
|
+
if (element.styleBindings) {
|
|
349
|
+
Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {
|
|
350
|
+
const val = itemData[variableName];
|
|
351
|
+
if (val !== undefined && val !== null) {
|
|
352
|
+
bindingStyles[styleProp] = String(val);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
347
357
|
const baseStyle = {
|
|
348
358
|
position: 'absolute',
|
|
349
359
|
left: element.x,
|
|
@@ -355,7 +365,8 @@ function renderTemplate(elements, data, options = {}) {
|
|
|
355
365
|
whiteSpace: element.autoGrow ? 'pre-wrap' : undefined,
|
|
356
366
|
wordBreak: element.autoGrow ? 'break-word' : undefined,
|
|
357
367
|
...element.style,
|
|
358
|
-
...conditionalStyles
|
|
368
|
+
...conditionalStyles,
|
|
369
|
+
...bindingStyles
|
|
359
370
|
};
|
|
360
371
|
|
|
361
372
|
// Fix: remove padding if it's not explicitly set, or handle it for text
|
|
@@ -410,8 +421,7 @@ function renderTemplate(elements, data, options = {}) {
|
|
|
410
421
|
const itemContainerStyle = styleObjectToString({
|
|
411
422
|
position: 'relative',
|
|
412
423
|
height: itemHeight,
|
|
413
|
-
width: '100%'
|
|
414
|
-
marginBottom: 0
|
|
424
|
+
width: '100%'
|
|
415
425
|
});
|
|
416
426
|
|
|
417
427
|
return \`<div class="list-item" style="\${itemContainerStyle}">\${itemHtml}</div>\`;
|
|
@@ -490,8 +500,7 @@ function renderTemplate(elements, data, options = {}) {
|
|
|
490
500
|
const itemContainerStyle = styleObjectToString({
|
|
491
501
|
position: 'relative',
|
|
492
502
|
height: itemHeight,
|
|
493
|
-
width: '100%'
|
|
494
|
-
marginBottom: 0
|
|
503
|
+
width: '100%'
|
|
495
504
|
});
|
|
496
505
|
|
|
497
506
|
const div = document.createElement('div');
|