@dmitryvim/form-builder 0.2.7 → 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 +104 -41
- package/dist/browser/formbuilder.v0.2.8.min.js +304 -0
- package/dist/cjs/index.cjs +657 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +651 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +104 -41
- package/dist/types/components/index.d.ts +2 -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 +20 -1
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.7.min.js +0 -241
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) {
|
|
@@ -2766,6 +2803,516 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
2766
2803
|
}
|
|
2767
2804
|
}
|
|
2768
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
|
+
|
|
2769
3316
|
// src/components/container.ts
|
|
2770
3317
|
var renderElementFunc = null;
|
|
2771
3318
|
function setRenderElement(fn) {
|
|
@@ -2779,6 +3326,24 @@ function renderElement(element, ctx) {
|
|
|
2779
3326
|
}
|
|
2780
3327
|
return renderElementFunc(element, ctx);
|
|
2781
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
|
+
}
|
|
2782
3347
|
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
2783
3348
|
const containerWrap = document.createElement("div");
|
|
2784
3349
|
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
@@ -2788,9 +3353,20 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2788
3353
|
const left = document.createElement("div");
|
|
2789
3354
|
left.className = "flex-1";
|
|
2790
3355
|
const itemsWrap = document.createElement("div");
|
|
2791
|
-
|
|
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
|
+
}
|
|
2792
3362
|
containerWrap.appendChild(header);
|
|
2793
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
|
+
}
|
|
2794
3370
|
const subCtx = {
|
|
2795
3371
|
path: pathJoin(ctx.path, element.key),
|
|
2796
3372
|
prefill: ctx.prefill?.[element.key] || {},
|
|
@@ -2825,6 +3401,12 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2825
3401
|
if (!state.config.readonly) {
|
|
2826
3402
|
header.appendChild(right);
|
|
2827
3403
|
}
|
|
3404
|
+
if (!ctx.state.config.readonly) {
|
|
3405
|
+
const hintsElement = createPrefillHints(element, element.key);
|
|
3406
|
+
if (hintsElement) {
|
|
3407
|
+
containerWrap.appendChild(hintsElement);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
2828
3410
|
const min = element.minCount ?? 0;
|
|
2829
3411
|
const max = element.maxCount ?? Infinity;
|
|
2830
3412
|
const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
|
|
@@ -2847,11 +3429,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2847
3429
|
const item = document.createElement("div");
|
|
2848
3430
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2849
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
|
+
}
|
|
2850
3439
|
element.elements.forEach((child) => {
|
|
2851
3440
|
if (!child.hidden) {
|
|
2852
|
-
|
|
3441
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
2853
3442
|
}
|
|
2854
3443
|
});
|
|
3444
|
+
item.appendChild(childWrapper);
|
|
2855
3445
|
if (!state.config.readonly) {
|
|
2856
3446
|
const rem = document.createElement("button");
|
|
2857
3447
|
rem.type = "button";
|
|
@@ -2894,11 +3484,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2894
3484
|
const item = document.createElement("div");
|
|
2895
3485
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2896
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
|
+
}
|
|
2897
3494
|
element.elements.forEach((child) => {
|
|
2898
3495
|
if (!child.hidden) {
|
|
2899
|
-
|
|
3496
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
2900
3497
|
}
|
|
2901
3498
|
});
|
|
3499
|
+
item.appendChild(childWrapper);
|
|
2902
3500
|
if (!state.config.readonly) {
|
|
2903
3501
|
const rem = document.createElement("button");
|
|
2904
3502
|
rem.type = "button";
|
|
@@ -2927,11 +3525,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2927
3525
|
const item = document.createElement("div");
|
|
2928
3526
|
item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2929
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
|
+
}
|
|
2930
3535
|
element.elements.forEach((child) => {
|
|
2931
3536
|
if (!child.hidden) {
|
|
2932
|
-
|
|
3537
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
2933
3538
|
}
|
|
2934
3539
|
});
|
|
3540
|
+
item.appendChild(childWrapper);
|
|
2935
3541
|
const rem = document.createElement("button");
|
|
2936
3542
|
rem.type = "button";
|
|
2937
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";
|
|
@@ -3318,6 +3924,13 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
3318
3924
|
renderColourElement(element, ctx, wrapper, pathKey);
|
|
3319
3925
|
}
|
|
3320
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;
|
|
3321
3934
|
case "group":
|
|
3322
3935
|
renderGroupElement(element, ctx, wrapper, pathKey);
|
|
3323
3936
|
break;
|
|
@@ -3635,6 +4248,10 @@ var componentRegistry = {
|
|
|
3635
4248
|
validate: validateColourElement,
|
|
3636
4249
|
update: updateColourField
|
|
3637
4250
|
},
|
|
4251
|
+
slider: {
|
|
4252
|
+
validate: validateSliderElement,
|
|
4253
|
+
update: updateSliderField
|
|
4254
|
+
},
|
|
3638
4255
|
container: {
|
|
3639
4256
|
validate: validateContainerElement,
|
|
3640
4257
|
update: updateContainerField
|
|
@@ -3991,6 +4608,33 @@ var FormBuilderInstance = class {
|
|
|
3991
4608
|
this.renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
|
|
3992
4609
|
}
|
|
3993
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
|
+
}
|
|
3994
4638
|
/**
|
|
3995
4639
|
* Render form from schema
|
|
3996
4640
|
*/
|
|
@@ -4023,6 +4667,9 @@ var FormBuilderInstance = class {
|
|
|
4023
4667
|
formEl.appendChild(block);
|
|
4024
4668
|
});
|
|
4025
4669
|
root.appendChild(formEl);
|
|
4670
|
+
if (!this.state.config.readonly) {
|
|
4671
|
+
root.addEventListener("click", this.handlePrefillHintClick.bind(this));
|
|
4672
|
+
}
|
|
4026
4673
|
if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
|
|
4027
4674
|
this.renderExternalActions();
|
|
4028
4675
|
}
|