@dmitryvim/form-builder 0.2.5 → 0.2.7
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 +121 -2
- package/dist/browser/formbuilder.min.js +117 -60
- package/dist/browser/formbuilder.v0.2.7.min.js +241 -0
- package/dist/cjs/index.cjs +896 -172
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +883 -167
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +117 -60
- package/dist/types/components/colour.d.ts +18 -0
- package/dist/types/components/index.d.ts +2 -1
- package/dist/types/instance/FormBuilderInstance.d.ts +5 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +19 -2
- package/dist/types/utils/display-conditions.d.ts +17 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.5.min.js +0 -184
package/dist/esm/index.js
CHANGED
|
@@ -56,9 +56,6 @@ function validateSchema(schema) {
|
|
|
56
56
|
errors.push("Schema must be an object");
|
|
57
57
|
return errors;
|
|
58
58
|
}
|
|
59
|
-
if (!schema.version) {
|
|
60
|
-
errors.push("Schema missing version");
|
|
61
|
-
}
|
|
62
59
|
if (!Array.isArray(schema.elements)) {
|
|
63
60
|
errors.push("Schema missing elements array");
|
|
64
61
|
return errors;
|
|
@@ -72,6 +69,20 @@ function validateSchema(schema) {
|
|
|
72
69
|
if (!element.key) {
|
|
73
70
|
errors.push(`${elementPath}: missing key`);
|
|
74
71
|
}
|
|
72
|
+
if (element.displayIf) {
|
|
73
|
+
const displayIf = element.displayIf;
|
|
74
|
+
if (!displayIf.key || typeof displayIf.key !== "string") {
|
|
75
|
+
errors.push(
|
|
76
|
+
`${elementPath}: displayIf must have a 'key' property of type string`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const hasOperator = "equals" in displayIf;
|
|
80
|
+
if (!hasOperator) {
|
|
81
|
+
errors.push(
|
|
82
|
+
`${elementPath}: displayIf must have at least one operator (equals, etc.)`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
75
86
|
if (element.type === "group" && "elements" in element && element.elements) {
|
|
76
87
|
validateElements(element.elements, `${elementPath}.elements`);
|
|
77
88
|
}
|
|
@@ -109,6 +120,66 @@ function clear(node) {
|
|
|
109
120
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
// src/utils/display-conditions.ts
|
|
124
|
+
function getValueByPath(data, path) {
|
|
125
|
+
if (!data || typeof data !== "object") {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
129
|
+
if (!segments || segments.length === 0) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
let current = data;
|
|
133
|
+
for (const segment of segments) {
|
|
134
|
+
if (current === void 0 || current === null) {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
138
|
+
const index = parseInt(segment.slice(1, -1), 10);
|
|
139
|
+
if (!Array.isArray(current) || isNaN(index)) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
current = current[index];
|
|
143
|
+
} else {
|
|
144
|
+
current = current[segment];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return current;
|
|
148
|
+
}
|
|
149
|
+
function evaluateDisplayCondition(condition, formData) {
|
|
150
|
+
if (!condition || !condition.key) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
"Invalid displayIf condition: must have a 'key' property"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const actualValue = getValueByPath(formData, condition.key);
|
|
156
|
+
if ("equals" in condition) {
|
|
157
|
+
return deepEqual(actualValue, condition.equals);
|
|
158
|
+
}
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Invalid displayIf condition: no recognized operator (equals, etc.)`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function deepEqual(a, b) {
|
|
164
|
+
if (a === b) return true;
|
|
165
|
+
if (a == null || b == null) return a === b;
|
|
166
|
+
if (typeof a !== typeof b) return false;
|
|
167
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
172
|
+
console.warn(
|
|
173
|
+
"deepEqual: Circular reference detected in displayIf comparison, using reference equality"
|
|
174
|
+
);
|
|
175
|
+
return a === b;
|
|
176
|
+
}
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return a === b;
|
|
181
|
+
}
|
|
182
|
+
|
|
112
183
|
// src/components/text.ts
|
|
113
184
|
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
114
185
|
const state = ctx.state;
|
|
@@ -1151,6 +1222,172 @@ function t(key, state) {
|
|
|
1151
1222
|
}
|
|
1152
1223
|
|
|
1153
1224
|
// src/components/file.ts
|
|
1225
|
+
function renderLocalImagePreview(container, file, fileName) {
|
|
1226
|
+
const img = document.createElement("img");
|
|
1227
|
+
img.className = "w-full h-full object-contain";
|
|
1228
|
+
img.alt = fileName || "Preview";
|
|
1229
|
+
const reader = new FileReader();
|
|
1230
|
+
reader.onload = (e) => {
|
|
1231
|
+
img.src = e.target?.result || "";
|
|
1232
|
+
};
|
|
1233
|
+
reader.readAsDataURL(file);
|
|
1234
|
+
container.appendChild(img);
|
|
1235
|
+
}
|
|
1236
|
+
function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
|
|
1237
|
+
const videoUrl = URL.createObjectURL(file);
|
|
1238
|
+
container.onclick = null;
|
|
1239
|
+
const newContainer = container.cloneNode(false);
|
|
1240
|
+
if (container.parentNode) {
|
|
1241
|
+
container.parentNode.replaceChild(newContainer, container);
|
|
1242
|
+
}
|
|
1243
|
+
newContainer.innerHTML = `
|
|
1244
|
+
<div class="relative group h-full">
|
|
1245
|
+
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
1246
|
+
<source src="${videoUrl}" type="${videoType}">
|
|
1247
|
+
Your browser does not support the video tag.
|
|
1248
|
+
</video>
|
|
1249
|
+
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
1250
|
+
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
1251
|
+
${t("removeElement", state)}
|
|
1252
|
+
</button>
|
|
1253
|
+
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
1254
|
+
Change
|
|
1255
|
+
</button>
|
|
1256
|
+
</div>
|
|
1257
|
+
</div>
|
|
1258
|
+
`;
|
|
1259
|
+
attachVideoButtonHandlers(newContainer, resourceId, state, deps);
|
|
1260
|
+
return newContainer;
|
|
1261
|
+
}
|
|
1262
|
+
function attachVideoButtonHandlers(container, resourceId, state, deps) {
|
|
1263
|
+
const changeBtn = container.querySelector(".change-file-btn");
|
|
1264
|
+
if (changeBtn) {
|
|
1265
|
+
changeBtn.onclick = (e) => {
|
|
1266
|
+
e.stopPropagation();
|
|
1267
|
+
if (deps?.picker) {
|
|
1268
|
+
deps.picker.click();
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
const deleteBtn = container.querySelector(".delete-file-btn");
|
|
1273
|
+
if (deleteBtn) {
|
|
1274
|
+
deleteBtn.onclick = (e) => {
|
|
1275
|
+
e.stopPropagation();
|
|
1276
|
+
handleVideoDelete(container, resourceId, state, deps);
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function handleVideoDelete(container, resourceId, state, deps) {
|
|
1281
|
+
state.resourceIndex.delete(resourceId);
|
|
1282
|
+
const hiddenInput = container.parentElement?.querySelector(
|
|
1283
|
+
'input[type="hidden"]'
|
|
1284
|
+
);
|
|
1285
|
+
if (hiddenInput) {
|
|
1286
|
+
hiddenInput.value = "";
|
|
1287
|
+
}
|
|
1288
|
+
if (deps?.fileUploadHandler) {
|
|
1289
|
+
container.onclick = deps.fileUploadHandler;
|
|
1290
|
+
}
|
|
1291
|
+
if (deps?.dragHandler) {
|
|
1292
|
+
setupDragAndDrop(container, deps.dragHandler);
|
|
1293
|
+
}
|
|
1294
|
+
container.innerHTML = `
|
|
1295
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1296
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1297
|
+
<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"/>
|
|
1298
|
+
</svg>
|
|
1299
|
+
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1300
|
+
</div>
|
|
1301
|
+
`;
|
|
1302
|
+
}
|
|
1303
|
+
function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
|
|
1304
|
+
const video = document.createElement("video");
|
|
1305
|
+
video.className = "w-full h-full object-contain";
|
|
1306
|
+
video.controls = true;
|
|
1307
|
+
video.preload = "metadata";
|
|
1308
|
+
video.muted = true;
|
|
1309
|
+
const source = document.createElement("source");
|
|
1310
|
+
source.src = thumbnailUrl;
|
|
1311
|
+
source.type = videoType;
|
|
1312
|
+
video.appendChild(source);
|
|
1313
|
+
video.appendChild(document.createTextNode("Your browser does not support the video tag."));
|
|
1314
|
+
container.appendChild(video);
|
|
1315
|
+
}
|
|
1316
|
+
function renderDeleteButton(container, resourceId, state) {
|
|
1317
|
+
addDeleteButton(container, state, () => {
|
|
1318
|
+
state.resourceIndex.delete(resourceId);
|
|
1319
|
+
const hiddenInput = container.parentElement?.querySelector(
|
|
1320
|
+
'input[type="hidden"]'
|
|
1321
|
+
);
|
|
1322
|
+
if (hiddenInput) {
|
|
1323
|
+
hiddenInput.value = "";
|
|
1324
|
+
}
|
|
1325
|
+
container.innerHTML = `
|
|
1326
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1327
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1328
|
+
<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"/>
|
|
1329
|
+
</svg>
|
|
1330
|
+
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1331
|
+
</div>
|
|
1332
|
+
`;
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
|
|
1336
|
+
if (!meta.file || !(meta.file instanceof File)) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (meta.type && meta.type.startsWith("image/")) {
|
|
1340
|
+
renderLocalImagePreview(container, meta.file, fileName);
|
|
1341
|
+
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
1342
|
+
const newContainer = renderLocalVideoPreview(
|
|
1343
|
+
container,
|
|
1344
|
+
meta.file,
|
|
1345
|
+
meta.type,
|
|
1346
|
+
resourceId,
|
|
1347
|
+
state,
|
|
1348
|
+
deps
|
|
1349
|
+
);
|
|
1350
|
+
container = newContainer;
|
|
1351
|
+
} else {
|
|
1352
|
+
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>`;
|
|
1353
|
+
}
|
|
1354
|
+
if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
|
|
1355
|
+
renderDeleteButton(container, resourceId, state);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
|
|
1359
|
+
if (!state.config.getThumbnail) {
|
|
1360
|
+
setEmptyFileContainer(container, state);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
try {
|
|
1364
|
+
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1365
|
+
if (thumbnailUrl) {
|
|
1366
|
+
clear(container);
|
|
1367
|
+
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1368
|
+
renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
|
|
1369
|
+
} else {
|
|
1370
|
+
const img = document.createElement("img");
|
|
1371
|
+
img.className = "w-full h-full object-contain";
|
|
1372
|
+
img.alt = fileName || "Preview";
|
|
1373
|
+
img.src = thumbnailUrl;
|
|
1374
|
+
container.appendChild(img);
|
|
1375
|
+
}
|
|
1376
|
+
} else {
|
|
1377
|
+
setEmptyFileContainer(container, state);
|
|
1378
|
+
}
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
console.error("Failed to get thumbnail:", error);
|
|
1381
|
+
container.innerHTML = `
|
|
1382
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1383
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1384
|
+
<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"/>
|
|
1385
|
+
</svg>
|
|
1386
|
+
<div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
`;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1154
1391
|
async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
1155
1392
|
const { fileName = "", isReadonly = false, deps = null } = options;
|
|
1156
1393
|
if (!isReadonly && deps && (!deps.picker || !deps.fileUploadHandler || !deps.dragHandler)) {
|
|
@@ -1162,141 +1399,19 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
1162
1399
|
if (isReadonly) {
|
|
1163
1400
|
container.classList.add("cursor-pointer");
|
|
1164
1401
|
}
|
|
1165
|
-
const img = document.createElement("img");
|
|
1166
|
-
img.className = "w-full h-full object-contain";
|
|
1167
|
-
img.alt = fileName || "Preview";
|
|
1168
1402
|
const meta = state.resourceIndex.get(resourceId);
|
|
1169
1403
|
if (meta && meta.file && meta.file instanceof File) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
container.onclick = null;
|
|
1180
|
-
const newContainer = container.cloneNode(false);
|
|
1181
|
-
if (container.parentNode) {
|
|
1182
|
-
container.parentNode.replaceChild(newContainer, container);
|
|
1183
|
-
}
|
|
1184
|
-
container = newContainer;
|
|
1185
|
-
container.innerHTML = `
|
|
1186
|
-
<div class="relative group h-full">
|
|
1187
|
-
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
1188
|
-
<source src="${videoUrl}" type="${meta.type}">
|
|
1189
|
-
Your browser does not support the video tag.
|
|
1190
|
-
</video>
|
|
1191
|
-
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
1192
|
-
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
1193
|
-
${t("removeElement", state)}
|
|
1194
|
-
</button>
|
|
1195
|
-
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
1196
|
-
Change
|
|
1197
|
-
</button>
|
|
1198
|
-
</div>
|
|
1199
|
-
</div>
|
|
1200
|
-
`;
|
|
1201
|
-
const changeBtn = container.querySelector(
|
|
1202
|
-
".change-file-btn"
|
|
1203
|
-
);
|
|
1204
|
-
if (changeBtn) {
|
|
1205
|
-
changeBtn.onclick = (e) => {
|
|
1206
|
-
e.stopPropagation();
|
|
1207
|
-
if (deps?.picker) {
|
|
1208
|
-
deps.picker.click();
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
const deleteBtn = container.querySelector(
|
|
1213
|
-
".delete-file-btn"
|
|
1214
|
-
);
|
|
1215
|
-
if (deleteBtn) {
|
|
1216
|
-
deleteBtn.onclick = (e) => {
|
|
1217
|
-
e.stopPropagation();
|
|
1218
|
-
state.resourceIndex.delete(resourceId);
|
|
1219
|
-
const hiddenInput = container.parentElement?.querySelector(
|
|
1220
|
-
'input[type="hidden"]'
|
|
1221
|
-
);
|
|
1222
|
-
if (hiddenInput) {
|
|
1223
|
-
hiddenInput.value = "";
|
|
1224
|
-
}
|
|
1225
|
-
if (deps?.fileUploadHandler) {
|
|
1226
|
-
container.onclick = deps.fileUploadHandler;
|
|
1227
|
-
}
|
|
1228
|
-
if (deps?.dragHandler) {
|
|
1229
|
-
setupDragAndDrop(container, deps.dragHandler);
|
|
1230
|
-
}
|
|
1231
|
-
container.innerHTML = `
|
|
1232
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1233
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1234
|
-
<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"/>
|
|
1235
|
-
</svg>
|
|
1236
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1237
|
-
</div>
|
|
1238
|
-
`;
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
} else {
|
|
1242
|
-
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>`;
|
|
1243
|
-
}
|
|
1244
|
-
if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
|
|
1245
|
-
addDeleteButton(container, state, () => {
|
|
1246
|
-
state.resourceIndex.delete(resourceId);
|
|
1247
|
-
const hiddenInput = container.parentElement?.querySelector(
|
|
1248
|
-
'input[type="hidden"]'
|
|
1249
|
-
);
|
|
1250
|
-
if (hiddenInput) {
|
|
1251
|
-
hiddenInput.value = "";
|
|
1252
|
-
}
|
|
1253
|
-
container.innerHTML = `
|
|
1254
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1255
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1256
|
-
<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"/>
|
|
1257
|
-
</svg>
|
|
1258
|
-
<div class="text-sm text-center">${t("clickDragText", state)}</div>
|
|
1259
|
-
</div>
|
|
1260
|
-
`;
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
} else if (state.config.getThumbnail) {
|
|
1264
|
-
try {
|
|
1265
|
-
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
1266
|
-
if (thumbnailUrl) {
|
|
1267
|
-
clear(container);
|
|
1268
|
-
if (meta && meta.type && meta.type.startsWith("video/")) {
|
|
1269
|
-
const video = document.createElement("video");
|
|
1270
|
-
video.className = "w-full h-full object-contain";
|
|
1271
|
-
video.controls = true;
|
|
1272
|
-
video.preload = "metadata";
|
|
1273
|
-
video.muted = true;
|
|
1274
|
-
const source = document.createElement("source");
|
|
1275
|
-
source.src = thumbnailUrl;
|
|
1276
|
-
source.type = meta.type;
|
|
1277
|
-
video.appendChild(source);
|
|
1278
|
-
video.appendChild(document.createTextNode("Your browser does not support the video tag."));
|
|
1279
|
-
container.appendChild(video);
|
|
1280
|
-
} else {
|
|
1281
|
-
img.src = thumbnailUrl;
|
|
1282
|
-
container.appendChild(img);
|
|
1283
|
-
}
|
|
1284
|
-
} else {
|
|
1285
|
-
setEmptyFileContainer(container, state);
|
|
1286
|
-
}
|
|
1287
|
-
} catch (error) {
|
|
1288
|
-
console.error("Failed to get thumbnail:", error);
|
|
1289
|
-
container.innerHTML = `
|
|
1290
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
1291
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
1292
|
-
<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"/>
|
|
1293
|
-
</svg>
|
|
1294
|
-
<div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
|
|
1295
|
-
</div>
|
|
1296
|
-
`;
|
|
1297
|
-
}
|
|
1404
|
+
await renderLocalFilePreview(
|
|
1405
|
+
container,
|
|
1406
|
+
meta,
|
|
1407
|
+
fileName,
|
|
1408
|
+
resourceId,
|
|
1409
|
+
isReadonly,
|
|
1410
|
+
state,
|
|
1411
|
+
deps
|
|
1412
|
+
);
|
|
1298
1413
|
} else {
|
|
1299
|
-
|
|
1414
|
+
await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
|
|
1300
1415
|
}
|
|
1301
1416
|
}
|
|
1302
1417
|
async function renderFilePreviewReadonly(resourceId, state, fileName) {
|
|
@@ -2197,6 +2312,460 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
2197
2312
|
}
|
|
2198
2313
|
}
|
|
2199
2314
|
|
|
2315
|
+
// src/components/colour.ts
|
|
2316
|
+
function normalizeColourValue(value) {
|
|
2317
|
+
if (!value) return "#000000";
|
|
2318
|
+
return value.toUpperCase();
|
|
2319
|
+
}
|
|
2320
|
+
function isValidHexColour(value) {
|
|
2321
|
+
return /^#[0-9A-F]{6}$/i.test(value) || /^#[0-9A-F]{3}$/i.test(value);
|
|
2322
|
+
}
|
|
2323
|
+
function expandHexColour(value) {
|
|
2324
|
+
if (/^#[0-9A-F]{3}$/i.test(value)) {
|
|
2325
|
+
const r = value[1];
|
|
2326
|
+
const g = value[2];
|
|
2327
|
+
const b = value[3];
|
|
2328
|
+
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
|
2329
|
+
}
|
|
2330
|
+
return value.toUpperCase();
|
|
2331
|
+
}
|
|
2332
|
+
function createReadonlyColourUI(value) {
|
|
2333
|
+
const container = document.createElement("div");
|
|
2334
|
+
container.className = "flex items-center gap-2";
|
|
2335
|
+
const normalizedValue = normalizeColourValue(value);
|
|
2336
|
+
const swatch = document.createElement("div");
|
|
2337
|
+
swatch.style.cssText = `
|
|
2338
|
+
width: 32px;
|
|
2339
|
+
height: 32px;
|
|
2340
|
+
border-radius: var(--fb-border-radius);
|
|
2341
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2342
|
+
background-color: ${normalizedValue};
|
|
2343
|
+
`;
|
|
2344
|
+
const hexText = document.createElement("span");
|
|
2345
|
+
hexText.style.cssText = `
|
|
2346
|
+
font-size: var(--fb-font-size);
|
|
2347
|
+
color: var(--fb-text-color);
|
|
2348
|
+
font-family: var(--fb-font-family-mono, monospace);
|
|
2349
|
+
`;
|
|
2350
|
+
hexText.textContent = normalizedValue;
|
|
2351
|
+
container.appendChild(swatch);
|
|
2352
|
+
container.appendChild(hexText);
|
|
2353
|
+
return container;
|
|
2354
|
+
}
|
|
2355
|
+
function createEditColourUI(value, pathKey, ctx) {
|
|
2356
|
+
const normalizedValue = normalizeColourValue(value);
|
|
2357
|
+
const pickerWrapper = document.createElement("div");
|
|
2358
|
+
pickerWrapper.className = "colour-picker-wrapper";
|
|
2359
|
+
pickerWrapper.style.cssText = `
|
|
2360
|
+
display: flex;
|
|
2361
|
+
align-items: center;
|
|
2362
|
+
gap: 8px;
|
|
2363
|
+
`;
|
|
2364
|
+
const swatch = document.createElement("div");
|
|
2365
|
+
swatch.className = "colour-swatch";
|
|
2366
|
+
swatch.style.cssText = `
|
|
2367
|
+
width: 40px;
|
|
2368
|
+
height: 40px;
|
|
2369
|
+
border-radius: var(--fb-border-radius);
|
|
2370
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2371
|
+
background-color: ${normalizedValue};
|
|
2372
|
+
cursor: pointer;
|
|
2373
|
+
transition: border-color var(--fb-transition-duration) ease-in-out;
|
|
2374
|
+
flex-shrink: 0;
|
|
2375
|
+
`;
|
|
2376
|
+
const hexInput = document.createElement("input");
|
|
2377
|
+
hexInput.type = "text";
|
|
2378
|
+
hexInput.className = "colour-hex-input";
|
|
2379
|
+
hexInput.name = pathKey;
|
|
2380
|
+
hexInput.value = normalizedValue;
|
|
2381
|
+
hexInput.placeholder = "#000000";
|
|
2382
|
+
hexInput.style.cssText = `
|
|
2383
|
+
width: 100px;
|
|
2384
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
2385
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2386
|
+
border-radius: var(--fb-border-radius);
|
|
2387
|
+
background-color: var(--fb-background-color);
|
|
2388
|
+
color: var(--fb-text-color);
|
|
2389
|
+
font-size: var(--fb-font-size);
|
|
2390
|
+
font-family: var(--fb-font-family-mono, monospace);
|
|
2391
|
+
transition: all var(--fb-transition-duration) ease-in-out;
|
|
2392
|
+
`;
|
|
2393
|
+
const colourInput = document.createElement("input");
|
|
2394
|
+
colourInput.type = "color";
|
|
2395
|
+
colourInput.className = "colour-picker-hidden";
|
|
2396
|
+
colourInput.value = normalizedValue.toLowerCase();
|
|
2397
|
+
colourInput.style.cssText = `
|
|
2398
|
+
position: absolute;
|
|
2399
|
+
opacity: 0;
|
|
2400
|
+
pointer-events: none;
|
|
2401
|
+
`;
|
|
2402
|
+
hexInput.addEventListener("input", () => {
|
|
2403
|
+
const inputValue = hexInput.value.trim();
|
|
2404
|
+
if (isValidHexColour(inputValue)) {
|
|
2405
|
+
const expanded = expandHexColour(inputValue);
|
|
2406
|
+
swatch.style.backgroundColor = expanded;
|
|
2407
|
+
colourInput.value = expanded.toLowerCase();
|
|
2408
|
+
hexInput.classList.remove("invalid");
|
|
2409
|
+
if (ctx.instance) {
|
|
2410
|
+
ctx.instance.triggerOnChange(pathKey, expanded);
|
|
2411
|
+
}
|
|
2412
|
+
} else {
|
|
2413
|
+
hexInput.classList.add("invalid");
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
hexInput.addEventListener("blur", () => {
|
|
2417
|
+
const inputValue = hexInput.value.trim();
|
|
2418
|
+
if (isValidHexColour(inputValue)) {
|
|
2419
|
+
const expanded = expandHexColour(inputValue);
|
|
2420
|
+
hexInput.value = expanded;
|
|
2421
|
+
swatch.style.backgroundColor = expanded;
|
|
2422
|
+
colourInput.value = expanded.toLowerCase();
|
|
2423
|
+
hexInput.classList.remove("invalid");
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
colourInput.addEventListener("change", () => {
|
|
2427
|
+
const normalized = normalizeColourValue(colourInput.value);
|
|
2428
|
+
hexInput.value = normalized;
|
|
2429
|
+
swatch.style.backgroundColor = normalized;
|
|
2430
|
+
if (ctx.instance) {
|
|
2431
|
+
ctx.instance.triggerOnChange(pathKey, normalized);
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
swatch.addEventListener("click", () => {
|
|
2435
|
+
colourInput.click();
|
|
2436
|
+
});
|
|
2437
|
+
swatch.addEventListener("mouseenter", () => {
|
|
2438
|
+
swatch.style.borderColor = "var(--fb-border-hover-color)";
|
|
2439
|
+
});
|
|
2440
|
+
swatch.addEventListener("mouseleave", () => {
|
|
2441
|
+
swatch.style.borderColor = "var(--fb-border-color)";
|
|
2442
|
+
});
|
|
2443
|
+
hexInput.addEventListener("focus", () => {
|
|
2444
|
+
hexInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
2445
|
+
hexInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
2446
|
+
hexInput.style.outlineOffset = "0";
|
|
2447
|
+
});
|
|
2448
|
+
hexInput.addEventListener("blur", () => {
|
|
2449
|
+
hexInput.style.borderColor = "var(--fb-border-color)";
|
|
2450
|
+
hexInput.style.outline = "none";
|
|
2451
|
+
});
|
|
2452
|
+
hexInput.addEventListener("mouseenter", () => {
|
|
2453
|
+
if (document.activeElement !== hexInput) {
|
|
2454
|
+
hexInput.style.borderColor = "var(--fb-border-hover-color)";
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
hexInput.addEventListener("mouseleave", () => {
|
|
2458
|
+
if (document.activeElement !== hexInput) {
|
|
2459
|
+
hexInput.style.borderColor = "var(--fb-border-color)";
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
pickerWrapper.appendChild(swatch);
|
|
2463
|
+
pickerWrapper.appendChild(hexInput);
|
|
2464
|
+
pickerWrapper.appendChild(colourInput);
|
|
2465
|
+
return pickerWrapper;
|
|
2466
|
+
}
|
|
2467
|
+
function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
2468
|
+
const state = ctx.state;
|
|
2469
|
+
const initialValue = ctx.prefill[element.key] || element.default || "#000000";
|
|
2470
|
+
if (state.config.readonly) {
|
|
2471
|
+
const readonlyUI = createReadonlyColourUI(initialValue);
|
|
2472
|
+
wrapper.appendChild(readonlyUI);
|
|
2473
|
+
} else {
|
|
2474
|
+
const editUI = createEditColourUI(initialValue, pathKey, ctx);
|
|
2475
|
+
wrapper.appendChild(editUI);
|
|
2476
|
+
}
|
|
2477
|
+
const colourHint = document.createElement("p");
|
|
2478
|
+
colourHint.className = "mt-1";
|
|
2479
|
+
colourHint.style.cssText = `
|
|
2480
|
+
font-size: var(--fb-font-size-small);
|
|
2481
|
+
color: var(--fb-text-secondary-color);
|
|
2482
|
+
`;
|
|
2483
|
+
colourHint.textContent = makeFieldHint(element);
|
|
2484
|
+
wrapper.appendChild(colourHint);
|
|
2485
|
+
}
|
|
2486
|
+
function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
2487
|
+
const state = ctx.state;
|
|
2488
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
2489
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2490
|
+
const minCount = element.minCount ?? 1;
|
|
2491
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
2492
|
+
while (values.length < minCount) {
|
|
2493
|
+
values.push(element.default || "#000000");
|
|
2494
|
+
}
|
|
2495
|
+
const container = document.createElement("div");
|
|
2496
|
+
container.className = "space-y-2";
|
|
2497
|
+
wrapper.appendChild(container);
|
|
2498
|
+
function updateIndices() {
|
|
2499
|
+
const items = container.querySelectorAll(".multiple-colour-item");
|
|
2500
|
+
items.forEach((item, index) => {
|
|
2501
|
+
const input = item.querySelector("input");
|
|
2502
|
+
if (input) {
|
|
2503
|
+
input.name = `${pathKey}[${index}]`;
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
function addColourItem(value = "#000000", index = -1) {
|
|
2508
|
+
const itemWrapper = document.createElement("div");
|
|
2509
|
+
itemWrapper.className = "multiple-colour-item flex items-center gap-2";
|
|
2510
|
+
if (state.config.readonly) {
|
|
2511
|
+
const readonlyUI = createReadonlyColourUI(value);
|
|
2512
|
+
while (readonlyUI.firstChild) {
|
|
2513
|
+
itemWrapper.appendChild(readonlyUI.firstChild);
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
2517
|
+
const editUI = createEditColourUI(value, tempPathKey, ctx);
|
|
2518
|
+
editUI.style.flex = "1";
|
|
2519
|
+
itemWrapper.appendChild(editUI);
|
|
2520
|
+
}
|
|
2521
|
+
if (index === -1) {
|
|
2522
|
+
container.appendChild(itemWrapper);
|
|
2523
|
+
} else {
|
|
2524
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
2525
|
+
}
|
|
2526
|
+
updateIndices();
|
|
2527
|
+
return itemWrapper;
|
|
2528
|
+
}
|
|
2529
|
+
function updateRemoveButtons() {
|
|
2530
|
+
if (state.config.readonly) return;
|
|
2531
|
+
const items = container.querySelectorAll(".multiple-colour-item");
|
|
2532
|
+
const currentCount = items.length;
|
|
2533
|
+
items.forEach((item) => {
|
|
2534
|
+
let removeBtn = item.querySelector(
|
|
2535
|
+
".remove-item-btn"
|
|
2536
|
+
);
|
|
2537
|
+
if (!removeBtn) {
|
|
2538
|
+
removeBtn = document.createElement("button");
|
|
2539
|
+
removeBtn.type = "button";
|
|
2540
|
+
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
2541
|
+
removeBtn.style.cssText = `
|
|
2542
|
+
color: var(--fb-error-color);
|
|
2543
|
+
background-color: transparent;
|
|
2544
|
+
transition: background-color var(--fb-transition-duration);
|
|
2545
|
+
`;
|
|
2546
|
+
removeBtn.innerHTML = "\u2715";
|
|
2547
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
2548
|
+
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2549
|
+
});
|
|
2550
|
+
removeBtn.addEventListener("mouseleave", () => {
|
|
2551
|
+
removeBtn.style.backgroundColor = "transparent";
|
|
2552
|
+
});
|
|
2553
|
+
removeBtn.onclick = () => {
|
|
2554
|
+
const currentIndex = Array.from(container.children).indexOf(
|
|
2555
|
+
item
|
|
2556
|
+
);
|
|
2557
|
+
if (container.children.length > minCount) {
|
|
2558
|
+
values.splice(currentIndex, 1);
|
|
2559
|
+
item.remove();
|
|
2560
|
+
updateIndices();
|
|
2561
|
+
updateAddButton();
|
|
2562
|
+
updateRemoveButtons();
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
item.appendChild(removeBtn);
|
|
2566
|
+
}
|
|
2567
|
+
const disabled = currentCount <= minCount;
|
|
2568
|
+
removeBtn.disabled = disabled;
|
|
2569
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2570
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
function updateAddButton() {
|
|
2574
|
+
const existingAddBtn = wrapper.querySelector(".add-colour-btn");
|
|
2575
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
2576
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
2577
|
+
const addBtn = document.createElement("button");
|
|
2578
|
+
addBtn.type = "button";
|
|
2579
|
+
addBtn.className = "add-colour-btn mt-2 px-3 py-1 rounded";
|
|
2580
|
+
addBtn.style.cssText = `
|
|
2581
|
+
color: var(--fb-primary-color);
|
|
2582
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
2583
|
+
background-color: transparent;
|
|
2584
|
+
font-size: var(--fb-font-size);
|
|
2585
|
+
transition: all var(--fb-transition-duration);
|
|
2586
|
+
`;
|
|
2587
|
+
addBtn.textContent = `+ Add ${element.label || "Colour"}`;
|
|
2588
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
2589
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
2590
|
+
});
|
|
2591
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
2592
|
+
addBtn.style.backgroundColor = "transparent";
|
|
2593
|
+
});
|
|
2594
|
+
addBtn.onclick = () => {
|
|
2595
|
+
const defaultColour = element.default || "#000000";
|
|
2596
|
+
values.push(defaultColour);
|
|
2597
|
+
addColourItem(defaultColour);
|
|
2598
|
+
updateAddButton();
|
|
2599
|
+
updateRemoveButtons();
|
|
2600
|
+
};
|
|
2601
|
+
wrapper.appendChild(addBtn);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
values.forEach((value) => addColourItem(value));
|
|
2605
|
+
updateAddButton();
|
|
2606
|
+
updateRemoveButtons();
|
|
2607
|
+
const hint = document.createElement("p");
|
|
2608
|
+
hint.className = "mt-1";
|
|
2609
|
+
hint.style.cssText = `
|
|
2610
|
+
font-size: var(--fb-font-size-small);
|
|
2611
|
+
color: var(--fb-text-secondary-color);
|
|
2612
|
+
`;
|
|
2613
|
+
hint.textContent = makeFieldHint(element);
|
|
2614
|
+
wrapper.appendChild(hint);
|
|
2615
|
+
}
|
|
2616
|
+
function validateColourElement(element, key, context) {
|
|
2617
|
+
const errors = [];
|
|
2618
|
+
const { scopeRoot, skipValidation } = context;
|
|
2619
|
+
const markValidity = (input, errorMessage) => {
|
|
2620
|
+
if (!input) return;
|
|
2621
|
+
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
2622
|
+
let errorElement = document.getElementById(errorId);
|
|
2623
|
+
if (errorMessage) {
|
|
2624
|
+
input.classList.add("invalid");
|
|
2625
|
+
input.title = errorMessage;
|
|
2626
|
+
if (!errorElement) {
|
|
2627
|
+
errorElement = document.createElement("div");
|
|
2628
|
+
errorElement.id = errorId;
|
|
2629
|
+
errorElement.className = "error-message";
|
|
2630
|
+
errorElement.style.cssText = `
|
|
2631
|
+
color: var(--fb-error-color);
|
|
2632
|
+
font-size: var(--fb-font-size-small);
|
|
2633
|
+
margin-top: 0.25rem;
|
|
2634
|
+
`;
|
|
2635
|
+
if (input.nextSibling) {
|
|
2636
|
+
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
2637
|
+
} else {
|
|
2638
|
+
input.parentNode?.appendChild(errorElement);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
errorElement.textContent = errorMessage;
|
|
2642
|
+
errorElement.style.display = "block";
|
|
2643
|
+
} else {
|
|
2644
|
+
input.classList.remove("invalid");
|
|
2645
|
+
input.title = "";
|
|
2646
|
+
if (errorElement) {
|
|
2647
|
+
errorElement.remove();
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
const validateColourValue = (input, val, fieldKey) => {
|
|
2652
|
+
if (!val) {
|
|
2653
|
+
if (!skipValidation && element.required) {
|
|
2654
|
+
errors.push(`${fieldKey}: required`);
|
|
2655
|
+
markValidity(input, "required");
|
|
2656
|
+
return "";
|
|
2657
|
+
}
|
|
2658
|
+
markValidity(input, null);
|
|
2659
|
+
return "";
|
|
2660
|
+
}
|
|
2661
|
+
const normalized = normalizeColourValue(val);
|
|
2662
|
+
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
2663
|
+
errors.push(`${fieldKey}: invalid hex colour format`);
|
|
2664
|
+
markValidity(input, "invalid hex colour format");
|
|
2665
|
+
return val;
|
|
2666
|
+
}
|
|
2667
|
+
markValidity(input, null);
|
|
2668
|
+
return normalized;
|
|
2669
|
+
};
|
|
2670
|
+
if (element.multiple) {
|
|
2671
|
+
const hexInputs = scopeRoot.querySelectorAll(
|
|
2672
|
+
`.colour-hex-input`
|
|
2673
|
+
);
|
|
2674
|
+
const values = [];
|
|
2675
|
+
hexInputs.forEach((input, index) => {
|
|
2676
|
+
const val = input?.value ?? "";
|
|
2677
|
+
const validated = validateColourValue(input, val, `${key}[${index}]`);
|
|
2678
|
+
values.push(validated);
|
|
2679
|
+
});
|
|
2680
|
+
if (!skipValidation) {
|
|
2681
|
+
const minCount = element.minCount ?? 1;
|
|
2682
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
2683
|
+
const filteredValues = values.filter((v) => v !== "");
|
|
2684
|
+
if (element.required && filteredValues.length === 0) {
|
|
2685
|
+
errors.push(`${key}: required`);
|
|
2686
|
+
}
|
|
2687
|
+
if (filteredValues.length < minCount) {
|
|
2688
|
+
errors.push(`${key}: minimum ${minCount} items required`);
|
|
2689
|
+
}
|
|
2690
|
+
if (filteredValues.length > maxCount) {
|
|
2691
|
+
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return { value: values, errors };
|
|
2695
|
+
} else {
|
|
2696
|
+
const hexInput = scopeRoot.querySelector(
|
|
2697
|
+
`[name="${key}"].colour-hex-input`
|
|
2698
|
+
);
|
|
2699
|
+
const val = hexInput?.value ?? "";
|
|
2700
|
+
if (!skipValidation && element.required && val === "") {
|
|
2701
|
+
errors.push(`${key}: required`);
|
|
2702
|
+
markValidity(hexInput, "required");
|
|
2703
|
+
return { value: "", errors };
|
|
2704
|
+
}
|
|
2705
|
+
const validated = validateColourValue(hexInput, val, key);
|
|
2706
|
+
return { value: validated, errors };
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
function updateColourField(element, fieldPath, value, context) {
|
|
2710
|
+
const { scopeRoot } = context;
|
|
2711
|
+
if (element.multiple) {
|
|
2712
|
+
if (!Array.isArray(value)) {
|
|
2713
|
+
console.warn(
|
|
2714
|
+
`updateColourField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
|
|
2715
|
+
);
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
const hexInputs = scopeRoot.querySelectorAll(
|
|
2719
|
+
`.colour-hex-input`
|
|
2720
|
+
);
|
|
2721
|
+
hexInputs.forEach((hexInput, index) => {
|
|
2722
|
+
if (index < value.length) {
|
|
2723
|
+
const normalized = normalizeColourValue(value[index]);
|
|
2724
|
+
hexInput.value = normalized;
|
|
2725
|
+
hexInput.classList.remove("invalid");
|
|
2726
|
+
hexInput.title = "";
|
|
2727
|
+
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
2728
|
+
if (wrapper) {
|
|
2729
|
+
const swatch = wrapper.querySelector(".colour-swatch");
|
|
2730
|
+
const colourInput = wrapper.querySelector(".colour-picker-hidden");
|
|
2731
|
+
if (swatch) {
|
|
2732
|
+
swatch.style.backgroundColor = normalized;
|
|
2733
|
+
}
|
|
2734
|
+
if (colourInput) {
|
|
2735
|
+
colourInput.value = normalized.toLowerCase();
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
if (value.length !== hexInputs.length) {
|
|
2741
|
+
console.warn(
|
|
2742
|
+
`updateColourField: Multiple field "${fieldPath}" has ${hexInputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
} else {
|
|
2746
|
+
const hexInput = scopeRoot.querySelector(
|
|
2747
|
+
`[name="${fieldPath}"].colour-hex-input`
|
|
2748
|
+
);
|
|
2749
|
+
if (hexInput) {
|
|
2750
|
+
const normalized = normalizeColourValue(value);
|
|
2751
|
+
hexInput.value = normalized;
|
|
2752
|
+
hexInput.classList.remove("invalid");
|
|
2753
|
+
hexInput.title = "";
|
|
2754
|
+
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
2755
|
+
if (wrapper) {
|
|
2756
|
+
const swatch = wrapper.querySelector(".colour-swatch");
|
|
2757
|
+
const colourInput = wrapper.querySelector(".colour-picker-hidden");
|
|
2758
|
+
if (swatch) {
|
|
2759
|
+
swatch.style.backgroundColor = normalized;
|
|
2760
|
+
}
|
|
2761
|
+
if (colourInput) {
|
|
2762
|
+
colourInput.value = normalized.toLowerCase();
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2200
2769
|
// src/components/container.ts
|
|
2201
2770
|
var renderElementFunc = null;
|
|
2202
2771
|
function setRenderElement(fn) {
|
|
@@ -2225,6 +2794,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2225
2794
|
const subCtx = {
|
|
2226
2795
|
path: pathJoin(ctx.path, element.key),
|
|
2227
2796
|
prefill: ctx.prefill?.[element.key] || {},
|
|
2797
|
+
// Sliced data for value population
|
|
2798
|
+
formData: ctx.formData ?? ctx.prefill,
|
|
2799
|
+
// Complete root data for displayIf evaluation
|
|
2228
2800
|
state: ctx.state
|
|
2229
2801
|
};
|
|
2230
2802
|
element.elements.forEach((child) => {
|
|
@@ -2268,7 +2840,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2268
2840
|
const subCtx = {
|
|
2269
2841
|
state: ctx.state,
|
|
2270
2842
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2271
|
-
prefill: {}
|
|
2843
|
+
prefill: {},
|
|
2844
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2845
|
+
// Complete root data for displayIf
|
|
2272
2846
|
};
|
|
2273
2847
|
const item = document.createElement("div");
|
|
2274
2848
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2313,7 +2887,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2313
2887
|
const subCtx = {
|
|
2314
2888
|
state: ctx.state,
|
|
2315
2889
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2316
|
-
prefill: prefillObj || {}
|
|
2890
|
+
prefill: prefillObj || {},
|
|
2891
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2892
|
+
// Complete root data for displayIf
|
|
2317
2893
|
};
|
|
2318
2894
|
const item = document.createElement("div");
|
|
2319
2895
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2344,7 +2920,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2344
2920
|
const subCtx = {
|
|
2345
2921
|
state: ctx.state,
|
|
2346
2922
|
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2347
|
-
prefill: {}
|
|
2923
|
+
prefill: {},
|
|
2924
|
+
formData: ctx.formData ?? ctx.prefill
|
|
2925
|
+
// Complete root data for displayIf
|
|
2348
2926
|
};
|
|
2349
2927
|
const item = document.createElement("div");
|
|
2350
2928
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
@@ -2624,11 +3202,33 @@ if (typeof document !== "undefined") {
|
|
|
2624
3202
|
}
|
|
2625
3203
|
});
|
|
2626
3204
|
}
|
|
2627
|
-
function
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
3205
|
+
function checkDisplayCondition(element, ctx) {
|
|
3206
|
+
if (!element.displayIf) {
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
const dataForCondition = ctx.formData ?? ctx.prefill;
|
|
3211
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
3212
|
+
element.displayIf,
|
|
3213
|
+
dataForCondition
|
|
3214
|
+
);
|
|
3215
|
+
if (!shouldDisplay) {
|
|
3216
|
+
const hiddenWrapper = document.createElement("div");
|
|
3217
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
3218
|
+
hiddenWrapper.style.display = "none";
|
|
3219
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
3220
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
3221
|
+
return hiddenWrapper;
|
|
3222
|
+
}
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
console.error(
|
|
3225
|
+
`Error evaluating displayIf for field "${element.key}":`,
|
|
3226
|
+
error
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
return null;
|
|
3230
|
+
}
|
|
3231
|
+
function createFieldLabel(element) {
|
|
2632
3232
|
const title = document.createElement("label");
|
|
2633
3233
|
title.className = "text-sm font-medium text-gray-900";
|
|
2634
3234
|
title.textContent = element.label || element.key;
|
|
@@ -2638,59 +3238,71 @@ function renderElement2(element, ctx) {
|
|
|
2638
3238
|
req.textContent = "*";
|
|
2639
3239
|
title.appendChild(req);
|
|
2640
3240
|
}
|
|
3241
|
+
return title;
|
|
3242
|
+
}
|
|
3243
|
+
function createInfoButton(element) {
|
|
3244
|
+
const infoBtn = document.createElement("button");
|
|
3245
|
+
infoBtn.type = "button";
|
|
3246
|
+
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
3247
|
+
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>';
|
|
3248
|
+
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3249
|
+
const tooltip = document.createElement("div");
|
|
3250
|
+
tooltip.id = tooltipId;
|
|
3251
|
+
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";
|
|
3252
|
+
tooltip.style.position = "fixed";
|
|
3253
|
+
tooltip.textContent = element.description || element.hint || "Field information";
|
|
3254
|
+
document.body.appendChild(tooltip);
|
|
3255
|
+
infoBtn.onclick = (e) => {
|
|
3256
|
+
e.preventDefault();
|
|
3257
|
+
e.stopPropagation();
|
|
3258
|
+
showTooltip(tooltipId, infoBtn);
|
|
3259
|
+
};
|
|
3260
|
+
return infoBtn;
|
|
3261
|
+
}
|
|
3262
|
+
function createLabelContainer(element) {
|
|
3263
|
+
const label = document.createElement("div");
|
|
3264
|
+
label.className = "flex items-center mb-2";
|
|
3265
|
+
const title = createFieldLabel(element);
|
|
2641
3266
|
label.appendChild(title);
|
|
2642
3267
|
if (element.description || element.hint) {
|
|
2643
|
-
const infoBtn =
|
|
2644
|
-
infoBtn.type = "button";
|
|
2645
|
-
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
2646
|
-
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>';
|
|
2647
|
-
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2648
|
-
const tooltip = document.createElement("div");
|
|
2649
|
-
tooltip.id = tooltipId;
|
|
2650
|
-
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";
|
|
2651
|
-
tooltip.style.position = "fixed";
|
|
2652
|
-
tooltip.textContent = element.description || element.hint || "Field information";
|
|
2653
|
-
document.body.appendChild(tooltip);
|
|
2654
|
-
infoBtn.onclick = (e) => {
|
|
2655
|
-
e.preventDefault();
|
|
2656
|
-
e.stopPropagation();
|
|
2657
|
-
showTooltip(tooltipId, infoBtn);
|
|
2658
|
-
};
|
|
3268
|
+
const infoBtn = createInfoButton(element);
|
|
2659
3269
|
label.appendChild(infoBtn);
|
|
2660
3270
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
3271
|
+
return label;
|
|
3272
|
+
}
|
|
3273
|
+
function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
3274
|
+
const isMultiple = "multiple" in element && element.multiple;
|
|
2663
3275
|
switch (element.type) {
|
|
2664
3276
|
case "text":
|
|
2665
|
-
if (
|
|
3277
|
+
if (isMultiple) {
|
|
2666
3278
|
renderMultipleTextElement(element, ctx, wrapper, pathKey);
|
|
2667
3279
|
} else {
|
|
2668
3280
|
renderTextElement(element, ctx, wrapper, pathKey);
|
|
2669
3281
|
}
|
|
2670
3282
|
break;
|
|
2671
3283
|
case "textarea":
|
|
2672
|
-
if (
|
|
3284
|
+
if (isMultiple) {
|
|
2673
3285
|
renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
|
|
2674
3286
|
} else {
|
|
2675
3287
|
renderTextareaElement(element, ctx, wrapper, pathKey);
|
|
2676
3288
|
}
|
|
2677
3289
|
break;
|
|
2678
3290
|
case "number":
|
|
2679
|
-
if (
|
|
3291
|
+
if (isMultiple) {
|
|
2680
3292
|
renderMultipleNumberElement(element, ctx, wrapper, pathKey);
|
|
2681
3293
|
} else {
|
|
2682
3294
|
renderNumberElement(element, ctx, wrapper, pathKey);
|
|
2683
3295
|
}
|
|
2684
3296
|
break;
|
|
2685
3297
|
case "select":
|
|
2686
|
-
if (
|
|
3298
|
+
if (isMultiple) {
|
|
2687
3299
|
renderMultipleSelectElement(element, ctx, wrapper, pathKey);
|
|
2688
3300
|
} else {
|
|
2689
3301
|
renderSelectElement(element, ctx, wrapper, pathKey);
|
|
2690
3302
|
}
|
|
2691
3303
|
break;
|
|
2692
3304
|
case "file":
|
|
2693
|
-
if (
|
|
3305
|
+
if (isMultiple) {
|
|
2694
3306
|
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
2695
3307
|
} else {
|
|
2696
3308
|
renderFileElement(element, ctx, wrapper, pathKey);
|
|
@@ -2699,11 +3311,18 @@ function renderElement2(element, ctx) {
|
|
|
2699
3311
|
case "files":
|
|
2700
3312
|
renderFilesElement(element, ctx, wrapper, pathKey);
|
|
2701
3313
|
break;
|
|
3314
|
+
case "colour":
|
|
3315
|
+
if (isMultiple) {
|
|
3316
|
+
renderMultipleColourElement(element, ctx, wrapper, pathKey);
|
|
3317
|
+
} else {
|
|
3318
|
+
renderColourElement(element, ctx, wrapper, pathKey);
|
|
3319
|
+
}
|
|
3320
|
+
break;
|
|
2702
3321
|
case "group":
|
|
2703
3322
|
renderGroupElement(element, ctx, wrapper, pathKey);
|
|
2704
3323
|
break;
|
|
2705
3324
|
case "container":
|
|
2706
|
-
if (
|
|
3325
|
+
if (isMultiple) {
|
|
2707
3326
|
renderMultipleContainerElement(element, ctx, wrapper);
|
|
2708
3327
|
} else {
|
|
2709
3328
|
renderSingleContainerElement(element, ctx, wrapper, pathKey);
|
|
@@ -2716,6 +3335,19 @@ function renderElement2(element, ctx) {
|
|
|
2716
3335
|
wrapper.appendChild(unsupported);
|
|
2717
3336
|
}
|
|
2718
3337
|
}
|
|
3338
|
+
}
|
|
3339
|
+
function renderElement2(element, ctx) {
|
|
3340
|
+
const hiddenElement = checkDisplayCondition(element, ctx);
|
|
3341
|
+
if (hiddenElement) {
|
|
3342
|
+
return hiddenElement;
|
|
3343
|
+
}
|
|
3344
|
+
const wrapper = document.createElement("div");
|
|
3345
|
+
wrapper.className = "mb-6 fb-field-wrapper";
|
|
3346
|
+
wrapper.setAttribute("data-field-key", element.key);
|
|
3347
|
+
const label = createLabelContainer(element);
|
|
3348
|
+
wrapper.appendChild(label);
|
|
3349
|
+
const pathKey = pathJoin(ctx.path, element.key);
|
|
3350
|
+
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
2719
3351
|
return wrapper;
|
|
2720
3352
|
}
|
|
2721
3353
|
setRenderElement(renderElement2);
|
|
@@ -2999,6 +3631,10 @@ var componentRegistry = {
|
|
|
2999
3631
|
validate: validateFileElement,
|
|
3000
3632
|
update: updateFileField
|
|
3001
3633
|
},
|
|
3634
|
+
colour: {
|
|
3635
|
+
validate: validateColourElement,
|
|
3636
|
+
update: updateColourField
|
|
3637
|
+
},
|
|
3002
3638
|
container: {
|
|
3003
3639
|
validate: validateContainerElement,
|
|
3004
3640
|
update: updateContainerField
|
|
@@ -3119,6 +3755,7 @@ var FormBuilderInstance = class {
|
|
|
3119
3755
|
}
|
|
3120
3756
|
this.state.debounceTimer = setTimeout(() => {
|
|
3121
3757
|
const formData = this.validateForm(true);
|
|
3758
|
+
this.reevaluateConditionalFields();
|
|
3122
3759
|
if (this.state.config.onChange) {
|
|
3123
3760
|
this.state.config.onChange(formData);
|
|
3124
3761
|
}
|
|
@@ -3378,6 +4015,8 @@ var FormBuilderInstance = class {
|
|
|
3378
4015
|
const block = renderElement2(element, {
|
|
3379
4016
|
path: "",
|
|
3380
4017
|
prefill: prefill || {},
|
|
4018
|
+
formData: prefill || {},
|
|
4019
|
+
// Pass complete root data for displayIf evaluation
|
|
3381
4020
|
state: this.state,
|
|
3382
4021
|
instance: this
|
|
3383
4022
|
});
|
|
@@ -3422,6 +4061,19 @@ var FormBuilderInstance = class {
|
|
|
3422
4061
|
};
|
|
3423
4062
|
setValidateElement(validateElement2);
|
|
3424
4063
|
this.state.schema.elements.forEach((element) => {
|
|
4064
|
+
if (element.displayIf) {
|
|
4065
|
+
try {
|
|
4066
|
+
const shouldDisplay = evaluateDisplayCondition(element.displayIf, data);
|
|
4067
|
+
if (!shouldDisplay) {
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
} catch (error) {
|
|
4071
|
+
console.error(
|
|
4072
|
+
`Error evaluating displayIf for field "${element.key}" during validation:`,
|
|
4073
|
+
error
|
|
4074
|
+
);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
3425
4077
|
if (element.hidden) {
|
|
3426
4078
|
data[element.key] = element.default !== void 0 ? element.default : null;
|
|
3427
4079
|
} else {
|
|
@@ -3578,6 +4230,70 @@ var FormBuilderInstance = class {
|
|
|
3578
4230
|
);
|
|
3579
4231
|
}
|
|
3580
4232
|
}
|
|
4233
|
+
/**
|
|
4234
|
+
* Re-evaluate all conditional fields (displayIf) based on current form data
|
|
4235
|
+
* This is called automatically when form data changes (via onChange events)
|
|
4236
|
+
*/
|
|
4237
|
+
reevaluateConditionalFields() {
|
|
4238
|
+
if (!this.state.schema || !this.state.formRoot) return;
|
|
4239
|
+
const formData = this.validateForm(true).data;
|
|
4240
|
+
const checkElements = (elements, currentPath) => {
|
|
4241
|
+
elements.forEach((element) => {
|
|
4242
|
+
const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
|
|
4243
|
+
if (element.displayIf) {
|
|
4244
|
+
const fieldWrappers = this.state.formRoot.querySelectorAll(
|
|
4245
|
+
`[data-field-key="${element.key}"]`
|
|
4246
|
+
);
|
|
4247
|
+
fieldWrappers.forEach((wrapper) => {
|
|
4248
|
+
try {
|
|
4249
|
+
const shouldDisplay = evaluateDisplayCondition(
|
|
4250
|
+
element.displayIf,
|
|
4251
|
+
formData
|
|
4252
|
+
// Use complete formData for condition evaluation
|
|
4253
|
+
);
|
|
4254
|
+
const isCurrentlyHidden = wrapper.getAttribute("data-conditionally-hidden") === "true";
|
|
4255
|
+
if (shouldDisplay && isCurrentlyHidden) {
|
|
4256
|
+
const newWrapper = renderElement2(element, {
|
|
4257
|
+
path: fullPath,
|
|
4258
|
+
// Use accumulated path
|
|
4259
|
+
prefill: formData,
|
|
4260
|
+
// Use complete formData for root-level elements
|
|
4261
|
+
formData,
|
|
4262
|
+
// Pass complete formData for displayIf evaluation
|
|
4263
|
+
state: this.state,
|
|
4264
|
+
instance: this
|
|
4265
|
+
});
|
|
4266
|
+
wrapper.parentNode?.replaceChild(newWrapper, wrapper);
|
|
4267
|
+
} else if (!shouldDisplay && !isCurrentlyHidden) {
|
|
4268
|
+
const hiddenWrapper = document.createElement("div");
|
|
4269
|
+
hiddenWrapper.className = "fb-field-wrapper-hidden";
|
|
4270
|
+
hiddenWrapper.style.display = "none";
|
|
4271
|
+
hiddenWrapper.setAttribute("data-field-key", element.key);
|
|
4272
|
+
hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
|
|
4273
|
+
wrapper.parentNode?.replaceChild(hiddenWrapper, wrapper);
|
|
4274
|
+
}
|
|
4275
|
+
} catch (error) {
|
|
4276
|
+
console.error(
|
|
4277
|
+
`Error re-evaluating displayIf for field "${element.key}":`,
|
|
4278
|
+
error
|
|
4279
|
+
);
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
|
|
4284
|
+
const containerData = formData?.[element.key];
|
|
4285
|
+
if (Array.isArray(containerData)) {
|
|
4286
|
+
containerData.forEach((_, index) => {
|
|
4287
|
+
checkElements(element.elements, `${fullPath}[${index}]`);
|
|
4288
|
+
});
|
|
4289
|
+
} else {
|
|
4290
|
+
checkElements(element.elements, fullPath);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
});
|
|
4294
|
+
};
|
|
4295
|
+
checkElements(this.state.schema.elements, "");
|
|
4296
|
+
}
|
|
3581
4297
|
/**
|
|
3582
4298
|
* Destroy instance and clean up resources
|
|
3583
4299
|
*/
|