@dmitryvim/form-builder 0.2.6 → 0.2.8
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 +3 -1
- package/dist/browser/formbuilder.min.js +183 -63
- package/dist/browser/formbuilder.v0.2.8.min.js +304 -0
- package/dist/cjs/index.cjs +1476 -286
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1464 -284
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +183 -63
- package/dist/types/components/colour.d.ts +18 -0
- package/dist/types/components/index.d.ts +3 -1
- package/dist/types/components/slider.d.ts +11 -0
- package/dist/types/instance/FormBuilderInstance.d.ts +4 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +27 -1
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.6.min.js +0 -184
package/dist/esm/index.js
CHANGED
|
@@ -87,6 +87,43 @@ function validateSchema(schema) {
|
|
|
87
87
|
validateElements(element.elements, `${elementPath}.elements`);
|
|
88
88
|
}
|
|
89
89
|
if (element.type === "container" && element.elements) {
|
|
90
|
+
if ("columns" in element && element.columns !== void 0) {
|
|
91
|
+
const columns = element.columns;
|
|
92
|
+
const validColumns = [1, 2, 3, 4];
|
|
93
|
+
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
94
|
+
errors.push(
|
|
95
|
+
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if ("prefillHints" in element && element.prefillHints) {
|
|
100
|
+
const prefillHints = element.prefillHints;
|
|
101
|
+
if (Array.isArray(prefillHints)) {
|
|
102
|
+
prefillHints.forEach((hint, hintIndex) => {
|
|
103
|
+
if (!hint.label || typeof hint.label !== "string") {
|
|
104
|
+
errors.push(
|
|
105
|
+
`${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (!hint.values || typeof hint.values !== "object") {
|
|
109
|
+
errors.push(
|
|
110
|
+
`${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
for (const fieldKey in hint.values) {
|
|
114
|
+
const fieldExists = element.elements.some(
|
|
115
|
+
(childElement) => childElement.key === fieldKey
|
|
116
|
+
);
|
|
117
|
+
if (!fieldExists) {
|
|
118
|
+
errors.push(
|
|
119
|
+
`container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
90
127
|
validateElements(element.elements, `${elementPath}.elements`);
|
|
91
128
|
}
|
|
92
129
|
if (element.type === "select" && element.options) {
|
|
@@ -1222,6 +1259,172 @@ function t(key, state) {
|
|
|
1222
1259
|
}
|
|
1223
1260
|
|
|
1224
1261
|
// src/components/file.ts
|
|
1262
|
+
function renderLocalImagePreview(container, file, fileName) {
|
|
1263
|
+
const img = document.createElement("img");
|
|
1264
|
+
img.className = "w-full h-full object-contain";
|
|
1265
|
+
img.alt = fileName || "Preview";
|
|
1266
|
+
const reader = new FileReader();
|
|
1267
|
+
reader.onload = (e) => {
|
|
1268
|
+
img.src = e.target?.result || "";
|
|
1269
|
+
};
|
|
1270
|
+
reader.readAsDataURL(file);
|
|
1271
|
+
container.appendChild(img);
|
|
1272
|
+
}
|
|
1273
|
+
function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
|
|
1274
|
+
const videoUrl = URL.createObjectURL(file);
|
|
1275
|
+
container.onclick = null;
|
|
1276
|
+
const newContainer = container.cloneNode(false);
|
|
1277
|
+
if (container.parentNode) {
|
|
1278
|
+
container.parentNode.replaceChild(newContainer, container);
|
|
1279
|
+
}
|
|
1280
|
+
newContainer.innerHTML = `
|
|
1281
|
+
<div class="relative group h-full">
|
|
1282
|
+
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
1283
|
+
<source src="${videoUrl}" type="${videoType}">
|
|
1284
|
+
Your browser does not support the video tag.
|
|
1285
|
+
</video>
|
|
1286
|
+
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
1287
|
+
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
1288
|
+
${t("removeElement", state)}
|
|
1289
|
+
</button>
|
|
1290
|
+
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
1291
|
+
Change
|
|
1292
|
+
</button>
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
`;
|
|
1296
|
+
attachVideoButtonHandlers(newContainer, resourceId, state, deps);
|
|
1297
|
+
return newContainer;
|
|
1298
|
+
}
|
|
1299
|
+
function attachVideoButtonHandlers(container, resourceId, state, deps) {
|
|
1300
|
+
const changeBtn = container.querySelector(".change-file-btn");
|
|
1301
|
+
if (changeBtn) {
|
|
1302
|
+
changeBtn.onclick = (e) => {
|
|
1303
|
+
e.stopPropagation();
|
|
1304
|
+
if (deps?.picker) {
|
|
1305
|
+
deps.picker.click();
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
const deleteBtn = container.querySelector(".delete-file-btn");
|
|
1310
|
+
if (deleteBtn) {
|
|
1311
|
+
deleteBtn.onclick = (e) => {
|
|
1312
|
+
e.stopPropagation();
|
|
1313
|
+
handleVideoDelete(container, resourceId, state, deps);
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function handleVideoDelete(container, resourceId, state, deps) {
|
|
1318
|
+
state.resourceIndex.delete(resourceId);
|
|
1319
|
+
const hiddenInput = container.parentElement?.querySelector(
|
|
1320
|
+
'input[type="hidden"]'
|
|
1321
|
+
);
|
|
1322
|
+
if (hiddenInput) {
|
|
1323
|
+
hiddenInput.value = "";
|
|
1324
|
+
}
|
|
1325
|
+
if (deps?.fileUploadHandler) {
|
|
1326
|
+
container.onclick = deps.fileUploadHandler;
|
|
1327
|
+
}
|
|
1328
|
+
if (deps?.dragHandler) {
|
|
1329
|
+
setupDragAndDrop(container, deps.dragHandler);
|
|
1330
|
+
}
|
|
1331
|
+
container.innerHTML = `
|
|
1332
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1333
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1334
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
1335
|
+
</svg>
|
|
1336
|
+
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
`;
|
|
1339
|
+
}
|
|
1340
|
+
function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
|
|
1341
|
+
const video = document.createElement("video");
|
|
1342
|
+
video.className = "w-full h-full object-contain";
|
|
1343
|
+
video.controls = true;
|
|
1344
|
+
video.preload = "metadata";
|
|
1345
|
+
video.muted = true;
|
|
1346
|
+
const source = document.createElement("source");
|
|
1347
|
+
source.src = thumbnailUrl;
|
|
1348
|
+
source.type = videoType;
|
|
1349
|
+
video.appendChild(source);
|
|
1350
|
+
video.appendChild(document.createTextNode("Your browser does not support the video tag."));
|
|
1351
|
+
container.appendChild(video);
|
|
1352
|
+
}
|
|
1353
|
+
function renderDeleteButton(container, resourceId, state) {
|
|
1354
|
+
addDeleteButton(container, state, () => {
|
|
1355
|
+
state.resourceIndex.delete(resourceId);
|
|
1356
|
+
const hiddenInput = container.parentElement?.querySelector(
|
|
1357
|
+
'input[type="hidden"]'
|
|
1358
|
+
);
|
|
1359
|
+
if (hiddenInput) {
|
|
1360
|
+
hiddenInput.value = "";
|
|
1361
|
+
}
|
|
1362
|
+
container.innerHTML = `
|
|
1363
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1364
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1365
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
1366
|
+
</svg>
|
|
1367
|
+
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1368
|
+
</div>
|
|
1369
|
+
`;
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
|
|
1373
|
+
if (!meta.file || !(meta.file instanceof File)) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (meta.type && meta.type.startsWith("image/")) {
|
|
1377
|
+
renderLocalImagePreview(container, meta.file, fileName);
|
|
1378
|
+
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
1379
|
+
const newContainer = renderLocalVideoPreview(
|
|
1380
|
+
container,
|
|
1381
|
+
meta.file,
|
|
1382
|
+
meta.type,
|
|
1383
|
+
resourceId,
|
|
1384
|
+
state,
|
|
1385
|
+
deps
|
|
1386
|
+
);
|
|
1387
|
+
container = newContainer;
|
|
1388
|
+
} else {
|
|
1389
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${fileName}</div></div>`;
|
|
1390
|
+
}
|
|
1391
|
+
if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
|
|
1392
|
+
renderDeleteButton(container, resourceId, state);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
|
|
1396
|
+
if (!state.config.getThumbnail) {
|
|
1397
|
+
setEmptyFileContainer(container, state);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1402
|
+
if (thumbnailUrl) {
|
|
1403
|
+
clear(container);
|
|
1404
|
+
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1405
|
+
renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
|
|
1406
|
+
} else {
|
|
1407
|
+
const img = document.createElement("img");
|
|
1408
|
+
img.className = "w-full h-full object-contain";
|
|
1409
|
+
img.alt = fileName || "Preview";
|
|
1410
|
+
img.src = thumbnailUrl;
|
|
1411
|
+
container.appendChild(img);
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
setEmptyFileContainer(container, state);
|
|
1415
|
+
}
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
console.error("Failed to get thumbnail:", error);
|
|
1418
|
+
container.innerHTML = `
|
|
1419
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1420
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1421
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
1422
|
+
</svg>
|
|
1423
|
+
<div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
|
|
1424
|
+
</div>
|
|
1425
|
+
`;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1225
1428
|
async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
1226
1429
|
const { fileName = "", isReadonly = false, deps = null } = options;
|
|
1227
1430
|
if (!isReadonly && deps && (!deps.picker || !deps.fileUploadHandler || !deps.dragHandler)) {
|
|
@@ -1233,141 +1436,19 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
1233
1436
|
if (isReadonly) {
|
|
1234
1437
|
container.classList.add("cursor-pointer");
|
|
1235
1438
|
}
|
|
1236
|
-
const img = document.createElement("img");
|
|
1237
|
-
img.className = "w-full h-full object-contain";
|
|
1238
|
-
img.alt = fileName || "Preview";
|
|
1239
1439
|
const meta = state.resourceIndex.get(resourceId);
|
|
1240
1440
|
if (meta && meta.file && meta.file instanceof File) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
container.onclick = null;
|
|
1251
|
-
const newContainer = container.cloneNode(false);
|
|
1252
|
-
if (container.parentNode) {
|
|
1253
|
-
container.parentNode.replaceChild(newContainer, container);
|
|
1254
|
-
}
|
|
1255
|
-
container = newContainer;
|
|
1256
|
-
container.innerHTML = `
|
|
1257
|
-
<div class="relative group h-full">
|
|
1258
|
-
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
1259
|
-
<source src="${videoUrl}" type="${meta.type}">
|
|
1260
|
-
Your browser does not support the video tag.
|
|
1261
|
-
</video>
|
|
1262
|
-
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
1263
|
-
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
1264
|
-
${t("removeElement", state)}
|
|
1265
|
-
</button>
|
|
1266
|
-
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
1267
|
-
Change
|
|
1268
|
-
</button>
|
|
1269
|
-
</div>
|
|
1270
|
-
</div>
|
|
1271
|
-
`;
|
|
1272
|
-
const changeBtn = container.querySelector(
|
|
1273
|
-
".change-file-btn"
|
|
1274
|
-
);
|
|
1275
|
-
if (changeBtn) {
|
|
1276
|
-
changeBtn.onclick = (e) => {
|
|
1277
|
-
e.stopPropagation();
|
|
1278
|
-
if (deps?.picker) {
|
|
1279
|
-
deps.picker.click();
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
const deleteBtn = container.querySelector(
|
|
1284
|
-
".delete-file-btn"
|
|
1285
|
-
);
|
|
1286
|
-
if (deleteBtn) {
|
|
1287
|
-
deleteBtn.onclick = (e) => {
|
|
1288
|
-
e.stopPropagation();
|
|
1289
|
-
state.resourceIndex.delete(resourceId);
|
|
1290
|
-
const hiddenInput = container.parentElement?.querySelector(
|
|
1291
|
-
'input[type="hidden"]'
|
|
1292
|
-
);
|
|
1293
|
-
if (hiddenInput) {
|
|
1294
|
-
hiddenInput.value = "";
|
|
1295
|
-
}
|
|
1296
|
-
if (deps?.fileUploadHandler) {
|
|
1297
|
-
container.onclick = deps.fileUploadHandler;
|
|
1298
|
-
}
|
|
1299
|
-
if (deps?.dragHandler) {
|
|
1300
|
-
setupDragAndDrop(container, deps.dragHandler);
|
|
1301
|
-
}
|
|
1302
|
-
container.innerHTML = `
|
|
1303
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1304
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1305
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
1306
|
-
</svg>
|
|
1307
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1308
|
-
</div>
|
|
1309
|
-
`;
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
} else {
|
|
1313
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${fileName}</div></div>`;
|
|
1314
|
-
}
|
|
1315
|
-
if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
|
|
1316
|
-
addDeleteButton(container, state, () => {
|
|
1317
|
-
state.resourceIndex.delete(resourceId);
|
|
1318
|
-
const hiddenInput = container.parentElement?.querySelector(
|
|
1319
|
-
'input[type="hidden"]'
|
|
1320
|
-
);
|
|
1321
|
-
if (hiddenInput) {
|
|
1322
|
-
hiddenInput.value = "";
|
|
1323
|
-
}
|
|
1324
|
-
container.innerHTML = `
|
|
1325
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1326
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1327
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
1328
|
-
</svg>
|
|
1329
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1330
|
-
</div>
|
|
1331
|
-
`;
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
} else if (state.config.getThumbnail) {
|
|
1335
|
-
try {
|
|
1336
|
-
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1337
|
-
if (thumbnailUrl) {
|
|
1338
|
-
clear(container);
|
|
1339
|
-
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1340
|
-
const video = document.createElement("video");
|
|
1341
|
-
video.className = "w-full h-full object-contain";
|
|
1342
|
-
video.controls = true;
|
|
1343
|
-
video.preload = "metadata";
|
|
1344
|
-
video.muted = true;
|
|
1345
|
-
const source = document.createElement("source");
|
|
1346
|
-
source.src = thumbnailUrl;
|
|
1347
|
-
source.type = meta.type;
|
|
1348
|
-
video.appendChild(source);
|
|
1349
|
-
video.appendChild(document.createTextNode("Your browser does not support the video tag."));
|
|
1350
|
-
container.appendChild(video);
|
|
1351
|
-
} else {
|
|
1352
|
-
img.src = thumbnailUrl;
|
|
1353
|
-
container.appendChild(img);
|
|
1354
|
-
}
|
|
1355
|
-
} else {
|
|
1356
|
-
setEmptyFileContainer(container, state);
|
|
1357
|
-
}
|
|
1358
|
-
} catch (error) {
|
|
1359
|
-
console.error("Failed to get thumbnail:", error);
|
|
1360
|
-
container.innerHTML = `
|
|
1361
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1362
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1363
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
1364
|
-
</svg>
|
|
1365
|
-
<div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
|
|
1366
|
-
</div>
|
|
1367
|
-
`;
|
|
1368
|
-
}
|
|
1441
|
+
await renderLocalFilePreview(
|
|
1442
|
+
container,
|
|
1443
|
+
meta,
|
|
1444
|
+
fileName,
|
|
1445
|
+
resourceId,
|
|
1446
|
+
isReadonly,
|
|
1447
|
+
state,
|
|
1448
|
+
deps
|
|
1449
|
+
);
|
|
1369
1450
|
} else {
|
|
1370
|
-
|
|
1451
|
+
await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
|
|
1371
1452
|
}
|
|
1372
1453
|
}
|
|
1373
1454
|
async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
@@ -2268,114 +2349,1121 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
2268
2349
|
}
|
|
2269
2350
|
}
|
|
2270
2351
|
|
|
2271
|
-
// src/components/
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2352
|
+
// src/components/colour.ts
|
|
2353
|
+
function normalizeColourValue(value) {
|
|
2354
|
+
if (!value) return "#000000";
|
|
2355
|
+
return value.toUpperCase();
|
|
2275
2356
|
}
|
|
2276
|
-
function
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2357
|
+
function isValidHexColour(value) {
|
|
2358
|
+
return /^#[0-9A-F]{6}$/i.test(value) || /^#[0-9A-F]{3}$/i.test(value);
|
|
2359
|
+
}
|
|
2360
|
+
function expandHexColour(value) {
|
|
2361
|
+
if (/^#[0-9A-F]{3}$/i.test(value)) {
|
|
2362
|
+
const r = value[1];
|
|
2363
|
+
const g = value[2];
|
|
2364
|
+
const b = value[3];
|
|
2365
|
+
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
|
2281
2366
|
}
|
|
2282
|
-
return
|
|
2367
|
+
return value.toUpperCase();
|
|
2283
2368
|
}
|
|
2284
|
-
function
|
|
2285
|
-
const
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
const
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2369
|
+
function createReadonlyColourUI(value) {
|
|
2370
|
+
const container = document.createElement("div");
|
|
2371
|
+
container.className = "flex items-center gap-2";
|
|
2372
|
+
const normalizedValue = normalizeColourValue(value);
|
|
2373
|
+
const swatch = document.createElement("div");
|
|
2374
|
+
swatch.style.cssText = `
|
|
2375
|
+
width: 32px;
|
|
2376
|
+
height: 32px;
|
|
2377
|
+
border-radius: var(--fb-border-radius);
|
|
2378
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2379
|
+
background-color: ${normalizedValue};
|
|
2380
|
+
`;
|
|
2381
|
+
const hexText = document.createElement("span");
|
|
2382
|
+
hexText.style.cssText = `
|
|
2383
|
+
font-size: var(--fb-font-size);
|
|
2384
|
+
color: var(--fb-text-color);
|
|
2385
|
+
font-family: var(--fb-font-family-mono, monospace);
|
|
2386
|
+
`;
|
|
2387
|
+
hexText.textContent = normalizedValue;
|
|
2388
|
+
container.appendChild(swatch);
|
|
2389
|
+
container.appendChild(hexText);
|
|
2390
|
+
return container;
|
|
2391
|
+
}
|
|
2392
|
+
function createEditColourUI(value, pathKey, ctx) {
|
|
2393
|
+
const normalizedValue = normalizeColourValue(value);
|
|
2394
|
+
const pickerWrapper = document.createElement("div");
|
|
2395
|
+
pickerWrapper.className = "colour-picker-wrapper";
|
|
2396
|
+
pickerWrapper.style.cssText = `
|
|
2397
|
+
display: flex;
|
|
2398
|
+
align-items: center;
|
|
2399
|
+
gap: 8px;
|
|
2400
|
+
`;
|
|
2401
|
+
const swatch = document.createElement("div");
|
|
2402
|
+
swatch.className = "colour-swatch";
|
|
2403
|
+
swatch.style.cssText = `
|
|
2404
|
+
width: 40px;
|
|
2405
|
+
height: 40px;
|
|
2406
|
+
border-radius: var(--fb-border-radius);
|
|
2407
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2408
|
+
background-color: ${normalizedValue};
|
|
2409
|
+
cursor: pointer;
|
|
2410
|
+
transition: border-color var(--fb-transition-duration) ease-in-out;
|
|
2411
|
+
flex-shrink: 0;
|
|
2412
|
+
`;
|
|
2413
|
+
const hexInput = document.createElement("input");
|
|
2414
|
+
hexInput.type = "text";
|
|
2415
|
+
hexInput.className = "colour-hex-input";
|
|
2416
|
+
hexInput.name = pathKey;
|
|
2417
|
+
hexInput.value = normalizedValue;
|
|
2418
|
+
hexInput.placeholder = "#000000";
|
|
2419
|
+
hexInput.style.cssText = `
|
|
2420
|
+
width: 100px;
|
|
2421
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
2422
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2423
|
+
border-radius: var(--fb-border-radius);
|
|
2424
|
+
background-color: var(--fb-background-color);
|
|
2425
|
+
color: var(--fb-text-color);
|
|
2426
|
+
font-size: var(--fb-font-size);
|
|
2427
|
+
font-family: var(--fb-font-family-mono, monospace);
|
|
2428
|
+
transition: all var(--fb-transition-duration) ease-in-out;
|
|
2429
|
+
`;
|
|
2430
|
+
const colourInput = document.createElement("input");
|
|
2431
|
+
colourInput.type = "color";
|
|
2432
|
+
colourInput.className = "colour-picker-hidden";
|
|
2433
|
+
colourInput.value = normalizedValue.toLowerCase();
|
|
2434
|
+
colourInput.style.cssText = `
|
|
2435
|
+
position: absolute;
|
|
2436
|
+
opacity: 0;
|
|
2437
|
+
pointer-events: none;
|
|
2438
|
+
`;
|
|
2439
|
+
hexInput.addEventListener("input", () => {
|
|
2440
|
+
const inputValue = hexInput.value.trim();
|
|
2441
|
+
if (isValidHexColour(inputValue)) {
|
|
2442
|
+
const expanded = expandHexColour(inputValue);
|
|
2443
|
+
swatch.style.backgroundColor = expanded;
|
|
2444
|
+
colourInput.value = expanded.toLowerCase();
|
|
2445
|
+
hexInput.classList.remove("invalid");
|
|
2446
|
+
if (ctx.instance) {
|
|
2447
|
+
ctx.instance.triggerOnChange(pathKey, expanded);
|
|
2448
|
+
}
|
|
2449
|
+
} else {
|
|
2450
|
+
hexInput.classList.add("invalid");
|
|
2307
2451
|
}
|
|
2308
2452
|
});
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2453
|
+
hexInput.addEventListener("blur", () => {
|
|
2454
|
+
const inputValue = hexInput.value.trim();
|
|
2455
|
+
if (isValidHexColour(inputValue)) {
|
|
2456
|
+
const expanded = expandHexColour(inputValue);
|
|
2457
|
+
hexInput.value = expanded;
|
|
2458
|
+
swatch.style.backgroundColor = expanded;
|
|
2459
|
+
colourInput.value = expanded.toLowerCase();
|
|
2460
|
+
hexInput.classList.remove("invalid");
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2463
|
+
colourInput.addEventListener("change", () => {
|
|
2464
|
+
const normalized = normalizeColourValue(colourInput.value);
|
|
2465
|
+
hexInput.value = normalized;
|
|
2466
|
+
swatch.style.backgroundColor = normalized;
|
|
2467
|
+
if (ctx.instance) {
|
|
2468
|
+
ctx.instance.triggerOnChange(pathKey, normalized);
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
swatch.addEventListener("click", () => {
|
|
2472
|
+
colourInput.click();
|
|
2473
|
+
});
|
|
2474
|
+
swatch.addEventListener("mouseenter", () => {
|
|
2475
|
+
swatch.style.borderColor = "var(--fb-border-hover-color)";
|
|
2476
|
+
});
|
|
2477
|
+
swatch.addEventListener("mouseleave", () => {
|
|
2478
|
+
swatch.style.borderColor = "var(--fb-border-color)";
|
|
2479
|
+
});
|
|
2480
|
+
hexInput.addEventListener("focus", () => {
|
|
2481
|
+
hexInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
2482
|
+
hexInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
2483
|
+
hexInput.style.outlineOffset = "0";
|
|
2484
|
+
});
|
|
2485
|
+
hexInput.addEventListener("blur", () => {
|
|
2486
|
+
hexInput.style.borderColor = "var(--fb-border-color)";
|
|
2487
|
+
hexInput.style.outline = "none";
|
|
2488
|
+
});
|
|
2489
|
+
hexInput.addEventListener("mouseenter", () => {
|
|
2490
|
+
if (document.activeElement !== hexInput) {
|
|
2491
|
+
hexInput.style.borderColor = "var(--fb-border-hover-color)";
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
hexInput.addEventListener("mouseleave", () => {
|
|
2495
|
+
if (document.activeElement !== hexInput) {
|
|
2496
|
+
hexInput.style.borderColor = "var(--fb-border-color)";
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
pickerWrapper.appendChild(swatch);
|
|
2500
|
+
pickerWrapper.appendChild(hexInput);
|
|
2501
|
+
pickerWrapper.appendChild(colourInput);
|
|
2502
|
+
return pickerWrapper;
|
|
2312
2503
|
}
|
|
2313
|
-
function
|
|
2504
|
+
function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
2314
2505
|
const state = ctx.state;
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
right.className = "flex gap-2";
|
|
2323
|
-
const itemsWrap = document.createElement("div");
|
|
2324
|
-
itemsWrap.className = "space-y-4";
|
|
2325
|
-
containerWrap.appendChild(header);
|
|
2326
|
-
header.appendChild(left);
|
|
2327
|
-
if (!state.config.readonly) {
|
|
2328
|
-
header.appendChild(right);
|
|
2506
|
+
const initialValue = ctx.prefill[element.key] || element.default || "#000000";
|
|
2507
|
+
if (state.config.readonly) {
|
|
2508
|
+
const readonlyUI = createReadonlyColourUI(initialValue);
|
|
2509
|
+
wrapper.appendChild(readonlyUI);
|
|
2510
|
+
} else {
|
|
2511
|
+
const editUI = createEditColourUI(initialValue, pathKey, ctx);
|
|
2512
|
+
wrapper.appendChild(editUI);
|
|
2329
2513
|
}
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2514
|
+
const colourHint = document.createElement("p");
|
|
2515
|
+
colourHint.className = "mt-1";
|
|
2516
|
+
colourHint.style.cssText = `
|
|
2517
|
+
font-size: var(--fb-font-size-small);
|
|
2518
|
+
color: var(--fb-text-secondary-color);
|
|
2519
|
+
`;
|
|
2520
|
+
colourHint.textContent = makeFieldHint(element);
|
|
2521
|
+
wrapper.appendChild(colourHint);
|
|
2522
|
+
}
|
|
2523
|
+
function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
2524
|
+
const state = ctx.state;
|
|
2525
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
2526
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2527
|
+
const minCount = element.minCount ?? 1;
|
|
2528
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
2529
|
+
while (values.length < minCount) {
|
|
2530
|
+
values.push(element.default || "#000000");
|
|
2531
|
+
}
|
|
2532
|
+
const container = document.createElement("div");
|
|
2533
|
+
container.className = "space-y-2";
|
|
2534
|
+
wrapper.appendChild(container);
|
|
2535
|
+
function updateIndices() {
|
|
2536
|
+
const items = container.querySelectorAll(".multiple-colour-item");
|
|
2537
|
+
items.forEach((item, index) => {
|
|
2538
|
+
const input = item.querySelector("input");
|
|
2539
|
+
if (input) {
|
|
2540
|
+
input.name = `${pathKey}[${index}]`;
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
function addColourItem(value = "#000000", index = -1) {
|
|
2545
|
+
const itemWrapper = document.createElement("div");
|
|
2546
|
+
itemWrapper.className = "multiple-colour-item flex items-center gap-2";
|
|
2547
|
+
if (state.config.readonly) {
|
|
2548
|
+
const readonlyUI = createReadonlyColourUI(value);
|
|
2549
|
+
while (readonlyUI.firstChild) {
|
|
2550
|
+
itemWrapper.appendChild(readonlyUI.firstChild);
|
|
2551
|
+
}
|
|
2552
|
+
} else {
|
|
2553
|
+
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
2554
|
+
const editUI = createEditColourUI(value, tempPathKey, ctx);
|
|
2555
|
+
editUI.style.flex = "1";
|
|
2556
|
+
itemWrapper.appendChild(editUI);
|
|
2557
|
+
}
|
|
2558
|
+
if (index === -1) {
|
|
2559
|
+
container.appendChild(itemWrapper);
|
|
2560
|
+
} else {
|
|
2561
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
2562
|
+
}
|
|
2563
|
+
updateIndices();
|
|
2564
|
+
return itemWrapper;
|
|
2565
|
+
}
|
|
2566
|
+
function updateRemoveButtons() {
|
|
2567
|
+
if (state.config.readonly) return;
|
|
2568
|
+
const items = container.querySelectorAll(".multiple-colour-item");
|
|
2569
|
+
const currentCount = items.length;
|
|
2570
|
+
items.forEach((item) => {
|
|
2571
|
+
let removeBtn = item.querySelector(
|
|
2572
|
+
".remove-item-btn"
|
|
2573
|
+
);
|
|
2574
|
+
if (!removeBtn) {
|
|
2575
|
+
removeBtn = document.createElement("button");
|
|
2576
|
+
removeBtn.type = "button";
|
|
2577
|
+
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
2578
|
+
removeBtn.style.cssText = `
|
|
2579
|
+
color: var(--fb-error-color);
|
|
2580
|
+
background-color: transparent;
|
|
2581
|
+
transition: background-color var(--fb-transition-duration);
|
|
2582
|
+
`;
|
|
2583
|
+
removeBtn.innerHTML = "\u2715";
|
|
2584
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
2585
|
+
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2586
|
+
});
|
|
2587
|
+
removeBtn.addEventListener("mouseleave", () => {
|
|
2588
|
+
removeBtn.style.backgroundColor = "transparent";
|
|
2589
|
+
});
|
|
2590
|
+
removeBtn.onclick = () => {
|
|
2591
|
+
const currentIndex = Array.from(container.children).indexOf(
|
|
2592
|
+
item
|
|
2593
|
+
);
|
|
2594
|
+
if (container.children.length > minCount) {
|
|
2595
|
+
values.splice(currentIndex, 1);
|
|
2596
|
+
item.remove();
|
|
2597
|
+
updateIndices();
|
|
2598
|
+
updateAddButton();
|
|
2599
|
+
updateRemoveButtons();
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
item.appendChild(removeBtn);
|
|
2603
|
+
}
|
|
2604
|
+
const disabled = currentCount <= minCount;
|
|
2605
|
+
removeBtn.disabled = disabled;
|
|
2606
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2607
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
function updateAddButton() {
|
|
2611
|
+
const existingAddBtn = wrapper.querySelector(".add-colour-btn");
|
|
2612
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
2613
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
2614
|
+
const addBtn = document.createElement("button");
|
|
2615
|
+
addBtn.type = "button";
|
|
2616
|
+
addBtn.className = "add-colour-btn mt-2 px-3 py-1 rounded";
|
|
2617
|
+
addBtn.style.cssText = `
|
|
2618
|
+
color: var(--fb-primary-color);
|
|
2619
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
2620
|
+
background-color: transparent;
|
|
2621
|
+
font-size: var(--fb-font-size);
|
|
2622
|
+
transition: all var(--fb-transition-duration);
|
|
2623
|
+
`;
|
|
2624
|
+
addBtn.textContent = `+ Add ${element.label || "Colour"}`;
|
|
2625
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
2626
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2627
|
+
});
|
|
2628
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
2629
|
+
addBtn.style.backgroundColor = "transparent";
|
|
2630
|
+
});
|
|
2631
|
+
addBtn.onclick = () => {
|
|
2632
|
+
const defaultColour = element.default || "#000000";
|
|
2633
|
+
values.push(defaultColour);
|
|
2634
|
+
addColourItem(defaultColour);
|
|
2635
|
+
updateAddButton();
|
|
2636
|
+
updateRemoveButtons();
|
|
2637
|
+
};
|
|
2638
|
+
wrapper.appendChild(addBtn);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
values.forEach((value) => addColourItem(value));
|
|
2642
|
+
updateAddButton();
|
|
2643
|
+
updateRemoveButtons();
|
|
2644
|
+
const hint = document.createElement("p");
|
|
2645
|
+
hint.className = "mt-1";
|
|
2646
|
+
hint.style.cssText = `
|
|
2647
|
+
font-size: var(--fb-font-size-small);
|
|
2648
|
+
color: var(--fb-text-secondary-color);
|
|
2649
|
+
`;
|
|
2650
|
+
hint.textContent = makeFieldHint(element);
|
|
2651
|
+
wrapper.appendChild(hint);
|
|
2652
|
+
}
|
|
2653
|
+
function validateColourElement(element, key, context) {
|
|
2654
|
+
const errors = [];
|
|
2655
|
+
const { scopeRoot, skipValidation } = context;
|
|
2656
|
+
const markValidity = (input, errorMessage) => {
|
|
2657
|
+
if (!input) return;
|
|
2658
|
+
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
2659
|
+
let errorElement = document.getElementById(errorId);
|
|
2660
|
+
if (errorMessage) {
|
|
2661
|
+
input.classList.add("invalid");
|
|
2662
|
+
input.title = errorMessage;
|
|
2663
|
+
if (!errorElement) {
|
|
2664
|
+
errorElement = document.createElement("div");
|
|
2665
|
+
errorElement.id = errorId;
|
|
2666
|
+
errorElement.className = "error-message";
|
|
2667
|
+
errorElement.style.cssText = `
|
|
2668
|
+
color: var(--fb-error-color);
|
|
2669
|
+
font-size: var(--fb-font-size-small);
|
|
2670
|
+
margin-top: 0.25rem;
|
|
2671
|
+
`;
|
|
2672
|
+
if (input.nextSibling) {
|
|
2673
|
+
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
2674
|
+
} else {
|
|
2675
|
+
input.parentNode?.appendChild(errorElement);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
errorElement.textContent = errorMessage;
|
|
2679
|
+
errorElement.style.display = "block";
|
|
2680
|
+
} else {
|
|
2681
|
+
input.classList.remove("invalid");
|
|
2682
|
+
input.title = "";
|
|
2683
|
+
if (errorElement) {
|
|
2684
|
+
errorElement.remove();
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
const validateColourValue = (input, val, fieldKey) => {
|
|
2689
|
+
if (!val) {
|
|
2690
|
+
if (!skipValidation && element.required) {
|
|
2691
|
+
errors.push(`${fieldKey}: required`);
|
|
2692
|
+
markValidity(input, "required");
|
|
2693
|
+
return "";
|
|
2694
|
+
}
|
|
2695
|
+
markValidity(input, null);
|
|
2696
|
+
return "";
|
|
2697
|
+
}
|
|
2698
|
+
const normalized = normalizeColourValue(val);
|
|
2699
|
+
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
2700
|
+
errors.push(`${fieldKey}: invalid hex colour format`);
|
|
2701
|
+
markValidity(input, "invalid hex colour format");
|
|
2702
|
+
return val;
|
|
2703
|
+
}
|
|
2704
|
+
markValidity(input, null);
|
|
2705
|
+
return normalized;
|
|
2706
|
+
};
|
|
2707
|
+
if (element.multiple) {
|
|
2708
|
+
const hexInputs = scopeRoot.querySelectorAll(
|
|
2709
|
+
`.colour-hex-input`
|
|
2710
|
+
);
|
|
2711
|
+
const values = [];
|
|
2712
|
+
hexInputs.forEach((input, index) => {
|
|
2713
|
+
const val = input?.value ?? "";
|
|
2714
|
+
const validated = validateColourValue(input, val, `${key}[${index}]`);
|
|
2715
|
+
values.push(validated);
|
|
2716
|
+
});
|
|
2717
|
+
if (!skipValidation) {
|
|
2718
|
+
const minCount = element.minCount ?? 1;
|
|
2719
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
2720
|
+
const filteredValues = values.filter((v) => v !== "");
|
|
2721
|
+
if (element.required && filteredValues.length === 0) {
|
|
2722
|
+
errors.push(`${key}: required`);
|
|
2723
|
+
}
|
|
2724
|
+
if (filteredValues.length < minCount) {
|
|
2725
|
+
errors.push(`${key}: minimum ${minCount} items required`);
|
|
2726
|
+
}
|
|
2727
|
+
if (filteredValues.length > maxCount) {
|
|
2728
|
+
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
return { value: values, errors };
|
|
2732
|
+
} else {
|
|
2733
|
+
const hexInput = scopeRoot.querySelector(
|
|
2734
|
+
`[name="${key}"].colour-hex-input`
|
|
2735
|
+
);
|
|
2736
|
+
const val = hexInput?.value ?? "";
|
|
2737
|
+
if (!skipValidation && element.required && val === "") {
|
|
2738
|
+
errors.push(`${key}: required`);
|
|
2739
|
+
markValidity(hexInput, "required");
|
|
2740
|
+
return { value: "", errors };
|
|
2741
|
+
}
|
|
2742
|
+
const validated = validateColourValue(hexInput, val, key);
|
|
2743
|
+
return { value: validated, errors };
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function updateColourField(element, fieldPath, value, context) {
|
|
2747
|
+
const { scopeRoot } = context;
|
|
2748
|
+
if (element.multiple) {
|
|
2749
|
+
if (!Array.isArray(value)) {
|
|
2750
|
+
console.warn(
|
|
2751
|
+
`updateColourField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
|
|
2752
|
+
);
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
const hexInputs = scopeRoot.querySelectorAll(
|
|
2756
|
+
`.colour-hex-input`
|
|
2757
|
+
);
|
|
2758
|
+
hexInputs.forEach((hexInput, index) => {
|
|
2759
|
+
if (index < value.length) {
|
|
2760
|
+
const normalized = normalizeColourValue(value[index]);
|
|
2761
|
+
hexInput.value = normalized;
|
|
2762
|
+
hexInput.classList.remove("invalid");
|
|
2763
|
+
hexInput.title = "";
|
|
2764
|
+
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
2765
|
+
if (wrapper) {
|
|
2766
|
+
const swatch = wrapper.querySelector(".colour-swatch");
|
|
2767
|
+
const colourInput = wrapper.querySelector(".colour-picker-hidden");
|
|
2768
|
+
if (swatch) {
|
|
2769
|
+
swatch.style.backgroundColor = normalized;
|
|
2770
|
+
}
|
|
2771
|
+
if (colourInput) {
|
|
2772
|
+
colourInput.value = normalized.toLowerCase();
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
if (value.length !== hexInputs.length) {
|
|
2778
|
+
console.warn(
|
|
2779
|
+
`updateColourField: Multiple field "${fieldPath}" has ${hexInputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
} else {
|
|
2783
|
+
const hexInput = scopeRoot.querySelector(
|
|
2784
|
+
`[name="${fieldPath}"].colour-hex-input`
|
|
2785
|
+
);
|
|
2786
|
+
if (hexInput) {
|
|
2787
|
+
const normalized = normalizeColourValue(value);
|
|
2788
|
+
hexInput.value = normalized;
|
|
2789
|
+
hexInput.classList.remove("invalid");
|
|
2790
|
+
hexInput.title = "";
|
|
2791
|
+
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
2792
|
+
if (wrapper) {
|
|
2793
|
+
const swatch = wrapper.querySelector(".colour-swatch");
|
|
2794
|
+
const colourInput = wrapper.querySelector(".colour-picker-hidden");
|
|
2795
|
+
if (swatch) {
|
|
2796
|
+
swatch.style.backgroundColor = normalized;
|
|
2797
|
+
}
|
|
2798
|
+
if (colourInput) {
|
|
2799
|
+
colourInput.value = normalized.toLowerCase();
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// src/components/slider.ts
|
|
2807
|
+
function positionToExponential(position, min, max) {
|
|
2808
|
+
if (min <= 0) {
|
|
2809
|
+
throw new Error("Exponential scale requires min > 0");
|
|
2810
|
+
}
|
|
2811
|
+
const logMin = Math.log(min);
|
|
2812
|
+
const logMax = Math.log(max);
|
|
2813
|
+
return Math.exp(logMin + position * (logMax - logMin));
|
|
2814
|
+
}
|
|
2815
|
+
function exponentialToPosition(value, min, max) {
|
|
2816
|
+
if (min <= 0) {
|
|
2817
|
+
throw new Error("Exponential scale requires min > 0");
|
|
2818
|
+
}
|
|
2819
|
+
const logMin = Math.log(min);
|
|
2820
|
+
const logMax = Math.log(max);
|
|
2821
|
+
const logValue = Math.log(value);
|
|
2822
|
+
return (logValue - logMin) / (logMax - logMin);
|
|
2823
|
+
}
|
|
2824
|
+
function alignToStep(value, step) {
|
|
2825
|
+
return Math.round(value / step) * step;
|
|
2826
|
+
}
|
|
2827
|
+
function createSliderUI(value, pathKey, element, ctx, readonly) {
|
|
2828
|
+
const container = document.createElement("div");
|
|
2829
|
+
container.className = "slider-container space-y-2";
|
|
2830
|
+
const sliderRow = document.createElement("div");
|
|
2831
|
+
sliderRow.className = "flex items-center gap-3";
|
|
2832
|
+
const slider = document.createElement("input");
|
|
2833
|
+
slider.type = "range";
|
|
2834
|
+
slider.name = pathKey;
|
|
2835
|
+
slider.className = "slider-input flex-1";
|
|
2836
|
+
slider.disabled = readonly;
|
|
2837
|
+
const scale = element.scale || "linear";
|
|
2838
|
+
const min = element.min;
|
|
2839
|
+
const max = element.max;
|
|
2840
|
+
const step = element.step ?? 1;
|
|
2841
|
+
if (scale === "exponential") {
|
|
2842
|
+
if (min <= 0) {
|
|
2843
|
+
throw new Error(
|
|
2844
|
+
`Slider "${element.key}": exponential scale requires min > 0 (got ${min})`
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
slider.min = "0";
|
|
2848
|
+
slider.max = "1000";
|
|
2849
|
+
slider.step = "1";
|
|
2850
|
+
const position = exponentialToPosition(value, min, max);
|
|
2851
|
+
slider.value = (position * 1e3).toString();
|
|
2852
|
+
} else {
|
|
2853
|
+
slider.min = min.toString();
|
|
2854
|
+
slider.max = max.toString();
|
|
2855
|
+
slider.step = step.toString();
|
|
2856
|
+
slider.value = value.toString();
|
|
2857
|
+
}
|
|
2858
|
+
slider.style.cssText = `
|
|
2859
|
+
height: 6px;
|
|
2860
|
+
border-radius: 3px;
|
|
2861
|
+
background: linear-gradient(
|
|
2862
|
+
to right,
|
|
2863
|
+
var(--fb-primary-color) 0%,
|
|
2864
|
+
var(--fb-primary-color) ${(value - min) / (max - min) * 100}%,
|
|
2865
|
+
var(--fb-border-color) ${(value - min) / (max - min) * 100}%,
|
|
2866
|
+
var(--fb-border-color) 100%
|
|
2867
|
+
);
|
|
2868
|
+
outline: none;
|
|
2869
|
+
transition: background 0.1s ease-in-out;
|
|
2870
|
+
cursor: ${readonly ? "not-allowed" : "pointer"};
|
|
2871
|
+
opacity: ${readonly ? "0.6" : "1"};
|
|
2872
|
+
`;
|
|
2873
|
+
const valueDisplay = document.createElement("span");
|
|
2874
|
+
valueDisplay.className = "slider-value";
|
|
2875
|
+
valueDisplay.style.cssText = `
|
|
2876
|
+
min-width: 60px;
|
|
2877
|
+
text-align: right;
|
|
2878
|
+
font-size: var(--fb-font-size);
|
|
2879
|
+
color: var(--fb-text-color);
|
|
2880
|
+
font-family: var(--fb-font-family-mono, monospace);
|
|
2881
|
+
font-weight: 500;
|
|
2882
|
+
`;
|
|
2883
|
+
valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
|
|
2884
|
+
sliderRow.appendChild(slider);
|
|
2885
|
+
sliderRow.appendChild(valueDisplay);
|
|
2886
|
+
container.appendChild(sliderRow);
|
|
2887
|
+
const labelsRow = document.createElement("div");
|
|
2888
|
+
labelsRow.className = "flex justify-between";
|
|
2889
|
+
labelsRow.style.cssText = `
|
|
2890
|
+
font-size: var(--fb-font-size-small);
|
|
2891
|
+
color: var(--fb-text-secondary-color);
|
|
2892
|
+
`;
|
|
2893
|
+
const minLabel = document.createElement("span");
|
|
2894
|
+
minLabel.textContent = min.toString();
|
|
2895
|
+
const maxLabel = document.createElement("span");
|
|
2896
|
+
maxLabel.textContent = max.toString();
|
|
2897
|
+
labelsRow.appendChild(minLabel);
|
|
2898
|
+
labelsRow.appendChild(maxLabel);
|
|
2899
|
+
container.appendChild(labelsRow);
|
|
2900
|
+
if (!readonly) {
|
|
2901
|
+
const updateValue = () => {
|
|
2902
|
+
let displayValue;
|
|
2903
|
+
if (scale === "exponential") {
|
|
2904
|
+
const position = parseFloat(slider.value) / 1e3;
|
|
2905
|
+
displayValue = positionToExponential(position, min, max);
|
|
2906
|
+
displayValue = alignToStep(displayValue, step);
|
|
2907
|
+
displayValue = Math.max(min, Math.min(max, displayValue));
|
|
2908
|
+
} else {
|
|
2909
|
+
displayValue = parseFloat(slider.value);
|
|
2910
|
+
displayValue = alignToStep(displayValue, step);
|
|
2911
|
+
}
|
|
2912
|
+
valueDisplay.textContent = displayValue.toFixed(step < 1 ? 2 : 0);
|
|
2913
|
+
const percentage = (displayValue - min) / (max - min) * 100;
|
|
2914
|
+
slider.style.background = `linear-gradient(
|
|
2915
|
+
to right,
|
|
2916
|
+
var(--fb-primary-color) 0%,
|
|
2917
|
+
var(--fb-primary-color) ${percentage}%,
|
|
2918
|
+
var(--fb-border-color) ${percentage}%,
|
|
2919
|
+
var(--fb-border-color) 100%
|
|
2920
|
+
)`;
|
|
2921
|
+
if (ctx.instance) {
|
|
2922
|
+
ctx.instance.triggerOnChange(pathKey, displayValue);
|
|
2923
|
+
}
|
|
2924
|
+
};
|
|
2925
|
+
slider.addEventListener("input", updateValue);
|
|
2926
|
+
slider.addEventListener("change", updateValue);
|
|
2927
|
+
}
|
|
2928
|
+
return container;
|
|
2929
|
+
}
|
|
2930
|
+
function renderSliderElement(element, ctx, wrapper, pathKey) {
|
|
2931
|
+
if (element.min === void 0 || element.min === null) {
|
|
2932
|
+
throw new Error(
|
|
2933
|
+
`Slider field "${element.key}" requires "min" property`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
if (element.max === void 0 || element.max === null) {
|
|
2937
|
+
throw new Error(
|
|
2938
|
+
`Slider field "${element.key}" requires "max" property`
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
if (element.min >= element.max) {
|
|
2942
|
+
throw new Error(
|
|
2943
|
+
`Slider field "${element.key}": min (${element.min}) must be less than max (${element.max})`
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
const state = ctx.state;
|
|
2947
|
+
const defaultValue = element.default !== void 0 ? element.default : (element.min + element.max) / 2;
|
|
2948
|
+
const initialValue = ctx.prefill[element.key] ?? defaultValue;
|
|
2949
|
+
const sliderUI = createSliderUI(
|
|
2950
|
+
initialValue,
|
|
2951
|
+
pathKey,
|
|
2952
|
+
element,
|
|
2953
|
+
ctx,
|
|
2954
|
+
state.config.readonly
|
|
2955
|
+
);
|
|
2956
|
+
wrapper.appendChild(sliderUI);
|
|
2957
|
+
const hint = document.createElement("p");
|
|
2958
|
+
hint.className = "mt-1";
|
|
2959
|
+
hint.style.cssText = `
|
|
2960
|
+
font-size: var(--fb-font-size-small);
|
|
2961
|
+
color: var(--fb-text-secondary-color);
|
|
2962
|
+
`;
|
|
2963
|
+
hint.textContent = makeFieldHint(element);
|
|
2964
|
+
wrapper.appendChild(hint);
|
|
2965
|
+
}
|
|
2966
|
+
function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
2967
|
+
if (element.min === void 0 || element.min === null) {
|
|
2968
|
+
throw new Error(
|
|
2969
|
+
`Slider field "${element.key}" requires "min" property`
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2972
|
+
if (element.max === void 0 || element.max === null) {
|
|
2973
|
+
throw new Error(
|
|
2974
|
+
`Slider field "${element.key}" requires "max" property`
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
if (element.min >= element.max) {
|
|
2978
|
+
throw new Error(
|
|
2979
|
+
`Slider field "${element.key}": min (${element.min}) must be less than max (${element.max})`
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
const state = ctx.state;
|
|
2983
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
2984
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2985
|
+
const minCount = element.minCount ?? 1;
|
|
2986
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
2987
|
+
const defaultValue = element.default !== void 0 ? element.default : (element.min + element.max) / 2;
|
|
2988
|
+
while (values.length < minCount) {
|
|
2989
|
+
values.push(defaultValue);
|
|
2990
|
+
}
|
|
2991
|
+
const container = document.createElement("div");
|
|
2992
|
+
container.className = "space-y-3";
|
|
2993
|
+
wrapper.appendChild(container);
|
|
2994
|
+
function updateIndices() {
|
|
2995
|
+
const items = container.querySelectorAll(".multiple-slider-item");
|
|
2996
|
+
items.forEach((item, index) => {
|
|
2997
|
+
const slider = item.querySelector("input[type=range]");
|
|
2998
|
+
if (slider) {
|
|
2999
|
+
slider.setAttribute("name", `${pathKey}[${index}]`);
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
function addSliderItem(value = defaultValue, index = -1) {
|
|
3004
|
+
const itemWrapper = document.createElement("div");
|
|
3005
|
+
itemWrapper.className = "multiple-slider-item flex items-start gap-2";
|
|
3006
|
+
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
3007
|
+
const sliderUI = createSliderUI(
|
|
3008
|
+
value,
|
|
3009
|
+
tempPathKey,
|
|
3010
|
+
element,
|
|
3011
|
+
ctx,
|
|
3012
|
+
state.config.readonly
|
|
3013
|
+
);
|
|
3014
|
+
sliderUI.style.flex = "1";
|
|
3015
|
+
itemWrapper.appendChild(sliderUI);
|
|
3016
|
+
if (index === -1) {
|
|
3017
|
+
container.appendChild(itemWrapper);
|
|
3018
|
+
} else {
|
|
3019
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
3020
|
+
}
|
|
3021
|
+
updateIndices();
|
|
3022
|
+
return itemWrapper;
|
|
3023
|
+
}
|
|
3024
|
+
function updateRemoveButtons() {
|
|
3025
|
+
if (state.config.readonly) return;
|
|
3026
|
+
const items = container.querySelectorAll(".multiple-slider-item");
|
|
3027
|
+
const currentCount = items.length;
|
|
3028
|
+
items.forEach((item) => {
|
|
3029
|
+
let removeBtn = item.querySelector(
|
|
3030
|
+
".remove-item-btn"
|
|
3031
|
+
);
|
|
3032
|
+
if (!removeBtn) {
|
|
3033
|
+
removeBtn = document.createElement("button");
|
|
3034
|
+
removeBtn.type = "button";
|
|
3035
|
+
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
3036
|
+
removeBtn.style.cssText = `
|
|
3037
|
+
color: var(--fb-error-color);
|
|
3038
|
+
background-color: transparent;
|
|
3039
|
+
transition: background-color var(--fb-transition-duration);
|
|
3040
|
+
margin-top: 8px;
|
|
3041
|
+
`;
|
|
3042
|
+
removeBtn.innerHTML = "\u2715";
|
|
3043
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
3044
|
+
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
3045
|
+
});
|
|
3046
|
+
removeBtn.addEventListener("mouseleave", () => {
|
|
3047
|
+
removeBtn.style.backgroundColor = "transparent";
|
|
3048
|
+
});
|
|
3049
|
+
removeBtn.onclick = () => {
|
|
3050
|
+
const currentIndex = Array.from(container.children).indexOf(
|
|
3051
|
+
item
|
|
3052
|
+
);
|
|
3053
|
+
if (container.children.length > minCount) {
|
|
3054
|
+
values.splice(currentIndex, 1);
|
|
3055
|
+
item.remove();
|
|
3056
|
+
updateIndices();
|
|
3057
|
+
updateAddButton();
|
|
3058
|
+
updateRemoveButtons();
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
item.appendChild(removeBtn);
|
|
3062
|
+
}
|
|
3063
|
+
const disabled = currentCount <= minCount;
|
|
3064
|
+
removeBtn.disabled = disabled;
|
|
3065
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
3066
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
function updateAddButton() {
|
|
3070
|
+
const existingAddBtn = wrapper.querySelector(".add-slider-btn");
|
|
3071
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
3072
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
3073
|
+
const addBtn = document.createElement("button");
|
|
3074
|
+
addBtn.type = "button";
|
|
3075
|
+
addBtn.className = "add-slider-btn mt-2 px-3 py-1 rounded";
|
|
3076
|
+
addBtn.style.cssText = `
|
|
3077
|
+
color: var(--fb-primary-color);
|
|
3078
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
3079
|
+
background-color: transparent;
|
|
3080
|
+
font-size: var(--fb-font-size);
|
|
3081
|
+
transition: all var(--fb-transition-duration);
|
|
3082
|
+
`;
|
|
3083
|
+
addBtn.textContent = `+ Add ${element.label || "Slider"}`;
|
|
3084
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
3085
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
3086
|
+
});
|
|
3087
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
3088
|
+
addBtn.style.backgroundColor = "transparent";
|
|
3089
|
+
});
|
|
3090
|
+
addBtn.onclick = () => {
|
|
3091
|
+
values.push(defaultValue);
|
|
3092
|
+
addSliderItem(defaultValue);
|
|
3093
|
+
updateAddButton();
|
|
3094
|
+
updateRemoveButtons();
|
|
3095
|
+
};
|
|
3096
|
+
wrapper.appendChild(addBtn);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
values.forEach((value) => addSliderItem(value));
|
|
3100
|
+
updateAddButton();
|
|
3101
|
+
updateRemoveButtons();
|
|
3102
|
+
const hint = document.createElement("p");
|
|
3103
|
+
hint.className = "mt-1";
|
|
3104
|
+
hint.style.cssText = `
|
|
3105
|
+
font-size: var(--fb-font-size-small);
|
|
3106
|
+
color: var(--fb-text-secondary-color);
|
|
3107
|
+
`;
|
|
3108
|
+
hint.textContent = makeFieldHint(element);
|
|
3109
|
+
wrapper.appendChild(hint);
|
|
3110
|
+
}
|
|
3111
|
+
function validateSliderElement(element, key, context) {
|
|
3112
|
+
const errors = [];
|
|
3113
|
+
const { scopeRoot, skipValidation } = context;
|
|
3114
|
+
if (element.min === void 0 || element.min === null) {
|
|
3115
|
+
throw new Error(
|
|
3116
|
+
`Slider validation: field "${key}" requires "min" property`
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
if (element.max === void 0 || element.max === null) {
|
|
3120
|
+
throw new Error(
|
|
3121
|
+
`Slider validation: field "${key}" requires "max" property`
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
const min = element.min;
|
|
3125
|
+
const max = element.max;
|
|
3126
|
+
const step = element.step ?? 1;
|
|
3127
|
+
const scale = element.scale || "linear";
|
|
3128
|
+
const markValidity = (input, errorMessage) => {
|
|
3129
|
+
if (!input) return;
|
|
3130
|
+
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
3131
|
+
let errorElement = document.getElementById(errorId);
|
|
3132
|
+
if (errorMessage) {
|
|
3133
|
+
input.classList.add("invalid");
|
|
3134
|
+
input.title = errorMessage;
|
|
3135
|
+
if (!errorElement) {
|
|
3136
|
+
errorElement = document.createElement("div");
|
|
3137
|
+
errorElement.id = errorId;
|
|
3138
|
+
errorElement.className = "error-message";
|
|
3139
|
+
errorElement.style.cssText = `
|
|
3140
|
+
color: var(--fb-error-color);
|
|
3141
|
+
font-size: var(--fb-font-size-small);
|
|
3142
|
+
margin-top: 0.25rem;
|
|
3143
|
+
`;
|
|
3144
|
+
const sliderContainer = input.closest(".slider-container");
|
|
3145
|
+
if (sliderContainer && sliderContainer.nextSibling) {
|
|
3146
|
+
sliderContainer.parentNode?.insertBefore(errorElement, sliderContainer.nextSibling);
|
|
3147
|
+
} else if (sliderContainer) {
|
|
3148
|
+
sliderContainer.parentNode?.appendChild(errorElement);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
errorElement.textContent = errorMessage;
|
|
3152
|
+
errorElement.style.display = "block";
|
|
3153
|
+
} else {
|
|
3154
|
+
input.classList.remove("invalid");
|
|
3155
|
+
input.title = "";
|
|
3156
|
+
if (errorElement) {
|
|
3157
|
+
errorElement.remove();
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
const validateSliderValue = (slider, fieldKey) => {
|
|
3162
|
+
const rawValue = slider.value;
|
|
3163
|
+
if (!rawValue) {
|
|
3164
|
+
if (!skipValidation && element.required) {
|
|
3165
|
+
errors.push(`${fieldKey}: required`);
|
|
3166
|
+
markValidity(slider, "required");
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
markValidity(slider, null);
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
let value;
|
|
3173
|
+
if (scale === "exponential") {
|
|
3174
|
+
const position = parseFloat(rawValue) / 1e3;
|
|
3175
|
+
value = positionToExponential(position, min, max);
|
|
3176
|
+
value = alignToStep(value, step);
|
|
3177
|
+
} else {
|
|
3178
|
+
value = parseFloat(rawValue);
|
|
3179
|
+
value = alignToStep(value, step);
|
|
3180
|
+
}
|
|
3181
|
+
if (!skipValidation) {
|
|
3182
|
+
if (value < min) {
|
|
3183
|
+
errors.push(`${fieldKey}: value ${value} < min ${min}`);
|
|
3184
|
+
markValidity(slider, `value must be >= ${min}`);
|
|
3185
|
+
return value;
|
|
3186
|
+
}
|
|
3187
|
+
if (value > max) {
|
|
3188
|
+
errors.push(`${fieldKey}: value ${value} > max ${max}`);
|
|
3189
|
+
markValidity(slider, `value must be <= ${max}`);
|
|
3190
|
+
return value;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
markValidity(slider, null);
|
|
3194
|
+
return value;
|
|
3195
|
+
};
|
|
3196
|
+
if (element.multiple) {
|
|
3197
|
+
const sliders = scopeRoot.querySelectorAll(
|
|
3198
|
+
`input[type="range"][name^="${key}["]`
|
|
3199
|
+
);
|
|
3200
|
+
const values = [];
|
|
3201
|
+
sliders.forEach((slider, index) => {
|
|
3202
|
+
const value = validateSliderValue(slider, `${key}[${index}]`);
|
|
3203
|
+
values.push(value);
|
|
3204
|
+
});
|
|
3205
|
+
if (!skipValidation) {
|
|
3206
|
+
const minCount = element.minCount ?? 1;
|
|
3207
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
3208
|
+
const filteredValues = values.filter((v) => v !== null);
|
|
3209
|
+
if (element.required && filteredValues.length === 0) {
|
|
3210
|
+
errors.push(`${key}: required`);
|
|
3211
|
+
}
|
|
3212
|
+
if (filteredValues.length < minCount) {
|
|
3213
|
+
errors.push(`${key}: minimum ${minCount} items required`);
|
|
3214
|
+
}
|
|
3215
|
+
if (filteredValues.length > maxCount) {
|
|
3216
|
+
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
return { value: values, errors };
|
|
3220
|
+
} else {
|
|
3221
|
+
const slider = scopeRoot.querySelector(
|
|
3222
|
+
`input[type="range"][name="${key}"]`
|
|
3223
|
+
);
|
|
3224
|
+
if (!slider) {
|
|
3225
|
+
if (!skipValidation && element.required) {
|
|
3226
|
+
errors.push(`${key}: required`);
|
|
3227
|
+
}
|
|
3228
|
+
return { value: null, errors };
|
|
3229
|
+
}
|
|
3230
|
+
const value = validateSliderValue(slider, key);
|
|
3231
|
+
return { value, errors };
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
function updateSliderField(element, fieldPath, value, context) {
|
|
3235
|
+
const { scopeRoot } = context;
|
|
3236
|
+
const min = element.min;
|
|
3237
|
+
const max = element.max;
|
|
3238
|
+
const step = element.step ?? 1;
|
|
3239
|
+
const scale = element.scale || "linear";
|
|
3240
|
+
if (element.multiple) {
|
|
3241
|
+
if (!Array.isArray(value)) {
|
|
3242
|
+
console.warn(
|
|
3243
|
+
`updateSliderField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
|
|
3244
|
+
);
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
const sliders = scopeRoot.querySelectorAll(
|
|
3248
|
+
`input[type="range"][name^="${fieldPath}["]`
|
|
3249
|
+
);
|
|
3250
|
+
sliders.forEach((slider, index) => {
|
|
3251
|
+
if (index < value.length && value[index] !== null) {
|
|
3252
|
+
const numValue = Number(value[index]);
|
|
3253
|
+
if (scale === "exponential") {
|
|
3254
|
+
const position = exponentialToPosition(numValue, min, max);
|
|
3255
|
+
slider.value = (position * 1e3).toString();
|
|
3256
|
+
} else {
|
|
3257
|
+
slider.value = numValue.toString();
|
|
3258
|
+
}
|
|
3259
|
+
const sliderContainer = slider.closest(".slider-container");
|
|
3260
|
+
if (sliderContainer) {
|
|
3261
|
+
const valueDisplay = sliderContainer.querySelector(".slider-value");
|
|
3262
|
+
if (valueDisplay) {
|
|
3263
|
+
valueDisplay.textContent = numValue.toFixed(step < 1 ? 2 : 0);
|
|
3264
|
+
}
|
|
3265
|
+
const percentage = (numValue - min) / (max - min) * 100;
|
|
3266
|
+
slider.style.background = `linear-gradient(
|
|
3267
|
+
to right,
|
|
3268
|
+
var(--fb-primary-color) 0%,
|
|
3269
|
+
var(--fb-primary-color) ${percentage}%,
|
|
3270
|
+
var(--fb-border-color) ${percentage}%,
|
|
3271
|
+
var(--fb-border-color) 100%
|
|
3272
|
+
)`;
|
|
3273
|
+
}
|
|
3274
|
+
slider.classList.remove("invalid");
|
|
3275
|
+
slider.title = "";
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
if (value.length !== sliders.length) {
|
|
3279
|
+
console.warn(
|
|
3280
|
+
`updateSliderField: Multiple field "${fieldPath}" has ${sliders.length} sliders but received ${value.length} values. Consider re-rendering for add/remove.`
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
} else {
|
|
3284
|
+
const slider = scopeRoot.querySelector(
|
|
3285
|
+
`input[type="range"][name="${fieldPath}"]`
|
|
3286
|
+
);
|
|
3287
|
+
if (slider && value !== null && value !== void 0) {
|
|
3288
|
+
const numValue = Number(value);
|
|
3289
|
+
if (scale === "exponential") {
|
|
3290
|
+
const position = exponentialToPosition(numValue, min, max);
|
|
3291
|
+
slider.value = (position * 1e3).toString();
|
|
3292
|
+
} else {
|
|
3293
|
+
slider.value = numValue.toString();
|
|
3294
|
+
}
|
|
3295
|
+
const sliderContainer = slider.closest(".slider-container");
|
|
3296
|
+
if (sliderContainer) {
|
|
3297
|
+
const valueDisplay = sliderContainer.querySelector(".slider-value");
|
|
3298
|
+
if (valueDisplay) {
|
|
3299
|
+
valueDisplay.textContent = numValue.toFixed(step < 1 ? 2 : 0);
|
|
3300
|
+
}
|
|
3301
|
+
const percentage = (numValue - min) / (max - min) * 100;
|
|
3302
|
+
slider.style.background = `linear-gradient(
|
|
3303
|
+
to right,
|
|
3304
|
+
var(--fb-primary-color) 0%,
|
|
3305
|
+
var(--fb-primary-color) ${percentage}%,
|
|
3306
|
+
var(--fb-border-color) ${percentage}%,
|
|
3307
|
+
var(--fb-border-color) 100%
|
|
3308
|
+
)`;
|
|
3309
|
+
}
|
|
3310
|
+
slider.classList.remove("invalid");
|
|
3311
|
+
slider.title = "";
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// src/components/container.ts
|
|
3317
|
+
var renderElementFunc = null;
|
|
3318
|
+
function setRenderElement(fn) {
|
|
3319
|
+
renderElementFunc = fn;
|
|
3320
|
+
}
|
|
3321
|
+
function renderElement(element, ctx) {
|
|
3322
|
+
if (!renderElementFunc) {
|
|
3323
|
+
throw new Error(
|
|
3324
|
+
"renderElement not initialized. Import from components/index.ts"
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
return renderElementFunc(element, ctx);
|
|
3328
|
+
}
|
|
3329
|
+
function createPrefillHints(element, pathKey) {
|
|
3330
|
+
if (!element.prefillHints || element.prefillHints.length === 0) {
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
const hintsContainer = document.createElement("div");
|
|
3334
|
+
hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
|
|
3335
|
+
element.prefillHints.forEach((hint, index) => {
|
|
3336
|
+
const hintButton = document.createElement("button");
|
|
3337
|
+
hintButton.type = "button";
|
|
3338
|
+
hintButton.className = "fb-prefill-hint";
|
|
3339
|
+
hintButton.textContent = hint.label;
|
|
3340
|
+
hintButton.setAttribute("data-hint-values", JSON.stringify(hint.values));
|
|
3341
|
+
hintButton.setAttribute("data-container-key", pathKey);
|
|
3342
|
+
hintButton.setAttribute("data-hint-index", String(index));
|
|
3343
|
+
hintsContainer.appendChild(hintButton);
|
|
3344
|
+
});
|
|
3345
|
+
return hintsContainer;
|
|
3346
|
+
}
|
|
3347
|
+
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
3348
|
+
const containerWrap = document.createElement("div");
|
|
3349
|
+
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
3350
|
+
containerWrap.setAttribute("data-container", pathKey);
|
|
3351
|
+
const header = document.createElement("div");
|
|
3352
|
+
header.className = "flex justify-between items-center mb-4";
|
|
3353
|
+
const left = document.createElement("div");
|
|
3354
|
+
left.className = "flex-1";
|
|
3355
|
+
const itemsWrap = document.createElement("div");
|
|
3356
|
+
const columns = element.columns || 1;
|
|
3357
|
+
if (columns === 1) {
|
|
3358
|
+
itemsWrap.className = "space-y-4";
|
|
3359
|
+
} else {
|
|
3360
|
+
itemsWrap.className = `grid grid-cols-${columns} gap-4`;
|
|
3361
|
+
}
|
|
3362
|
+
containerWrap.appendChild(header);
|
|
3363
|
+
header.appendChild(left);
|
|
3364
|
+
if (!ctx.state.config.readonly) {
|
|
3365
|
+
const hintsElement = createPrefillHints(element, pathKey);
|
|
3366
|
+
if (hintsElement) {
|
|
3367
|
+
containerWrap.appendChild(hintsElement);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
const subCtx = {
|
|
3371
|
+
path: pathJoin(ctx.path, element.key),
|
|
3372
|
+
prefill: ctx.prefill?.[element.key] || {},
|
|
3373
|
+
// Sliced data for value population
|
|
3374
|
+
formData: ctx.formData ?? ctx.prefill,
|
|
3375
|
+
// Complete root data for displayIf evaluation
|
|
3376
|
+
state: ctx.state
|
|
3377
|
+
};
|
|
3378
|
+
element.elements.forEach((child) => {
|
|
3379
|
+
if (!child.hidden) {
|
|
3380
|
+
itemsWrap.appendChild(renderElement(child, subCtx));
|
|
3381
|
+
}
|
|
3382
|
+
});
|
|
3383
|
+
containerWrap.appendChild(itemsWrap);
|
|
3384
|
+
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
3385
|
+
wrapper.appendChild(containerWrap);
|
|
3386
|
+
}
|
|
3387
|
+
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
3388
|
+
const state = ctx.state;
|
|
3389
|
+
const containerWrap = document.createElement("div");
|
|
3390
|
+
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
3391
|
+
const header = document.createElement("div");
|
|
3392
|
+
header.className = "flex justify-between items-center mb-4";
|
|
3393
|
+
const left = document.createElement("div");
|
|
3394
|
+
left.className = "flex-1";
|
|
3395
|
+
const right = document.createElement("div");
|
|
3396
|
+
right.className = "flex gap-2";
|
|
3397
|
+
const itemsWrap = document.createElement("div");
|
|
3398
|
+
itemsWrap.className = "space-y-4";
|
|
3399
|
+
containerWrap.appendChild(header);
|
|
3400
|
+
header.appendChild(left);
|
|
3401
|
+
if (!state.config.readonly) {
|
|
3402
|
+
header.appendChild(right);
|
|
3403
|
+
}
|
|
3404
|
+
if (!ctx.state.config.readonly) {
|
|
3405
|
+
const hintsElement = createPrefillHints(element, element.key);
|
|
3406
|
+
if (hintsElement) {
|
|
3407
|
+
containerWrap.appendChild(hintsElement);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
const min = element.minCount ?? 0;
|
|
3411
|
+
const max = element.maxCount ?? Infinity;
|
|
3412
|
+
const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
|
|
3413
|
+
const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
|
|
3414
|
+
const createAddButton = () => {
|
|
3415
|
+
const add = document.createElement("button");
|
|
3416
|
+
add.type = "button";
|
|
3417
|
+
add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
|
|
3418
|
+
add.textContent = t("addElement", state);
|
|
3419
|
+
add.onclick = () => {
|
|
3420
|
+
if (countItems() < max) {
|
|
3421
|
+
const idx = countItems();
|
|
3422
|
+
const subCtx = {
|
|
3423
|
+
state: ctx.state,
|
|
3424
|
+
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
3425
|
+
prefill: {},
|
|
3426
|
+
formData: ctx.formData ?? ctx.prefill
|
|
3427
|
+
// Complete root data for displayIf
|
|
3428
|
+
};
|
|
3429
|
+
const item = document.createElement("div");
|
|
3430
|
+
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
3431
|
+
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3432
|
+
const childWrapper = document.createElement("div");
|
|
3433
|
+
const columns = element.columns || 1;
|
|
3434
|
+
if (columns === 1) {
|
|
3435
|
+
childWrapper.className = "space-y-4";
|
|
3436
|
+
} else {
|
|
3437
|
+
childWrapper.className = `grid grid-cols-${columns} gap-4`;
|
|
3438
|
+
}
|
|
3439
|
+
element.elements.forEach((child) => {
|
|
3440
|
+
if (!child.hidden) {
|
|
3441
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
item.appendChild(childWrapper);
|
|
3445
|
+
if (!state.config.readonly) {
|
|
3446
|
+
const rem = document.createElement("button");
|
|
3447
|
+
rem.type = "button";
|
|
3448
|
+
rem.className = "absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
3449
|
+
rem.textContent = "\xD7";
|
|
3450
|
+
rem.onclick = () => {
|
|
3451
|
+
item.remove();
|
|
3452
|
+
updateAddButton();
|
|
3453
|
+
};
|
|
3454
|
+
item.style.position = "relative";
|
|
3455
|
+
item.appendChild(rem);
|
|
3456
|
+
}
|
|
3457
|
+
itemsWrap.appendChild(item);
|
|
3458
|
+
updateAddButton();
|
|
3459
|
+
}
|
|
3460
|
+
};
|
|
3461
|
+
return add;
|
|
3462
|
+
};
|
|
3463
|
+
const updateAddButton = () => {
|
|
3464
|
+
const currentCount = countItems();
|
|
3465
|
+
const addBtn = right.querySelector("button");
|
|
3466
|
+
if (addBtn) {
|
|
2379
3467
|
addBtn.disabled = currentCount >= max;
|
|
2380
3468
|
addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
2381
3469
|
}
|
|
@@ -2396,11 +3484,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2396
3484
|
const item = document.createElement("div");
|
|
2397
3485
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2398
3486
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3487
|
+
const childWrapper = document.createElement("div");
|
|
3488
|
+
const columns = element.columns || 1;
|
|
3489
|
+
if (columns === 1) {
|
|
3490
|
+
childWrapper.className = "space-y-4";
|
|
3491
|
+
} else {
|
|
3492
|
+
childWrapper.className = `grid grid-cols-${columns} gap-4`;
|
|
3493
|
+
}
|
|
2399
3494
|
element.elements.forEach((child) => {
|
|
2400
3495
|
if (!child.hidden) {
|
|
2401
|
-
|
|
3496
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
2402
3497
|
}
|
|
2403
3498
|
});
|
|
3499
|
+
item.appendChild(childWrapper);
|
|
2404
3500
|
if (!state.config.readonly) {
|
|
2405
3501
|
const rem = document.createElement("button");
|
|
2406
3502
|
rem.type = "button";
|
|
@@ -2429,11 +3525,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2429
3525
|
const item = document.createElement("div");
|
|
2430
3526
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2431
3527
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3528
|
+
const childWrapper = document.createElement("div");
|
|
3529
|
+
const columns = element.columns || 1;
|
|
3530
|
+
if (columns === 1) {
|
|
3531
|
+
childWrapper.className = "space-y-4";
|
|
3532
|
+
} else {
|
|
3533
|
+
childWrapper.className = `grid grid-cols-${columns} gap-4`;
|
|
3534
|
+
}
|
|
2432
3535
|
element.elements.forEach((child) => {
|
|
2433
3536
|
if (!child.hidden) {
|
|
2434
|
-
|
|
3537
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
2435
3538
|
}
|
|
2436
3539
|
});
|
|
3540
|
+
item.appendChild(childWrapper);
|
|
2437
3541
|
const rem = document.createElement("button");
|
|
2438
3542
|
rem.type = "button";
|
|
2439
3543
|
rem.className = "absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
@@ -2704,34 +3808,33 @@ if (typeof document !== "undefined") {
|
|
|
2704
3808
|
}
|
|
2705
3809
|
});
|
|
2706
3810
|
}
|
|
2707
|
-
function
|
|
2708
|
-
if (element.displayIf) {
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
console.error(
|
|
2725
|
-
`Error evaluating displayIf for field "${element.key}":`,
|
|
2726
|
-
error
|
|
2727
|
-
);
|
|
3811
|
+
function checkDisplayCondition(element, ctx) {
|
|
3812
|
+
if (!element.displayIf) {
|
|
3813
|
+
return null;
|
|
3814
|
+
}
|
|
3815
|
+
try {
|
|
3816
|
+
const dataForCondition = ctx.formData ?? ctx.prefill;
|
|
3817
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
3818
|
+
element.displayIf,
|
|
3819
|
+
dataForCondition
|
|
3820
|
+
);
|
|
3821
|
+
if (!shouldDisplay) {
|
|
3822
|
+
const hiddenWrapper = document.createElement("div");
|
|
3823
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
3824
|
+
hiddenWrapper.style.display = "none";
|
|
3825
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
3826
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
3827
|
+
return hiddenWrapper;
|
|
2728
3828
|
}
|
|
3829
|
+
} catch (error) {
|
|
3830
|
+
console.error(
|
|
3831
|
+
`Error evaluating displayIf for field "${element.key}":`,
|
|
3832
|
+
error
|
|
3833
|
+
);
|
|
2729
3834
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const label = document.createElement("div");
|
|
2734
|
-
label.className = "flex items-center mb-2";
|
|
3835
|
+
return null;
|
|
3836
|
+
}
|
|
3837
|
+
function createFieldLabel(element) {
|
|
2735
3838
|
const title = document.createElement("label");
|
|
2736
3839
|
title.className = "text-sm font-medium text-gray-900";
|
|
2737
3840
|
title.textContent = element.label || element.key;
|
|
@@ -2741,59 +3844,71 @@ function renderElement2(element, ctx) {
|
|
|
2741
3844
|
req.textContent = "*";
|
|
2742
3845
|
title.appendChild(req);
|
|
2743
3846
|
}
|
|
3847
|
+
return title;
|
|
3848
|
+
}
|
|
3849
|
+
function createInfoButton(element) {
|
|
3850
|
+
const infoBtn = document.createElement("button");
|
|
3851
|
+
infoBtn.type = "button";
|
|
3852
|
+
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
3853
|
+
infoBtn.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
|
|
3854
|
+
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3855
|
+
const tooltip = document.createElement("div");
|
|
3856
|
+
tooltip.id = tooltipId;
|
|
3857
|
+
tooltip.className = "hidden absolute z-50 bg-gray-200 text-gray-900 text-sm rounded-lg p-3 max-w-sm border border-gray-300 shadow-lg";
|
|
3858
|
+
tooltip.style.position = "fixed";
|
|
3859
|
+
tooltip.textContent = element.description || element.hint || "Field information";
|
|
3860
|
+
document.body.appendChild(tooltip);
|
|
3861
|
+
infoBtn.onclick = (e) => {
|
|
3862
|
+
e.preventDefault();
|
|
3863
|
+
e.stopPropagation();
|
|
3864
|
+
showTooltip(tooltipId, infoBtn);
|
|
3865
|
+
};
|
|
3866
|
+
return infoBtn;
|
|
3867
|
+
}
|
|
3868
|
+
function createLabelContainer(element) {
|
|
3869
|
+
const label = document.createElement("div");
|
|
3870
|
+
label.className = "flex items-center mb-2";
|
|
3871
|
+
const title = createFieldLabel(element);
|
|
2744
3872
|
label.appendChild(title);
|
|
2745
3873
|
if (element.description || element.hint) {
|
|
2746
|
-
const infoBtn =
|
|
2747
|
-
infoBtn.type = "button";
|
|
2748
|
-
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
2749
|
-
infoBtn.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
|
|
2750
|
-
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2751
|
-
const tooltip = document.createElement("div");
|
|
2752
|
-
tooltip.id = tooltipId;
|
|
2753
|
-
tooltip.className = "hidden absolute z-50 bg-gray-200 text-gray-900 text-sm rounded-lg p-3 max-w-sm border border-gray-300 shadow-lg";
|
|
2754
|
-
tooltip.style.position = "fixed";
|
|
2755
|
-
tooltip.textContent = element.description || element.hint || "Field information";
|
|
2756
|
-
document.body.appendChild(tooltip);
|
|
2757
|
-
infoBtn.onclick = (e) => {
|
|
2758
|
-
e.preventDefault();
|
|
2759
|
-
e.stopPropagation();
|
|
2760
|
-
showTooltip(tooltipId, infoBtn);
|
|
2761
|
-
};
|
|
3874
|
+
const infoBtn = createInfoButton(element);
|
|
2762
3875
|
label.appendChild(infoBtn);
|
|
2763
3876
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
3877
|
+
return label;
|
|
3878
|
+
}
|
|
3879
|
+
function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
3880
|
+
const isMultiple = "multiple" in element && element.multiple;
|
|
2766
3881
|
switch (element.type) {
|
|
2767
3882
|
case "text":
|
|
2768
|
-
if (
|
|
3883
|
+
if (isMultiple) {
|
|
2769
3884
|
renderMultipleTextElement(element, ctx, wrapper, pathKey);
|
|
2770
3885
|
} else {
|
|
2771
3886
|
renderTextElement(element, ctx, wrapper, pathKey);
|
|
2772
3887
|
}
|
|
2773
3888
|
break;
|
|
2774
3889
|
case "textarea":
|
|
2775
|
-
if (
|
|
3890
|
+
if (isMultiple) {
|
|
2776
3891
|
renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
|
|
2777
3892
|
} else {
|
|
2778
3893
|
renderTextareaElement(element, ctx, wrapper, pathKey);
|
|
2779
3894
|
}
|
|
2780
3895
|
break;
|
|
2781
3896
|
case "number":
|
|
2782
|
-
if (
|
|
3897
|
+
if (isMultiple) {
|
|
2783
3898
|
renderMultipleNumberElement(element, ctx, wrapper, pathKey);
|
|
2784
3899
|
} else {
|
|
2785
3900
|
renderNumberElement(element, ctx, wrapper, pathKey);
|
|
2786
3901
|
}
|
|
2787
3902
|
break;
|
|
2788
3903
|
case "select":
|
|
2789
|
-
if (
|
|
3904
|
+
if (isMultiple) {
|
|
2790
3905
|
renderMultipleSelectElement(element, ctx, wrapper, pathKey);
|
|
2791
3906
|
} else {
|
|
2792
3907
|
renderSelectElement(element, ctx, wrapper, pathKey);
|
|
2793
3908
|
}
|
|
2794
3909
|
break;
|
|
2795
3910
|
case "file":
|
|
2796
|
-
if (
|
|
3911
|
+
if (isMultiple) {
|
|
2797
3912
|
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
2798
3913
|
} else {
|
|
2799
3914
|
renderFileElement(element, ctx, wrapper, pathKey);
|
|
@@ -2802,11 +3917,25 @@ function renderElement2(element, ctx) {
|
|
|
2802
3917
|
case "files":
|
|
2803
3918
|
renderFilesElement(element, ctx, wrapper, pathKey);
|
|
2804
3919
|
break;
|
|
3920
|
+
case "colour":
|
|
3921
|
+
if (isMultiple) {
|
|
3922
|
+
renderMultipleColourElement(element, ctx, wrapper, pathKey);
|
|
3923
|
+
} else {
|
|
3924
|
+
renderColourElement(element, ctx, wrapper, pathKey);
|
|
3925
|
+
}
|
|
3926
|
+
break;
|
|
3927
|
+
case "slider":
|
|
3928
|
+
if (isMultiple) {
|
|
3929
|
+
renderMultipleSliderElement(element, ctx, wrapper, pathKey);
|
|
3930
|
+
} else {
|
|
3931
|
+
renderSliderElement(element, ctx, wrapper, pathKey);
|
|
3932
|
+
}
|
|
3933
|
+
break;
|
|
2805
3934
|
case "group":
|
|
2806
3935
|
renderGroupElement(element, ctx, wrapper, pathKey);
|
|
2807
3936
|
break;
|
|
2808
3937
|
case "container":
|
|
2809
|
-
if (
|
|
3938
|
+
if (isMultiple) {
|
|
2810
3939
|
renderMultipleContainerElement(element, ctx, wrapper);
|
|
2811
3940
|
} else {
|
|
2812
3941
|
renderSingleContainerElement(element, ctx, wrapper, pathKey);
|
|
@@ -2819,6 +3948,19 @@ function renderElement2(element, ctx) {
|
|
|
2819
3948
|
wrapper.appendChild(unsupported);
|
|
2820
3949
|
}
|
|
2821
3950
|
}
|
|
3951
|
+
}
|
|
3952
|
+
function renderElement2(element, ctx) {
|
|
3953
|
+
const hiddenElement = checkDisplayCondition(element, ctx);
|
|
3954
|
+
if (hiddenElement) {
|
|
3955
|
+
return hiddenElement;
|
|
3956
|
+
}
|
|
3957
|
+
const wrapper = document.createElement("div");
|
|
3958
|
+
wrapper.className = "mb-6 fb-field-wrapper";
|
|
3959
|
+
wrapper.setAttribute("data-field-key", element.key);
|
|
3960
|
+
const label = createLabelContainer(element);
|
|
3961
|
+
wrapper.appendChild(label);
|
|
3962
|
+
const pathKey = pathJoin(ctx.path, element.key);
|
|
3963
|
+
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
2822
3964
|
return wrapper;
|
|
2823
3965
|
}
|
|
2824
3966
|
setRenderElement(renderElement2);
|
|
@@ -3102,6 +4244,14 @@ var componentRegistry = {
|
|
|
3102
4244
|
validate: validateFileElement,
|
|
3103
4245
|
update: updateFileField
|
|
3104
4246
|
},
|
|
4247
|
+
colour: {
|
|
4248
|
+
validate: validateColourElement,
|
|
4249
|
+
update: updateColourField
|
|
4250
|
+
},
|
|
4251
|
+
slider: {
|
|
4252
|
+
validate: validateSliderElement,
|
|
4253
|
+
update: updateSliderField
|
|
4254
|
+
},
|
|
3105
4255
|
container: {
|
|
3106
4256
|
validate: validateContainerElement,
|
|
3107
4257
|
update: updateContainerField
|
|
@@ -3458,6 +4608,33 @@ var FormBuilderInstance = class {
|
|
|
3458
4608
|
this.renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
|
|
3459
4609
|
}
|
|
3460
4610
|
}
|
|
4611
|
+
/**
|
|
4612
|
+
* Handle prefill hint click - updates container fields with hint values
|
|
4613
|
+
*/
|
|
4614
|
+
handlePrefillHintClick(event) {
|
|
4615
|
+
const target = event.target;
|
|
4616
|
+
if (!target.classList.contains("fb-prefill-hint")) {
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
event.preventDefault();
|
|
4620
|
+
event.stopPropagation();
|
|
4621
|
+
const hintValuesJson = target.getAttribute("data-hint-values");
|
|
4622
|
+
const containerKey = target.getAttribute("data-container-key");
|
|
4623
|
+
if (!hintValuesJson || !containerKey) {
|
|
4624
|
+
console.warn("Prefill hint missing required data attributes");
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
const hintValues = JSON.parse(hintValuesJson);
|
|
4629
|
+
for (const fieldKey in hintValues) {
|
|
4630
|
+
const fullPath = `${containerKey}.${fieldKey}`;
|
|
4631
|
+
const value = hintValues[fieldKey];
|
|
4632
|
+
this.updateField(fullPath, value);
|
|
4633
|
+
}
|
|
4634
|
+
} catch (error) {
|
|
4635
|
+
console.error("Error parsing prefill hint values:", error);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
3461
4638
|
/**
|
|
3462
4639
|
* Render form from schema
|
|
3463
4640
|
*/
|
|
@@ -3490,6 +4667,9 @@ var FormBuilderInstance = class {
|
|
|
3490
4667
|
formEl.appendChild(block);
|
|
3491
4668
|
});
|
|
3492
4669
|
root.appendChild(formEl);
|
|
4670
|
+
if (!this.state.config.readonly) {
|
|
4671
|
+
root.addEventListener("click", this.handlePrefillHintClick.bind(this));
|
|
4672
|
+
}
|
|
3493
4673
|
if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
|
|
3494
4674
|
this.renderExternalActions();
|
|
3495
4675
|
}
|