@dmitryvim/form-builder 0.2.29 → 0.2.32
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 +51 -0
- package/dist/browser/formbuilder.min.js +578 -386
- package/dist/browser/formbuilder.v0.2.32.min.js +1148 -0
- package/dist/cjs/index.cjs +1477 -789
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1449 -775
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +578 -386
- package/dist/types/components/file/dom.d.ts +4 -5
- package/dist/types/components/file/preview.d.ts +5 -0
- package/dist/types/components/file/render-edit.d.ts +8 -1
- package/dist/types/components/file/render-readonly.d.ts +4 -7
- package/dist/types/components/file/upload.d.ts +6 -0
- package/dist/types/components/markdown/index.d.ts +15 -0
- package/dist/types/components/markdown/render.d.ts +6 -0
- package/dist/types/components/markdown/snarkdown.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/instance/FormBuilderInstance.d.ts +10 -0
- package/dist/types/types/component-operations.d.ts +5 -0
- package/dist/types/types/config.d.ts +4 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +22 -1
- package/dist/types/types/state.d.ts +5 -1
- package/dist/types/utils/helpers.d.ts +14 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.29.min.js +0 -956
package/dist/esm/index.js
CHANGED
|
@@ -46,8 +46,9 @@ function addRangeHint(element, parts, state) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
function addFileSizeHint(element, parts, state) {
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const sizeMB = element.maxSize ?? element.maxSizeMB;
|
|
50
|
+
if (sizeMB && sizeMB !== Infinity) {
|
|
51
|
+
parts.push(t("hintMaxSize", state, { size: sizeMB }));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
function addFormatHint(element, parts, state) {
|
|
@@ -120,6 +121,25 @@ function validateSchema(schema) {
|
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
}
|
|
124
|
+
function validateContainerProps(element, elementPath, errors2) {
|
|
125
|
+
if ("columns" in element && element.columns !== void 0) {
|
|
126
|
+
const columns = element.columns;
|
|
127
|
+
const validColumns = [1, 2, 3, 4];
|
|
128
|
+
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
129
|
+
errors2.push(
|
|
130
|
+
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if ("displayMode" in element && element.displayMode !== void 0) {
|
|
135
|
+
const displayMode = element.displayMode;
|
|
136
|
+
if (displayMode !== "stack" && displayMode !== "slides") {
|
|
137
|
+
errors2.push(
|
|
138
|
+
`${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
123
143
|
function checkFlatOutputCollisions(elements, scopePath) {
|
|
124
144
|
const allOutputKeys = /* @__PURE__ */ new Set();
|
|
125
145
|
for (const el of elements) {
|
|
@@ -153,12 +173,14 @@ function validateSchema(schema) {
|
|
|
153
173
|
allOutputKeys.add(textKey);
|
|
154
174
|
allOutputKeys.add(filesKey);
|
|
155
175
|
} else {
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
if (el.key) {
|
|
177
|
+
if (allOutputKeys.has(el.key)) {
|
|
178
|
+
errors.push(
|
|
179
|
+
`${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
allOutputKeys.add(el.key);
|
|
160
183
|
}
|
|
161
|
-
allOutputKeys.add(el.key);
|
|
162
184
|
}
|
|
163
185
|
}
|
|
164
186
|
}
|
|
@@ -168,9 +190,17 @@ function validateSchema(schema) {
|
|
|
168
190
|
if (!element.type) {
|
|
169
191
|
errors.push(`${elementPath}: missing type`);
|
|
170
192
|
}
|
|
171
|
-
if (!element.key) {
|
|
193
|
+
if (!element.key && element.type !== "markdown") {
|
|
172
194
|
errors.push(`${elementPath}: missing key`);
|
|
173
195
|
}
|
|
196
|
+
if (element.type === "markdown") {
|
|
197
|
+
const content = element.content;
|
|
198
|
+
if (typeof content !== "string") {
|
|
199
|
+
errors.push(
|
|
200
|
+
`${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
174
204
|
if (element.enableIf) {
|
|
175
205
|
const enableIf = element.enableIf;
|
|
176
206
|
if (!enableIf.key || typeof enableIf.key !== "string") {
|
|
@@ -189,15 +219,7 @@ function validateSchema(schema) {
|
|
|
189
219
|
validateElements(element.elements, `${elementPath}.elements`);
|
|
190
220
|
}
|
|
191
221
|
if (element.type === "container" && element.elements) {
|
|
192
|
-
|
|
193
|
-
const columns = element.columns;
|
|
194
|
-
const validColumns = [1, 2, 3, 4];
|
|
195
|
-
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
196
|
-
errors.push(
|
|
197
|
-
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
222
|
+
validateContainerProps(element, elementPath, errors);
|
|
201
223
|
if ("prefillHints" in element && element.prefillHints) {
|
|
202
224
|
const prefillHints = element.prefillHints;
|
|
203
225
|
if (Array.isArray(prefillHints)) {
|
|
@@ -263,6 +285,18 @@ function escapeHtml(text) {
|
|
|
263
285
|
div.textContent = text;
|
|
264
286
|
return div.innerHTML;
|
|
265
287
|
}
|
|
288
|
+
function getElementLookupKey(element, state) {
|
|
289
|
+
if (element.key) {
|
|
290
|
+
return element.key;
|
|
291
|
+
}
|
|
292
|
+
const cached = state.syntheticElementIds.get(element);
|
|
293
|
+
if (cached !== void 0) {
|
|
294
|
+
return cached;
|
|
295
|
+
}
|
|
296
|
+
const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
|
|
297
|
+
state.syntheticElementIds.set(element, id);
|
|
298
|
+
return id;
|
|
299
|
+
}
|
|
266
300
|
function pathJoin(base, key) {
|
|
267
301
|
return base ? `${base}.${key}` : key;
|
|
268
302
|
}
|
|
@@ -818,8 +852,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
818
852
|
const textareaWrapper = document.createElement("div");
|
|
819
853
|
textareaWrapper.style.cssText = "position: relative;";
|
|
820
854
|
const textareaInput = document.createElement("textarea");
|
|
821
|
-
textareaInput.className = "w-full
|
|
822
|
-
textareaInput.style.cssText =
|
|
855
|
+
textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
856
|
+
textareaInput.style.cssText = `
|
|
857
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
|
|
858
|
+
font-size: var(--fb-font-size);
|
|
859
|
+
font-family: var(--fb-font-family);
|
|
860
|
+
`;
|
|
823
861
|
textareaInput.name = pathKey;
|
|
824
862
|
textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
825
863
|
textareaInput.rows = element.rows || 4;
|
|
@@ -871,8 +909,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
871
909
|
const textareaContainer = document.createElement("div");
|
|
872
910
|
textareaContainer.style.cssText = "position: relative;";
|
|
873
911
|
const textareaInput = document.createElement("textarea");
|
|
874
|
-
textareaInput.className = "w-full
|
|
875
|
-
textareaInput.style.cssText =
|
|
912
|
+
textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
913
|
+
textareaInput.style.cssText = `
|
|
914
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
|
|
915
|
+
font-size: var(--fb-font-size);
|
|
916
|
+
font-family: var(--fb-font-family);
|
|
917
|
+
`;
|
|
876
918
|
textareaInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
877
919
|
textareaInput.rows = element.rows || 4;
|
|
878
920
|
textareaInput.value = value;
|
|
@@ -1058,8 +1100,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1058
1100
|
inputWrapper.style.cssText = "position: relative;";
|
|
1059
1101
|
const numberInput = document.createElement("input");
|
|
1060
1102
|
numberInput.type = "number";
|
|
1061
|
-
numberInput.className = "w-full
|
|
1062
|
-
numberInput.style.cssText =
|
|
1103
|
+
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1104
|
+
numberInput.style.cssText = `
|
|
1105
|
+
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1106
|
+
font-size: var(--fb-font-size);
|
|
1107
|
+
font-family: var(--fb-font-family);
|
|
1108
|
+
width: 100%;
|
|
1109
|
+
box-sizing: border-box;
|
|
1110
|
+
`;
|
|
1063
1111
|
numberInput.name = pathKey;
|
|
1064
1112
|
numberInput.placeholder = element.placeholder || "0";
|
|
1065
1113
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
@@ -1111,8 +1159,14 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1111
1159
|
inputContainer.style.cssText = "position: relative; flex: 1;";
|
|
1112
1160
|
const numberInput = document.createElement("input");
|
|
1113
1161
|
numberInput.type = "number";
|
|
1114
|
-
numberInput.className = "w-full
|
|
1115
|
-
numberInput.style.cssText =
|
|
1162
|
+
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1163
|
+
numberInput.style.cssText = `
|
|
1164
|
+
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1165
|
+
font-size: var(--fb-font-size);
|
|
1166
|
+
font-family: var(--fb-font-family);
|
|
1167
|
+
width: 100%;
|
|
1168
|
+
box-sizing: border-box;
|
|
1169
|
+
`;
|
|
1116
1170
|
numberInput.placeholder = element.placeholder || "0";
|
|
1117
1171
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1118
1172
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
@@ -1383,7 +1437,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1383
1437
|
const state = ctx.state;
|
|
1384
1438
|
const readonly = isElementReadonly(element, state, ctx);
|
|
1385
1439
|
const selectInput = document.createElement("select");
|
|
1386
|
-
selectInput.className = "w-full
|
|
1440
|
+
selectInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1441
|
+
selectInput.style.cssText = `
|
|
1442
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1443
|
+
font-size: var(--fb-font-size);
|
|
1444
|
+
font-family: var(--fb-font-family);
|
|
1445
|
+
`;
|
|
1387
1446
|
selectInput.name = pathKey;
|
|
1388
1447
|
selectInput.disabled = readonly;
|
|
1389
1448
|
(element.options || []).forEach((option) => {
|
|
@@ -1435,7 +1494,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1435
1494
|
const itemWrapper = document.createElement("div");
|
|
1436
1495
|
itemWrapper.className = "multiple-select-item flex items-center gap-2";
|
|
1437
1496
|
const selectInput = document.createElement("select");
|
|
1438
|
-
selectInput.className = "flex-1
|
|
1497
|
+
selectInput.className = "flex-1 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1498
|
+
selectInput.style.cssText = `
|
|
1499
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1500
|
+
font-size: var(--fb-font-size);
|
|
1501
|
+
font-family: var(--fb-font-family);
|
|
1502
|
+
`;
|
|
1439
1503
|
selectInput.disabled = readonly;
|
|
1440
1504
|
(element.options || []).forEach((option) => {
|
|
1441
1505
|
const optionElement = document.createElement("option");
|
|
@@ -2170,7 +2234,13 @@ function ensureFileStyles() {
|
|
|
2170
2234
|
style.textContent = `
|
|
2171
2235
|
@keyframes fb-spin { to { transform: rotate(360deg); } }
|
|
2172
2236
|
|
|
2173
|
-
/*
|
|
2237
|
+
/* \u2500\u2500\u2500 Checker background utility \u2500\u2500\u2500 */
|
|
2238
|
+
/* Neutral diagonal-stripe background for image previews (never crops) */
|
|
2239
|
+
.fb-checker {
|
|
2240
|
+
background-image: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
/* \u2500\u2500\u2500 Spinner \u2500\u2500\u2500 */
|
|
2174
2244
|
.fb-spinner {
|
|
2175
2245
|
width: 36px;
|
|
2176
2246
|
height: 36px;
|
|
@@ -2181,207 +2251,271 @@ function ensureFileStyles() {
|
|
|
2181
2251
|
flex-shrink: 0;
|
|
2182
2252
|
}
|
|
2183
2253
|
|
|
2184
|
-
/*
|
|
2185
|
-
.fb-tile {
|
|
2186
|
-
width:
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2254
|
+
/* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
|
|
2255
|
+
.fb-wide-tile {
|
|
2256
|
+
width: 100%;
|
|
2257
|
+
border-radius: 0.75rem;
|
|
2258
|
+
border: 1px dashed #60a5fa;
|
|
2259
|
+
background: rgba(239,246,255,0.5);
|
|
2260
|
+
display: flex;
|
|
2190
2261
|
overflow: hidden;
|
|
2191
|
-
|
|
2192
|
-
|
|
2262
|
+
height: 180px;
|
|
2263
|
+
transition: border-color 150ms, background 150ms, box-shadow 150ms;
|
|
2264
|
+
cursor: pointer;
|
|
2193
2265
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
.fb-tile-resource {
|
|
2197
|
-
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2266
|
+
.fb-wide-tile:hover {
|
|
2267
|
+
background: #eff6ff;
|
|
2198
2268
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2269
|
+
.fb-wide-tile.fb-drag-over {
|
|
2270
|
+
border-color: #3b82f6;
|
|
2271
|
+
border-width: 2px;
|
|
2272
|
+
background: #eff6ff;
|
|
2273
|
+
box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
|
|
2203
2274
|
}
|
|
2204
2275
|
|
|
2205
|
-
/*
|
|
2206
|
-
.fb-tile-
|
|
2207
|
-
|
|
2276
|
+
/* Upload zone inside wide tile */
|
|
2277
|
+
.fb-wide-tile-upload {
|
|
2278
|
+
flex: 1;
|
|
2208
2279
|
display: flex;
|
|
2280
|
+
flex-direction: column;
|
|
2209
2281
|
align-items: center;
|
|
2210
2282
|
justify-content: center;
|
|
2283
|
+
gap: 8px;
|
|
2284
|
+
color: #2563eb;
|
|
2285
|
+
padding: 16px;
|
|
2286
|
+
transition: background 150ms;
|
|
2211
2287
|
cursor: pointer;
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
border-color var(--fb-transition-duration, 200ms),
|
|
2216
|
-
color var(--fb-transition-duration, 200ms);
|
|
2288
|
+
background: transparent;
|
|
2289
|
+
border: none;
|
|
2290
|
+
font-family: inherit;
|
|
2217
2291
|
}
|
|
2218
|
-
.fb-tile-
|
|
2219
|
-
|
|
2220
|
-
color: var(--fb-text-color, #1f2937);
|
|
2292
|
+
.fb-wide-tile-upload:hover {
|
|
2293
|
+
background: rgba(191,219,254,0.25);
|
|
2221
2294
|
}
|
|
2222
2295
|
|
|
2223
|
-
/*
|
|
2224
|
-
.fb-tile-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
padding: 2px 6px;
|
|
2231
|
-
align-self: flex-end;
|
|
2232
|
-
margin-bottom: 4px;
|
|
2296
|
+
/* Vertical dashed divider between upload and library zones */
|
|
2297
|
+
.fb-wide-tile-divider {
|
|
2298
|
+
width: 1px;
|
|
2299
|
+
margin: 16px 0;
|
|
2300
|
+
border-left: 1px dashed rgba(96,165,250,0.5);
|
|
2301
|
+
background: transparent;
|
|
2302
|
+
flex-shrink: 0;
|
|
2233
2303
|
}
|
|
2234
2304
|
|
|
2235
|
-
/*
|
|
2236
|
-
.fb-
|
|
2237
|
-
width:
|
|
2238
|
-
|
|
2239
|
-
border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
|
|
2240
|
-
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2305
|
+
/* Library zone inside wide tile */
|
|
2306
|
+
.fb-wide-tile-library {
|
|
2307
|
+
width: 176px;
|
|
2308
|
+
flex-shrink: 0;
|
|
2241
2309
|
display: flex;
|
|
2242
2310
|
flex-direction: column;
|
|
2243
2311
|
align-items: center;
|
|
2244
2312
|
justify-content: center;
|
|
2245
|
-
gap:
|
|
2313
|
+
gap: 8px;
|
|
2314
|
+
color: #2563eb;
|
|
2315
|
+
padding: 12px;
|
|
2316
|
+
transition: background 150ms;
|
|
2246
2317
|
cursor: pointer;
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2318
|
+
background: transparent;
|
|
2319
|
+
border: none;
|
|
2320
|
+
font-family: inherit;
|
|
2250
2321
|
}
|
|
2251
|
-
.fb-
|
|
2252
|
-
|
|
2253
|
-
background: var(--fb-background-hover-color, #f9fafb);
|
|
2322
|
+
.fb-wide-tile-library:hover {
|
|
2323
|
+
background: rgba(191,219,254,0.25);
|
|
2254
2324
|
}
|
|
2255
2325
|
|
|
2256
|
-
/*
|
|
2257
|
-
.fb-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2326
|
+
/* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
|
|
2327
|
+
.fb-multi-outer {
|
|
2328
|
+
border-radius: 0.75rem;
|
|
2329
|
+
border: 1px dashed #cbd5e1;
|
|
2330
|
+
background: rgba(248,250,252,0.4);
|
|
2331
|
+
padding: 12px;
|
|
2332
|
+
transition: border-color 150ms, background 150ms, box-shadow 150ms;
|
|
2333
|
+
}
|
|
2334
|
+
.fb-multi-outer.fb-drag-over {
|
|
2335
|
+
border-width: 2px;
|
|
2336
|
+
border-color: #3b82f6;
|
|
2337
|
+
background: rgba(239,246,255,0.4);
|
|
2338
|
+
box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
|
|
2264
2339
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2340
|
+
|
|
2341
|
+
/* With files present: white solid border */
|
|
2342
|
+
.fb-multi-outer.fb-multi-has-files {
|
|
2343
|
+
border-style: solid;
|
|
2344
|
+
border-color: #e2e8f0;
|
|
2345
|
+
background: #fff;
|
|
2268
2346
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2347
|
+
|
|
2348
|
+
/* The CSS grid inside */
|
|
2349
|
+
.fb-multi-grid {
|
|
2350
|
+
display: grid;
|
|
2351
|
+
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
2352
|
+
gap: 10px;
|
|
2273
2353
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2354
|
+
|
|
2355
|
+
/* \u2500\u2500\u2500 Multi square add-tile (combined upload + library) \u2500\u2500\u2500 */
|
|
2356
|
+
.fb-multi-add-tile {
|
|
2357
|
+
aspect-ratio: 1 / 1;
|
|
2358
|
+
border-radius: 0.5rem;
|
|
2359
|
+
border: 1px dashed #60a5fa;
|
|
2360
|
+
background: rgba(239,246,255,0.5);
|
|
2361
|
+
display: flex;
|
|
2362
|
+
flex-direction: column;
|
|
2363
|
+
overflow: hidden;
|
|
2364
|
+
transition: background 150ms;
|
|
2278
2365
|
}
|
|
2279
|
-
.fb-
|
|
2280
|
-
|
|
2281
|
-
color: var(--fb-text-secondary-color, #6b7280);
|
|
2366
|
+
.fb-multi-add-tile:hover {
|
|
2367
|
+
background: #eff6ff;
|
|
2282
2368
|
}
|
|
2283
|
-
.fb-
|
|
2284
|
-
|
|
2285
|
-
color:
|
|
2369
|
+
.fb-multi-add-tile.fb-drag-over-tile {
|
|
2370
|
+
border-width: 2px;
|
|
2371
|
+
border-color: #3b82f6;
|
|
2372
|
+
background: rgba(255,255,255,0.8);
|
|
2286
2373
|
}
|
|
2287
2374
|
|
|
2288
|
-
/*
|
|
2289
|
-
.fb-
|
|
2290
|
-
|
|
2291
|
-
inset: 0;
|
|
2292
|
-
background: transparent;
|
|
2293
|
-
transition: background var(--fb-transition-duration, 200ms);
|
|
2294
|
-
display: flex;
|
|
2295
|
-
align-items: flex-start;
|
|
2296
|
-
justify-content: flex-end;
|
|
2297
|
-
}
|
|
2298
|
-
.fb-tile-resource:hover .fb-tile-overlay {
|
|
2299
|
-
background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
|
|
2300
|
-
}
|
|
2301
|
-
.fb-tile-x-btn {
|
|
2302
|
-
margin: 3px;
|
|
2303
|
-
width: 18px;
|
|
2304
|
-
height: 18px;
|
|
2305
|
-
background: var(--fb-error-color, #ef4444);
|
|
2306
|
-
color: var(--fb-file-bg-color, #fff);
|
|
2307
|
-
border: none;
|
|
2308
|
-
border-radius: 50%;
|
|
2309
|
-
font-size: 11px;
|
|
2310
|
-
line-height: 1;
|
|
2311
|
-
cursor: pointer;
|
|
2375
|
+
/* Upload half of add-tile */
|
|
2376
|
+
.fb-multi-add-upload {
|
|
2377
|
+
flex: 1;
|
|
2312
2378
|
display: flex;
|
|
2379
|
+
flex-direction: column;
|
|
2313
2380
|
align-items: center;
|
|
2314
2381
|
justify-content: center;
|
|
2315
|
-
|
|
2316
|
-
|
|
2382
|
+
gap: 4px;
|
|
2383
|
+
color: #2563eb;
|
|
2384
|
+
cursor: pointer;
|
|
2385
|
+
background: transparent;
|
|
2386
|
+
border: none;
|
|
2387
|
+
font-family: inherit;
|
|
2388
|
+
width: 100%;
|
|
2389
|
+
transition: background 150ms;
|
|
2317
2390
|
}
|
|
2318
|
-
.fb-
|
|
2319
|
-
|
|
2391
|
+
.fb-multi-add-upload:hover {
|
|
2392
|
+
background: rgba(191,219,254,0.35);
|
|
2320
2393
|
}
|
|
2321
2394
|
|
|
2322
|
-
/*
|
|
2323
|
-
.fb-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
align-items: center;
|
|
2328
|
-
justify-content: center;
|
|
2329
|
-
background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
|
|
2395
|
+
/* Horizontal dashed divider inside add-tile */
|
|
2396
|
+
.fb-multi-add-divider {
|
|
2397
|
+
border-top: 1px dashed rgba(96,165,250,0.5);
|
|
2398
|
+
margin: 0;
|
|
2399
|
+
flex-shrink: 0;
|
|
2330
2400
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2401
|
+
|
|
2402
|
+
/* Library strip at bottom of add-tile */
|
|
2403
|
+
.fb-multi-add-library {
|
|
2404
|
+
padding: 6px 0;
|
|
2334
2405
|
display: flex;
|
|
2335
2406
|
align-items: center;
|
|
2336
2407
|
justify-content: center;
|
|
2408
|
+
gap: 4px;
|
|
2409
|
+
color: #2563eb;
|
|
2410
|
+
font-size: 11px;
|
|
2411
|
+
font-weight: 500;
|
|
2412
|
+
cursor: pointer;
|
|
2413
|
+
background: transparent;
|
|
2414
|
+
border: none;
|
|
2415
|
+
font-family: inherit;
|
|
2416
|
+
width: 100%;
|
|
2417
|
+
transition: background 150ms;
|
|
2418
|
+
flex-shrink: 0;
|
|
2419
|
+
}
|
|
2420
|
+
.fb-multi-add-library:hover {
|
|
2421
|
+
background: rgba(191,219,254,0.35);
|
|
2337
2422
|
}
|
|
2338
2423
|
|
|
2339
|
-
/*
|
|
2340
|
-
.fb-
|
|
2424
|
+
/* \u2500\u2500\u2500 Capacity placeholder squares \u2500\u2500\u2500 */
|
|
2425
|
+
.fb-multi-placeholder {
|
|
2426
|
+
aspect-ratio: 1 / 1;
|
|
2427
|
+
border-radius: 0.5rem;
|
|
2428
|
+
border: 1px solid #e2e8f0;
|
|
2429
|
+
}
|
|
2430
|
+
.fb-multi-placeholder.fb-drag-over {
|
|
2431
|
+
border-width: 2px;
|
|
2432
|
+
border-style: dashed;
|
|
2433
|
+
border-color: #93c5fd;
|
|
2434
|
+
background: rgba(219,234,254,0.6);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/* \u2500\u2500\u2500 Filled preview tile \u2500\u2500\u2500 */
|
|
2438
|
+
.fb-preview-tile {
|
|
2439
|
+
aspect-ratio: 1 / 1;
|
|
2440
|
+
border-radius: 0.5rem;
|
|
2441
|
+
border: 1px solid #e2e8f0;
|
|
2442
|
+
overflow: hidden;
|
|
2341
2443
|
position: relative;
|
|
2444
|
+
cursor: pointer;
|
|
2445
|
+
}
|
|
2446
|
+
.fb-preview-tile img {
|
|
2342
2447
|
width: 100%;
|
|
2343
2448
|
height: 100%;
|
|
2449
|
+
object-fit: contain;
|
|
2450
|
+
display: block;
|
|
2344
2451
|
}
|
|
2345
2452
|
|
|
2346
|
-
/*
|
|
2347
|
-
.fb-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
z-index: 10;
|
|
2453
|
+
/* \u2500\u2500\u2500 Uploading placeholder tile \u2500\u2500\u2500 */
|
|
2454
|
+
.fb-uploading-tile {
|
|
2455
|
+
aspect-ratio: 1 / 1;
|
|
2456
|
+
border-radius: 0.5rem;
|
|
2457
|
+
border: 2px dashed #d1d5db;
|
|
2352
2458
|
display: flex;
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2459
|
+
flex-direction: column;
|
|
2460
|
+
align-items: center;
|
|
2461
|
+
justify-content: center;
|
|
2462
|
+
gap: 6px;
|
|
2463
|
+
padding: 6px;
|
|
2357
2464
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2465
|
+
|
|
2466
|
+
/* \u2500\u2500\u2500 Meta line below multi grid \u2500\u2500\u2500 */
|
|
2467
|
+
.fb-meta-line {
|
|
2468
|
+
margin-top: 10px;
|
|
2469
|
+
display: flex;
|
|
2470
|
+
align-items: center;
|
|
2471
|
+
justify-content: space-between;
|
|
2472
|
+
gap: 8px;
|
|
2473
|
+
flex-wrap: wrap;
|
|
2361
2474
|
}
|
|
2362
|
-
.fb-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
line-height: 1.2;
|
|
2475
|
+
.fb-meta-text {
|
|
2476
|
+
font-size: 12px;
|
|
2477
|
+
color: #94a3b8;
|
|
2478
|
+
display: flex;
|
|
2479
|
+
align-items: center;
|
|
2480
|
+
gap: 8px;
|
|
2481
|
+
flex-wrap: wrap;
|
|
2370
2482
|
}
|
|
2371
|
-
.fb-
|
|
2372
|
-
|
|
2483
|
+
.fb-meta-dot {
|
|
2484
|
+
width: 4px;
|
|
2485
|
+
height: 4px;
|
|
2486
|
+
border-radius: 50%;
|
|
2487
|
+
background: #cbd5e1;
|
|
2488
|
+
flex-shrink: 0;
|
|
2373
2489
|
}
|
|
2374
|
-
.fb-
|
|
2375
|
-
|
|
2490
|
+
.fb-meta-mono {
|
|
2491
|
+
font-family: ui-monospace, 'JetBrains Mono', monospace;
|
|
2492
|
+
font-size: 11px;
|
|
2493
|
+
letter-spacing: -0.02em;
|
|
2376
2494
|
}
|
|
2377
|
-
.fb-
|
|
2378
|
-
|
|
2495
|
+
.fb-clear-all-btn {
|
|
2496
|
+
font-size: 12px;
|
|
2497
|
+
color: #94a3b8;
|
|
2498
|
+
background: none;
|
|
2499
|
+
border: none;
|
|
2500
|
+
cursor: pointer;
|
|
2501
|
+
padding: 0;
|
|
2502
|
+
font-family: inherit;
|
|
2503
|
+
transition: color 150ms;
|
|
2504
|
+
white-space: nowrap;
|
|
2505
|
+
flex-shrink: 0;
|
|
2379
2506
|
}
|
|
2380
|
-
.fb-
|
|
2381
|
-
|
|
2507
|
+
.fb-clear-all-btn:hover {
|
|
2508
|
+
color: #dc2626;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
/* \u2500\u2500\u2500 Empty text (readonly) \u2500\u2500\u2500 */
|
|
2512
|
+
.fb-tile-empty-text {
|
|
2513
|
+
font-size: 11px;
|
|
2514
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
2515
|
+
padding: 4px 0;
|
|
2382
2516
|
}
|
|
2383
2517
|
|
|
2384
|
-
/* Tile action
|
|
2518
|
+
/* \u2500\u2500\u2500 Tile action buttons (for zoom popup, compat) \u2500\u2500\u2500 */
|
|
2385
2519
|
.fb-tile-actions {
|
|
2386
2520
|
position: absolute;
|
|
2387
2521
|
top: 3px;
|
|
@@ -2393,37 +2527,35 @@ function ensureFileStyles() {
|
|
|
2393
2527
|
transition: opacity var(--fb-transition-duration, 200ms);
|
|
2394
2528
|
z-index: 10;
|
|
2395
2529
|
}
|
|
2396
|
-
.fb-tile
|
|
2530
|
+
.fb-preview-tile:hover .fb-tile-actions {
|
|
2397
2531
|
opacity: 1;
|
|
2398
2532
|
}
|
|
2399
2533
|
.fb-tile-action-btn {
|
|
2400
|
-
width:
|
|
2401
|
-
height:
|
|
2534
|
+
width: 24px;
|
|
2535
|
+
height: 24px;
|
|
2402
2536
|
display: flex;
|
|
2403
2537
|
align-items: center;
|
|
2404
2538
|
justify-content: center;
|
|
2405
|
-
border:
|
|
2406
|
-
border-radius:
|
|
2539
|
+
border: 1px solid rgba(15,23,42,0.08);
|
|
2540
|
+
border-radius: 0.375rem;
|
|
2407
2541
|
cursor: pointer;
|
|
2408
|
-
background: rgba(
|
|
2409
|
-
color: #
|
|
2542
|
+
background: rgba(255,255,255,0.92);
|
|
2543
|
+
color: #374151;
|
|
2410
2544
|
padding: 0;
|
|
2411
2545
|
flex-shrink: 0;
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2546
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
|
2547
|
+
transition: background var(--fb-transition-duration, 200ms),
|
|
2548
|
+
color var(--fb-transition-duration, 200ms);
|
|
2415
2549
|
}
|
|
2416
2550
|
.fb-tile-action-btn:hover {
|
|
2417
|
-
background:
|
|
2418
|
-
|
|
2419
|
-
.fb-tile-action-remove {
|
|
2420
|
-
background: rgba(220, 38, 38, 0.8);
|
|
2551
|
+
background: #ffffff;
|
|
2552
|
+
color: #0f172a;
|
|
2421
2553
|
}
|
|
2422
2554
|
.fb-tile-action-remove:hover {
|
|
2423
|
-
|
|
2555
|
+
color: #dc2626;
|
|
2424
2556
|
}
|
|
2425
2557
|
|
|
2426
|
-
/*
|
|
2558
|
+
/* Zoom popup action buttons always visible */
|
|
2427
2559
|
.fb-tile-zoom-preview .fb-tile-actions {
|
|
2428
2560
|
position: absolute;
|
|
2429
2561
|
top: 6px;
|
|
@@ -2432,116 +2564,145 @@ function ensureFileStyles() {
|
|
|
2432
2564
|
z-index: 10000;
|
|
2433
2565
|
}
|
|
2434
2566
|
|
|
2435
|
-
/*
|
|
2436
|
-
.fb-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2567
|
+
/* \u2500\u2500\u2500 Hover zoom preview popup \u2500\u2500\u2500 */
|
|
2568
|
+
.fb-tile-zoom-preview {
|
|
2569
|
+
position: fixed;
|
|
2570
|
+
z-index: 9999;
|
|
2571
|
+
background: var(--fb-background-color, #fff);
|
|
2572
|
+
border: 1px solid #e2e8f0;
|
|
2573
|
+
border-radius: 0.5rem;
|
|
2574
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
2575
|
+
padding: 4px;
|
|
2576
|
+
width: 350px;
|
|
2577
|
+
height: 350px;
|
|
2578
|
+
pointer-events: none;
|
|
2579
|
+
opacity: 0;
|
|
2580
|
+
transition: opacity 150ms ease;
|
|
2440
2581
|
}
|
|
2441
|
-
.fb-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2582
|
+
.fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
|
|
2583
|
+
opacity: 1;
|
|
2584
|
+
}
|
|
2585
|
+
.fb-tile-zoom-preview-img {
|
|
2586
|
+
width: 100%;
|
|
2587
|
+
height: 100%;
|
|
2588
|
+
object-fit: contain;
|
|
2589
|
+
display: block;
|
|
2590
|
+
border-radius: calc(0.5rem - 2px);
|
|
2445
2591
|
}
|
|
2446
2592
|
|
|
2447
|
-
/*
|
|
2448
|
-
.fb-
|
|
2449
|
-
height:
|
|
2450
|
-
border:
|
|
2451
|
-
border
|
|
2593
|
+
/* \u2500\u2500\u2500 Single-file uploading state \u2500\u2500\u2500 */
|
|
2594
|
+
.fb-single-uploading {
|
|
2595
|
+
height: 180px;
|
|
2596
|
+
border-radius: 0.75rem;
|
|
2597
|
+
border: 1px dashed #60a5fa;
|
|
2598
|
+
background: rgba(239,246,255,0.5);
|
|
2452
2599
|
display: flex;
|
|
2453
2600
|
flex-direction: column;
|
|
2454
2601
|
align-items: center;
|
|
2455
2602
|
justify-content: center;
|
|
2456
|
-
gap:
|
|
2457
|
-
cursor: pointer;
|
|
2458
|
-
background: none;
|
|
2459
|
-
padding: 0;
|
|
2460
|
-
transition:
|
|
2461
|
-
border-color var(--fb-transition-duration, 200ms),
|
|
2462
|
-
background var(--fb-transition-duration, 200ms);
|
|
2463
|
-
width: 100%;
|
|
2603
|
+
gap: 8px;
|
|
2464
2604
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2605
|
+
|
|
2606
|
+
/* \u2500\u2500\u2500 Video overlays \u2500\u2500\u2500 */
|
|
2607
|
+
.fb-video-overlay {
|
|
2608
|
+
position: absolute;
|
|
2609
|
+
inset: 0;
|
|
2610
|
+
display: flex;
|
|
2611
|
+
align-items: center;
|
|
2612
|
+
justify-content: center;
|
|
2613
|
+
background: rgba(0,0,0,0.25);
|
|
2470
2614
|
}
|
|
2471
|
-
.fb-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2615
|
+
.fb-play-btn {
|
|
2616
|
+
background: rgba(255,255,255,0.9);
|
|
2617
|
+
border-radius: 50%;
|
|
2618
|
+
display: flex;
|
|
2619
|
+
align-items: center;
|
|
2620
|
+
justify-content: center;
|
|
2475
2621
|
}
|
|
2476
|
-
.fb-
|
|
2477
|
-
|
|
2478
|
-
|
|
2622
|
+
.fb-video-preview-wrap {
|
|
2623
|
+
position: relative;
|
|
2624
|
+
width: 100%;
|
|
2625
|
+
height: 100%;
|
|
2479
2626
|
}
|
|
2480
|
-
.fb-
|
|
2627
|
+
.fb-video-btn-overlay {
|
|
2628
|
+
position: absolute;
|
|
2629
|
+
top: 8px;
|
|
2630
|
+
right: 8px;
|
|
2631
|
+
z-index: 10;
|
|
2632
|
+
display: flex;
|
|
2633
|
+
gap: 4px;
|
|
2634
|
+
opacity: 0;
|
|
2635
|
+
transition: opacity 150ms;
|
|
2636
|
+
pointer-events: none;
|
|
2637
|
+
}
|
|
2638
|
+
.fb-video-preview-wrap:hover .fb-video-btn-overlay {
|
|
2639
|
+
opacity: 1;
|
|
2640
|
+
pointer-events: auto;
|
|
2641
|
+
}
|
|
2642
|
+
.fb-video-btn {
|
|
2643
|
+
border: none;
|
|
2644
|
+
border-radius: 4px;
|
|
2481
2645
|
font-size: 11px;
|
|
2482
|
-
|
|
2646
|
+
padding: 4px 8px;
|
|
2647
|
+
cursor: pointer;
|
|
2648
|
+
color: #fff;
|
|
2649
|
+
line-height: 1.2;
|
|
2483
2650
|
}
|
|
2651
|
+
.fb-video-btn-delete { background: rgba(220,38,38,0.85); }
|
|
2652
|
+
.fb-video-btn-delete:hover { background: rgba(185,28,28,0.95); }
|
|
2653
|
+
.fb-video-btn-change { background: rgba(31,41,55,0.85); }
|
|
2654
|
+
.fb-video-btn-change:hover { background: rgba(17,24,39,0.95); }
|
|
2484
2655
|
|
|
2485
|
-
/*
|
|
2486
|
-
.fb-tile
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
justify-content: center;
|
|
2491
|
-
cursor: pointer;
|
|
2492
|
-
font-size: 24px;
|
|
2493
|
-
color: var(--fb-file-upload-text-color, #9ca3af);
|
|
2494
|
-
transition:
|
|
2495
|
-
border-color var(--fb-transition-duration, 200ms),
|
|
2496
|
-
color var(--fb-transition-duration, 200ms);
|
|
2497
|
-
background: none;
|
|
2498
|
-
padding: 0;
|
|
2499
|
-
width: var(--fb-tile-size, 160px);
|
|
2500
|
-
height: var(--fb-tile-size, 160px);
|
|
2501
|
-
flex-shrink: 0;
|
|
2502
|
-
position: relative;
|
|
2656
|
+
/* \u2500\u2500\u2500 Readonly readonly tile \u2500\u2500\u2500 */
|
|
2657
|
+
.fb-readonly-tile {
|
|
2658
|
+
aspect-ratio: 1 / 1;
|
|
2659
|
+
border-radius: 0.5rem;
|
|
2660
|
+
border: 1px solid #e2e8f0;
|
|
2503
2661
|
overflow: hidden;
|
|
2504
|
-
|
|
2662
|
+
position: relative;
|
|
2663
|
+
cursor: pointer;
|
|
2505
2664
|
}
|
|
2506
|
-
.fb-tile
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2665
|
+
.fb-readonly-tile img {
|
|
2666
|
+
width: 100%;
|
|
2667
|
+
height: 100%;
|
|
2668
|
+
object-fit: contain;
|
|
2669
|
+
display: block;
|
|
2511
2670
|
}
|
|
2512
|
-
|
|
2513
|
-
/* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
|
|
2514
|
-
.fb-tile-zoom-preview {
|
|
2515
|
-
position: fixed;
|
|
2516
|
-
z-index: 9999;
|
|
2517
|
-
background: var(--fb-background-color, #fff);
|
|
2518
|
-
border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
|
|
2519
|
-
border-radius: var(--fb-border-radius, 0.5rem);
|
|
2520
|
-
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
2521
|
-
padding: 4px;
|
|
2522
|
-
width: 350px;
|
|
2523
|
-
height: 350px;
|
|
2524
|
-
pointer-events: none;
|
|
2671
|
+
.fb-readonly-tile .fb-tile-actions {
|
|
2525
2672
|
opacity: 0;
|
|
2526
|
-
transition: opacity 150ms ease;
|
|
2527
2673
|
}
|
|
2528
|
-
.fb-tile
|
|
2674
|
+
.fb-readonly-tile:hover .fb-tile-actions {
|
|
2529
2675
|
opacity: 1;
|
|
2530
2676
|
}
|
|
2531
|
-
|
|
2677
|
+
|
|
2678
|
+
/* \u2500\u2500\u2500 Readonly single-file filled \u2500\u2500\u2500 */
|
|
2679
|
+
.fb-single-readonly-filled {
|
|
2680
|
+
position: relative;
|
|
2681
|
+
border-radius: 0.75rem;
|
|
2682
|
+
border: 1px solid #e2e8f0;
|
|
2683
|
+
overflow: hidden;
|
|
2684
|
+
height: 220px;
|
|
2685
|
+
display: block;
|
|
2686
|
+
cursor: pointer;
|
|
2687
|
+
}
|
|
2688
|
+
.fb-single-readonly-filled img {
|
|
2532
2689
|
width: 100%;
|
|
2533
2690
|
height: 100%;
|
|
2534
2691
|
object-fit: contain;
|
|
2535
2692
|
display: block;
|
|
2536
|
-
|
|
2537
|
-
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/* \u2500\u2500\u2500 Readonly multi grid \u2500\u2500\u2500 */
|
|
2696
|
+
.fb-multi-readonly-grid {
|
|
2697
|
+
display: grid;
|
|
2698
|
+
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
2699
|
+
gap: 10px;
|
|
2538
2700
|
}
|
|
2539
2701
|
`;
|
|
2540
2702
|
document.head.appendChild(style);
|
|
2541
2703
|
}
|
|
2542
2704
|
|
|
2543
2705
|
// src/components/file/dom.ts
|
|
2544
|
-
var TILE_SIZE = "160px";
|
|
2545
2706
|
function createFileTile() {
|
|
2546
2707
|
ensureFileStyles();
|
|
2547
2708
|
const tile = document.createElement("div");
|
|
@@ -2549,7 +2710,7 @@ function createFileTile() {
|
|
|
2549
2710
|
return tile;
|
|
2550
2711
|
}
|
|
2551
2712
|
function showFileError(container, message) {
|
|
2552
|
-
const existing = container.closest("
|
|
2713
|
+
const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
|
|
2553
2714
|
if (existing) existing.remove();
|
|
2554
2715
|
const errorEl = document.createElement("div");
|
|
2555
2716
|
errorEl.className = "file-error-message error-message";
|
|
@@ -2559,10 +2720,10 @@ function showFileError(container, message) {
|
|
|
2559
2720
|
margin-top: 0.25rem;
|
|
2560
2721
|
`;
|
|
2561
2722
|
errorEl.textContent = message;
|
|
2562
|
-
container.closest("
|
|
2723
|
+
container.closest("[data-files-wrapper]")?.appendChild(errorEl);
|
|
2563
2724
|
}
|
|
2564
2725
|
function clearFileError(container) {
|
|
2565
|
-
const existing = container.closest("
|
|
2726
|
+
const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
|
|
2566
2727
|
if (existing) existing.remove();
|
|
2567
2728
|
}
|
|
2568
2729
|
function addDeleteButton(container, state, onDelete) {
|
|
@@ -2580,13 +2741,6 @@ function addDeleteButton(container, state, onDelete) {
|
|
|
2580
2741
|
overlay.appendChild(deleteBtn);
|
|
2581
2742
|
container.appendChild(overlay);
|
|
2582
2743
|
}
|
|
2583
|
-
function findFilePicker(container) {
|
|
2584
|
-
let el = container.parentElement;
|
|
2585
|
-
while (el && !el.dataset.filesWrapper) {
|
|
2586
|
-
el = el.parentElement;
|
|
2587
|
-
}
|
|
2588
|
-
return el?.querySelector('input[type="file"]') ?? null;
|
|
2589
|
-
}
|
|
2590
2744
|
function createUploadingTile(fileName, state) {
|
|
2591
2745
|
ensureFileStyles();
|
|
2592
2746
|
const tile = createFileTile();
|
|
@@ -2602,10 +2756,13 @@ function createUploadingTile(fileName, state) {
|
|
|
2602
2756
|
return tile;
|
|
2603
2757
|
}
|
|
2604
2758
|
function ensureTilesWrap(list) {
|
|
2759
|
+
const existingGrid = list.querySelector(".fb-multi-grid");
|
|
2760
|
+
if (existingGrid) return existingGrid;
|
|
2605
2761
|
const existing = list.querySelector(".fb-tiles-wrap");
|
|
2606
2762
|
if (existing) return existing;
|
|
2607
|
-
|
|
2608
|
-
|
|
2763
|
+
list.querySelector(".fb-file-dropzone")?.remove();
|
|
2764
|
+
list.querySelector(".fb-wide-tile")?.remove();
|
|
2765
|
+
list.querySelector(".fb-multi-outer")?.remove();
|
|
2609
2766
|
const tilesWrap = document.createElement("div");
|
|
2610
2767
|
tilesWrap.className = "fb-tiles-wrap";
|
|
2611
2768
|
tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
|
|
@@ -2617,7 +2774,7 @@ function ensureTilesWrap(list) {
|
|
|
2617
2774
|
return tilesWrap;
|
|
2618
2775
|
}
|
|
2619
2776
|
function setEmptyFileContainer(fileContainer, state, hint) {
|
|
2620
|
-
const hintHtml =
|
|
2777
|
+
const hintHtml = "";
|
|
2621
2778
|
fileContainer.innerHTML = `
|
|
2622
2779
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2623
2780
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
@@ -2659,9 +2816,11 @@ function setupDragAndDrop(element, dropHandler) {
|
|
|
2659
2816
|
}
|
|
2660
2817
|
|
|
2661
2818
|
// src/components/file/preview.ts
|
|
2662
|
-
var ICON_DOWNLOAD = `<svg width="
|
|
2663
|
-
var ICON_OPEN = `<svg width="
|
|
2664
|
-
var ICON_REMOVE = `<svg width="
|
|
2819
|
+
var ICON_DOWNLOAD = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
|
2820
|
+
var ICON_OPEN = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
|
|
2821
|
+
var ICON_REMOVE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>`;
|
|
2822
|
+
var ICON_REPLACE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 18a4 4 0 000-8 6 6 0 00-11.5 2A4 4 0 006 20h11M12 12v7M12 12l-3 3M12 12l3 3"/></svg>`;
|
|
2823
|
+
var ICON_LIBRARY = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 5l3.5 1-3 14L13 19"/></svg>`;
|
|
2665
2824
|
function canDownload(state, meta) {
|
|
2666
2825
|
return Boolean(
|
|
2667
2826
|
state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
|
|
@@ -2673,7 +2832,16 @@ function canOpenInTab(state, meta) {
|
|
|
2673
2832
|
);
|
|
2674
2833
|
}
|
|
2675
2834
|
function createTileActions(options) {
|
|
2676
|
-
const {
|
|
2835
|
+
const {
|
|
2836
|
+
canRemove,
|
|
2837
|
+
removeHandler,
|
|
2838
|
+
state,
|
|
2839
|
+
resourceId,
|
|
2840
|
+
fileName,
|
|
2841
|
+
meta,
|
|
2842
|
+
replaceHandler,
|
|
2843
|
+
libraryHandler
|
|
2844
|
+
} = options;
|
|
2677
2845
|
const group = document.createElement("div");
|
|
2678
2846
|
group.className = "fb-tile-actions";
|
|
2679
2847
|
const makeBtn = (icon, label, cls) => {
|
|
@@ -2688,6 +2856,16 @@ function createTileActions(options) {
|
|
|
2688
2856
|
});
|
|
2689
2857
|
return btn;
|
|
2690
2858
|
};
|
|
2859
|
+
if (replaceHandler) {
|
|
2860
|
+
const replaceBtn = makeBtn(ICON_REPLACE, t("replaceFile", state), "fb-tile-action-replace");
|
|
2861
|
+
replaceBtn.addEventListener("click", () => replaceHandler());
|
|
2862
|
+
group.appendChild(replaceBtn);
|
|
2863
|
+
}
|
|
2864
|
+
if (libraryHandler) {
|
|
2865
|
+
const libBtn = makeBtn(ICON_LIBRARY, t("fromLibrary", state), "fb-tile-action-library");
|
|
2866
|
+
libBtn.addEventListener("click", () => libraryHandler());
|
|
2867
|
+
group.appendChild(libBtn);
|
|
2868
|
+
}
|
|
2691
2869
|
if (canDownload(state, meta)) {
|
|
2692
2870
|
const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
|
|
2693
2871
|
dlBtn.addEventListener("click", () => {
|
|
@@ -2873,8 +3051,7 @@ function attachClonedActionListeners(cloned, original) {
|
|
|
2873
3051
|
}
|
|
2874
3052
|
function renderLocalImagePreview(container, file, fileName, state) {
|
|
2875
3053
|
const img = document.createElement("img");
|
|
2876
|
-
img.
|
|
2877
|
-
img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
|
|
3054
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
2878
3055
|
img.alt = fileName || t("previewAlt", state);
|
|
2879
3056
|
const reader = new FileReader();
|
|
2880
3057
|
reader.onload = (e) => {
|
|
@@ -2896,7 +3073,7 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
|
|
|
2896
3073
|
const newContainer = setupDragDropless(container);
|
|
2897
3074
|
newContainer.innerHTML = `
|
|
2898
3075
|
<div class="fb-video-preview-wrap">
|
|
2899
|
-
<video
|
|
3076
|
+
<video style="width:100%;height:100%;object-fit:contain;" controls preload="auto" muted src="${videoUrl}">
|
|
2900
3077
|
${escapeHtml(t("videoNotSupported", state))}
|
|
2901
3078
|
</video>
|
|
2902
3079
|
<div class="fb-video-btn-overlay">
|
|
@@ -2940,11 +3117,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
|
|
|
2940
3117
|
container.onclick = deps.fileUploadHandler;
|
|
2941
3118
|
}
|
|
2942
3119
|
container.innerHTML = `
|
|
2943
|
-
<div
|
|
2944
|
-
<svg
|
|
3120
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
|
|
3121
|
+
<svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
|
|
2945
3122
|
<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"/>
|
|
2946
3123
|
</svg>
|
|
2947
|
-
<div
|
|
3124
|
+
<div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
|
|
2948
3125
|
</div>
|
|
2949
3126
|
`;
|
|
2950
3127
|
if (deps?.setupDrop) {
|
|
@@ -2961,11 +3138,11 @@ function renderDeleteButton(container, resourceId, state) {
|
|
|
2961
3138
|
hiddenInput.value = "";
|
|
2962
3139
|
}
|
|
2963
3140
|
container.innerHTML = `
|
|
2964
|
-
<div
|
|
2965
|
-
<svg
|
|
3141
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
|
|
3142
|
+
<svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
|
|
2966
3143
|
<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"/>
|
|
2967
3144
|
</svg>
|
|
2968
|
-
<div
|
|
3145
|
+
<div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
|
|
2969
3146
|
</div>
|
|
2970
3147
|
`;
|
|
2971
3148
|
});
|
|
@@ -2984,7 +3161,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
2984
3161
|
deps
|
|
2985
3162
|
);
|
|
2986
3163
|
} else {
|
|
2987
|
-
container.innerHTML = `<div
|
|
3164
|
+
container.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);"><div style="font-size:36px;margin-bottom:0.5rem;">\u{1F4C1}</div><div style="font-size:0.875rem;">${escapeHtml(fileName)}</div></div>`;
|
|
2988
3165
|
}
|
|
2989
3166
|
if (!isReadonly && !meta.type?.startsWith("video/")) {
|
|
2990
3167
|
renderDeleteButton(container, resourceId, state);
|
|
@@ -2992,7 +3169,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
|
|
|
2992
3169
|
}
|
|
2993
3170
|
function renderUploadedVideoPreview(container, thumbnailUrl, state) {
|
|
2994
3171
|
const video = document.createElement("video");
|
|
2995
|
-
video.
|
|
3172
|
+
video.style.cssText = "width:100%;height:100%;object-fit:contain;";
|
|
2996
3173
|
video.controls = true;
|
|
2997
3174
|
video.preload = "metadata";
|
|
2998
3175
|
video.muted = true;
|
|
@@ -3013,8 +3190,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
3013
3190
|
renderUploadedVideoPreview(container, thumbnailUrl, state);
|
|
3014
3191
|
} else {
|
|
3015
3192
|
const img = document.createElement("img");
|
|
3016
|
-
img.
|
|
3017
|
-
img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
|
|
3193
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
3018
3194
|
img.alt = fileName || t("previewAlt", state);
|
|
3019
3195
|
img.src = thumbnailUrl;
|
|
3020
3196
|
container.appendChild(img);
|
|
@@ -3025,11 +3201,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
|
|
|
3025
3201
|
} catch (error) {
|
|
3026
3202
|
console.error("Failed to get thumbnail:", error);
|
|
3027
3203
|
container.innerHTML = `
|
|
3028
|
-
<div
|
|
3029
|
-
<svg
|
|
3204
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
|
|
3205
|
+
<svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
|
|
3030
3206
|
<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"/>
|
|
3031
3207
|
</svg>
|
|
3032
|
-
<div
|
|
3208
|
+
<div style="font-size:0.875rem;text-align:center;">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
|
|
3033
3209
|
</div>
|
|
3034
3210
|
`;
|
|
3035
3211
|
}
|
|
@@ -3203,13 +3379,9 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
|
|
|
3203
3379
|
const img = document.createElement("img");
|
|
3204
3380
|
img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
|
|
3205
3381
|
img.alt = meta.name;
|
|
3206
|
-
|
|
3207
|
-
reader.onload = (e) => {
|
|
3208
|
-
img.src = e.target?.result || "";
|
|
3209
|
-
attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
|
|
3210
|
-
};
|
|
3211
|
-
reader.readAsDataURL(meta.file);
|
|
3382
|
+
img.src = getLocalFileUrl(meta.file);
|
|
3212
3383
|
tile.appendChild(img);
|
|
3384
|
+
attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
|
|
3213
3385
|
} else if (state.config.getThumbnail) {
|
|
3214
3386
|
try {
|
|
3215
3387
|
const url = await state.config.getThumbnail(rid);
|
|
@@ -3266,17 +3438,20 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
|
|
|
3266
3438
|
}
|
|
3267
3439
|
if (actionsEl) tile.appendChild(actionsEl);
|
|
3268
3440
|
} else {
|
|
3269
|
-
|
|
3270
|
-
const hasExtension = name.includes(".");
|
|
3271
|
-
const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
|
|
3272
|
-
tile.innerHTML = `
|
|
3273
|
-
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
|
|
3274
|
-
<div style="font-size:36px;">\u{1F4C1}</div>
|
|
3275
|
-
${captionHtml}
|
|
3276
|
-
</div>`;
|
|
3277
|
-
if (actionsEl) tile.appendChild(actionsEl);
|
|
3441
|
+
fillDocumentFallback(tile, rid, meta, actionsEl);
|
|
3278
3442
|
}
|
|
3279
3443
|
}
|
|
3444
|
+
function fillDocumentFallback(tile, rid, meta, actionsEl) {
|
|
3445
|
+
const fileName = meta?.name ?? rid.split("/").pop() ?? "";
|
|
3446
|
+
if (fileName) tile.title = fileName;
|
|
3447
|
+
const labelHtml = fileName ? `<div style="font-size:11px;line-height:1.2;text-align:center;color:var(--fb-text-secondary-color,#6b7280);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(fileName)}</div>` : "";
|
|
3448
|
+
tile.innerHTML = `
|
|
3449
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
|
|
3450
|
+
<div style="font-size:36px;">\u{1F4C1}</div>
|
|
3451
|
+
${labelHtml}
|
|
3452
|
+
</div>`;
|
|
3453
|
+
if (actionsEl) tile.appendChild(actionsEl);
|
|
3454
|
+
}
|
|
3280
3455
|
async function forceDownload(resourceId, fileName, state) {
|
|
3281
3456
|
try {
|
|
3282
3457
|
let fileUrl = null;
|
|
@@ -3376,6 +3551,10 @@ async function handleFileSelect(opts) {
|
|
|
3376
3551
|
return;
|
|
3377
3552
|
}
|
|
3378
3553
|
clearFileError(container);
|
|
3554
|
+
const existingHiddenInput = container.parentElement?.querySelector(
|
|
3555
|
+
'input[type="hidden"]'
|
|
3556
|
+
);
|
|
3557
|
+
const previousRid = existingHiddenInput?.value || null;
|
|
3379
3558
|
ensureFileStyles();
|
|
3380
3559
|
container.innerHTML = `
|
|
3381
3560
|
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
|
|
@@ -3386,7 +3565,13 @@ async function handleFileSelect(opts) {
|
|
|
3386
3565
|
try {
|
|
3387
3566
|
rid = await uploadSingleFile(file, state);
|
|
3388
3567
|
} catch (error) {
|
|
3389
|
-
|
|
3568
|
+
if (previousRid && deps?.onAfterUpload) {
|
|
3569
|
+
deps.onAfterUpload(container, previousRid);
|
|
3570
|
+
} else if (deps?.onRemove) {
|
|
3571
|
+
deps.onRemove();
|
|
3572
|
+
} else {
|
|
3573
|
+
setEmptyFileContainer(container, state);
|
|
3574
|
+
}
|
|
3390
3575
|
throw error;
|
|
3391
3576
|
}
|
|
3392
3577
|
state.resourceIndex.set(rid, {
|
|
@@ -3396,9 +3581,10 @@ async function handleFileSelect(opts) {
|
|
|
3396
3581
|
uploadedAt: /* @__PURE__ */ new Date(),
|
|
3397
3582
|
file
|
|
3398
3583
|
});
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3584
|
+
if (previousRid && previousRid !== rid) {
|
|
3585
|
+
releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
|
|
3586
|
+
}
|
|
3587
|
+
let hiddenInput = existingHiddenInput;
|
|
3402
3588
|
if (!hiddenInput) {
|
|
3403
3589
|
hiddenInput = document.createElement("input");
|
|
3404
3590
|
hiddenInput.type = "hidden";
|
|
@@ -3407,7 +3593,9 @@ async function handleFileSelect(opts) {
|
|
|
3407
3593
|
}
|
|
3408
3594
|
hiddenInput.value = rid;
|
|
3409
3595
|
const isVideo = file.type.startsWith("video/");
|
|
3410
|
-
if (!isVideo && deps) {
|
|
3596
|
+
if (!isVideo && deps?.onAfterUpload) {
|
|
3597
|
+
deps.onAfterUpload(container, rid);
|
|
3598
|
+
} else if (!isVideo && deps) {
|
|
3411
3599
|
renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
|
|
3412
3600
|
} else {
|
|
3413
3601
|
renderFilePreview(container, rid, state, {
|
|
@@ -3468,17 +3656,17 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3468
3656
|
return { accepted, errorMessage: errorParts.join(" \u2022 ") };
|
|
3469
3657
|
}
|
|
3470
3658
|
async function uploadBatch(accepted, resourceIds, listEl, state) {
|
|
3659
|
+
if (listEl) {
|
|
3660
|
+
const tilesWrap = ensureTilesWrap(listEl);
|
|
3661
|
+
const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
|
|
3662
|
+
if (addTile) addTile.style.display = "none";
|
|
3663
|
+
}
|
|
3471
3664
|
await Promise.all(
|
|
3472
3665
|
accepted.map(async (file) => {
|
|
3473
3666
|
const placeholder = createUploadingTile(file.name, state);
|
|
3474
3667
|
if (listEl) {
|
|
3475
3668
|
const tilesWrap = ensureTilesWrap(listEl);
|
|
3476
|
-
|
|
3477
|
-
if (addTile) {
|
|
3478
|
-
tilesWrap.insertBefore(placeholder, addTile);
|
|
3479
|
-
} else {
|
|
3480
|
-
tilesWrap.appendChild(placeholder);
|
|
3481
|
-
}
|
|
3669
|
+
tilesWrap.appendChild(placeholder);
|
|
3482
3670
|
}
|
|
3483
3671
|
try {
|
|
3484
3672
|
const rid = await uploadSingleFile(file, state);
|
|
@@ -3520,7 +3708,7 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
|
|
|
3520
3708
|
function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
|
|
3521
3709
|
filesPicker.onchange = async () => {
|
|
3522
3710
|
if (!filesPicker.files) return;
|
|
3523
|
-
const wrapperEl = filesPicker.closest("
|
|
3711
|
+
const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
|
|
3524
3712
|
const { accepted, errorMessage } = filterAndSlice(
|
|
3525
3713
|
Array.from(filesPicker.files),
|
|
3526
3714
|
resourceIds.length,
|
|
@@ -3685,6 +3873,10 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
3685
3873
|
hiddenInput.name = pathKey;
|
|
3686
3874
|
fileWrapper.appendChild(hiddenInput);
|
|
3687
3875
|
}
|
|
3876
|
+
const previousRid = hiddenInput.value || null;
|
|
3877
|
+
if (previousRid && previousRid !== first.resourceId) {
|
|
3878
|
+
releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
|
|
3879
|
+
}
|
|
3688
3880
|
hiddenInput.value = first.resourceId;
|
|
3689
3881
|
await renderCallback(first.resourceId);
|
|
3690
3882
|
if (!state.config.readonly) {
|
|
@@ -3693,7 +3885,9 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
3693
3885
|
}
|
|
3694
3886
|
|
|
3695
3887
|
// src/components/file/render-edit.ts
|
|
3696
|
-
|
|
3888
|
+
var ICON_CLOUD = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 18a4 4 0 000-8 6 6 0 00-11.5 2A4 4 0 006 20h11M12 12v7M12 12l-3 3M12 12l3 3"/></svg>`;
|
|
3889
|
+
var ICON_LIBRARY2 = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 5l3.5 1-3 14L13 19"/></svg>`;
|
|
3890
|
+
function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps, extras) {
|
|
3697
3891
|
seedInferredResource(initial, state.resourceIndex);
|
|
3698
3892
|
const meta = state.resourceIndex.get(initial);
|
|
3699
3893
|
const isVideo = meta?.type?.startsWith("video/");
|
|
@@ -3704,7 +3898,7 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
|
|
|
3704
3898
|
deps
|
|
3705
3899
|
}).catch(console.error);
|
|
3706
3900
|
} else {
|
|
3707
|
-
|
|
3901
|
+
renderSingleFileFilled(fileContainer, initial, state, deps, extras);
|
|
3708
3902
|
}
|
|
3709
3903
|
const hiddenInput = document.createElement("input");
|
|
3710
3904
|
hiddenInput.type = "hidden";
|
|
@@ -3712,161 +3906,423 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
|
|
|
3712
3906
|
hiddenInput.value = initial;
|
|
3713
3907
|
fileWrapper.appendChild(hiddenInput);
|
|
3714
3908
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3909
|
+
function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false, constraintHint = "") {
|
|
3910
|
+
ensureFileStyles();
|
|
3911
|
+
const outer = document.createElement("div");
|
|
3912
|
+
outer.className = `fb-wide-tile${hasLibrary ? " fb-file-card-row" : ""}${isDragOver ? " fb-drag-over" : ""}`;
|
|
3913
|
+
const uploadBtn = document.createElement("button");
|
|
3914
|
+
uploadBtn.type = "button";
|
|
3915
|
+
uploadBtn.className = "fb-wide-tile-upload fb-file-dropzone";
|
|
3916
|
+
const cloudIcon = document.createElement("span");
|
|
3917
|
+
cloudIcon.style.cssText = "width:36px;height:36px;display:block;flex-shrink:0;";
|
|
3918
|
+
cloudIcon.innerHTML = ICON_CLOUD;
|
|
3919
|
+
uploadBtn.appendChild(cloudIcon);
|
|
3920
|
+
const primaryText = document.createElement("div");
|
|
3921
|
+
primaryText.className = "fb-wide-tile-label";
|
|
3922
|
+
primaryText.style.cssText = "font-size:14px;font-weight:600;";
|
|
3923
|
+
primaryText.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragText", state);
|
|
3924
|
+
uploadBtn.appendChild(primaryText);
|
|
3925
|
+
if (constraintHint) {
|
|
3926
|
+
const hintEl = document.createElement("div");
|
|
3927
|
+
hintEl.style.cssText = "font-size:11px;opacity:0.65;margin-top:2px;";
|
|
3928
|
+
hintEl.textContent = constraintHint;
|
|
3929
|
+
uploadBtn.appendChild(hintEl);
|
|
3930
|
+
}
|
|
3931
|
+
uploadBtn.onclick = (e) => {
|
|
3932
|
+
e.stopPropagation();
|
|
3933
|
+
onUploadClick();
|
|
3934
|
+
};
|
|
3935
|
+
outer.appendChild(uploadBtn);
|
|
3936
|
+
if (hasLibrary && onLibraryClick) {
|
|
3937
|
+
const divider = document.createElement("div");
|
|
3938
|
+
divider.className = "fb-wide-tile-divider";
|
|
3939
|
+
outer.appendChild(divider);
|
|
3940
|
+
const libBtn = document.createElement("button");
|
|
3941
|
+
libBtn.type = "button";
|
|
3942
|
+
libBtn.className = "fb-wide-tile-library fb-file-library-card";
|
|
3943
|
+
const libIcon = document.createElement("span");
|
|
3944
|
+
libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
|
|
3945
|
+
libIcon.innerHTML = ICON_LIBRARY2;
|
|
3946
|
+
libBtn.appendChild(libIcon);
|
|
3947
|
+
const libLabel = document.createElement("div");
|
|
3948
|
+
libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
|
|
3949
|
+
libLabel.textContent = t("fromLibrary", state);
|
|
3950
|
+
libBtn.appendChild(libLabel);
|
|
3951
|
+
const libHint = document.createElement("div");
|
|
3952
|
+
libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
|
|
3953
|
+
libHint.textContent = t("libraryHint", state);
|
|
3954
|
+
libBtn.appendChild(libHint);
|
|
3955
|
+
libBtn.onclick = (e) => {
|
|
3956
|
+
e.stopPropagation();
|
|
3957
|
+
onLibraryClick();
|
|
3958
|
+
};
|
|
3959
|
+
outer.appendChild(libBtn);
|
|
3960
|
+
}
|
|
3961
|
+
attachDragOverFeedback(outer, {
|
|
3962
|
+
onEnter: () => {
|
|
3963
|
+
const primaryText2 = outer.querySelector(".fb-wide-tile-label");
|
|
3964
|
+
if (primaryText2) primaryText2.textContent = t("dropToUpload", state);
|
|
3965
|
+
},
|
|
3966
|
+
onLeave: () => {
|
|
3967
|
+
const primaryText2 = outer.querySelector(".fb-wide-tile-label");
|
|
3968
|
+
if (primaryText2) primaryText2.textContent = t("clickDragText", state);
|
|
3969
|
+
},
|
|
3970
|
+
activeClass: "fb-drag-over"
|
|
3971
|
+
});
|
|
3972
|
+
return outer;
|
|
3728
3973
|
}
|
|
3729
|
-
function
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3974
|
+
function attachDragOverFeedback(el, hooks) {
|
|
3975
|
+
let depth = 0;
|
|
3976
|
+
el.addEventListener("dragover", (e) => {
|
|
3977
|
+
e.preventDefault();
|
|
3978
|
+
});
|
|
3979
|
+
el.addEventListener("dragenter", (e) => {
|
|
3980
|
+
e.preventDefault();
|
|
3981
|
+
depth++;
|
|
3982
|
+
if (depth === 1) {
|
|
3983
|
+
el.classList.add(hooks.activeClass);
|
|
3984
|
+
hooks.onEnter();
|
|
3985
|
+
}
|
|
3986
|
+
});
|
|
3987
|
+
el.addEventListener("dragleave", (e) => {
|
|
3988
|
+
e.preventDefault();
|
|
3989
|
+
depth = Math.max(0, depth - 1);
|
|
3990
|
+
if (depth === 0) {
|
|
3991
|
+
el.classList.remove(hooks.activeClass);
|
|
3992
|
+
hooks.onLeave();
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
el.addEventListener("drop", () => {
|
|
3996
|
+
depth = 0;
|
|
3997
|
+
el.classList.remove(hooks.activeClass);
|
|
3998
|
+
hooks.onLeave();
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras) {
|
|
4002
|
+
const meta = state.resourceIndex.get(resourceId);
|
|
4003
|
+
const isVideo = meta?.type?.startsWith("video/");
|
|
4004
|
+
if (isVideo) {
|
|
4005
|
+
renderFilePreview(fileContainer, resourceId, state, {
|
|
4006
|
+
fileName: meta?.name ?? "",
|
|
4007
|
+
isReadonly: false,
|
|
4008
|
+
deps
|
|
4009
|
+
}).catch(console.error);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
ensureFileStyles();
|
|
4013
|
+
const outer = document.createElement("div");
|
|
4014
|
+
outer.className = "fb-multi-outer fb-multi-has-files";
|
|
4015
|
+
const grid = document.createElement("div");
|
|
4016
|
+
grid.className = "fb-multi-grid fb-tiles-wrap";
|
|
4017
|
+
outer.appendChild(grid);
|
|
4018
|
+
const tile = buildPreviewTile(
|
|
4019
|
+
resourceId,
|
|
4020
|
+
state,
|
|
4021
|
+
Boolean(deps.onRemove),
|
|
4022
|
+
deps.onRemove ? () => deps.onRemove?.() : null,
|
|
4023
|
+
extras
|
|
4024
|
+
);
|
|
4025
|
+
grid.appendChild(tile);
|
|
4026
|
+
fileContainer.className = "file-preview-container";
|
|
4027
|
+
fileContainer.removeAttribute("style");
|
|
4028
|
+
while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
|
|
4029
|
+
fileContainer.appendChild(outer);
|
|
4030
|
+
}
|
|
4031
|
+
function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
|
|
4032
|
+
const tile = document.createElement("div");
|
|
4033
|
+
tile.className = `fb-multi-add-tile fb-multi-add-tile-js${isDragOver ? " fb-drag-over-tile" : ""}`;
|
|
4034
|
+
const uploadBtn = document.createElement("button");
|
|
4035
|
+
uploadBtn.type = "button";
|
|
4036
|
+
uploadBtn.className = "fb-multi-add-upload fb-tile-add fb-file-dropzone";
|
|
4037
|
+
const cloudIcon = document.createElement("span");
|
|
4038
|
+
cloudIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
|
|
4039
|
+
cloudIcon.innerHTML = ICON_CLOUD;
|
|
4040
|
+
uploadBtn.appendChild(cloudIcon);
|
|
4041
|
+
const uploadLabel = document.createElement("span");
|
|
4042
|
+
uploadLabel.className = "fb-multi-add-label";
|
|
4043
|
+
uploadLabel.style.cssText = "font-size:11px;font-weight:600;";
|
|
4044
|
+
uploadLabel.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragTextMultiple", state);
|
|
4045
|
+
uploadBtn.appendChild(uploadLabel);
|
|
4046
|
+
uploadBtn.onclick = (e) => {
|
|
4047
|
+
e.stopPropagation();
|
|
4048
|
+
onUploadClick();
|
|
4049
|
+
};
|
|
4050
|
+
tile.appendChild(uploadBtn);
|
|
4051
|
+
if (hasLibrary && onLibraryClick) {
|
|
4052
|
+
const divider = document.createElement("div");
|
|
4053
|
+
divider.className = "fb-multi-add-divider";
|
|
4054
|
+
tile.appendChild(divider);
|
|
4055
|
+
const libBtn = document.createElement("button");
|
|
4056
|
+
libBtn.type = "button";
|
|
4057
|
+
libBtn.className = "fb-multi-add-library fb-tile-add-library fb-file-library-card";
|
|
4058
|
+
libBtn.setAttribute("aria-label", t("fromLibrary", state));
|
|
4059
|
+
const libIcon = document.createElement("span");
|
|
4060
|
+
libIcon.style.cssText = "width:14px;height:14px;display:block;flex-shrink:0;";
|
|
4061
|
+
libIcon.innerHTML = ICON_LIBRARY2;
|
|
4062
|
+
libBtn.appendChild(libIcon);
|
|
4063
|
+
libBtn.appendChild(document.createTextNode(t("fromLibrary", state)));
|
|
4064
|
+
libBtn.onclick = (e) => {
|
|
4065
|
+
e.stopPropagation();
|
|
4066
|
+
onLibraryClick();
|
|
4067
|
+
};
|
|
4068
|
+
tile.appendChild(libBtn);
|
|
4069
|
+
}
|
|
4070
|
+
return tile;
|
|
4071
|
+
}
|
|
4072
|
+
function buildPreviewTile(rid, state, canRemove, onRemove, extras) {
|
|
4073
|
+
ensureFileStyles();
|
|
4074
|
+
const meta = state.resourceIndex.get(rid);
|
|
4075
|
+
const tile = document.createElement("div");
|
|
4076
|
+
tile.className = "fb-preview-tile fb-checker fb-tile-resource resource-pill";
|
|
4077
|
+
tile.dataset.resourceId = rid;
|
|
4078
|
+
const actionsEl = createTileActions({
|
|
4079
|
+
canRemove: canRemove && onRemove !== null,
|
|
4080
|
+
removeHandler: onRemove,
|
|
4081
|
+
state,
|
|
4082
|
+
resourceId: rid,
|
|
4083
|
+
fileName: meta?.name ?? "",
|
|
4084
|
+
meta,
|
|
4085
|
+
replaceHandler: extras?.replaceHandler ?? null,
|
|
4086
|
+
libraryHandler: extras?.libraryHandler ?? null
|
|
4087
|
+
});
|
|
4088
|
+
fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
|
|
4089
|
+
console.error("Failed to render tile:", err);
|
|
4090
|
+
});
|
|
4091
|
+
return tile;
|
|
4092
|
+
}
|
|
4093
|
+
function buildPlaceholderTile(isDragOver = false) {
|
|
4094
|
+
const div = document.createElement("div");
|
|
4095
|
+
div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
|
|
4096
|
+
return div;
|
|
4097
|
+
}
|
|
4098
|
+
function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearAll) {
|
|
4099
|
+
const line = document.createElement("div");
|
|
4100
|
+
line.className = "fb-meta-line";
|
|
4101
|
+
const metaText = document.createElement("div");
|
|
4102
|
+
metaText.className = "fb-meta-text";
|
|
4103
|
+
if (element.maxSize && element.maxSize !== Infinity) {
|
|
4104
|
+
const sizeSpan = document.createElement("span");
|
|
4105
|
+
sizeSpan.textContent = t("hintMaxSize", state, { size: element.maxSize });
|
|
4106
|
+
metaText.appendChild(sizeSpan);
|
|
4107
|
+
metaText.appendChild(buildMetaDot());
|
|
4108
|
+
}
|
|
4109
|
+
const exts = getAllowedExtensions(element.accept);
|
|
4110
|
+
if (exts.length > 0) {
|
|
4111
|
+
const fmtSpan = document.createElement("span");
|
|
4112
|
+
fmtSpan.className = "fb-meta-mono";
|
|
4113
|
+
fmtSpan.textContent = exts.map((e) => e.toUpperCase()).join(", ");
|
|
4114
|
+
metaText.appendChild(fmtSpan);
|
|
4115
|
+
metaText.appendChild(buildMetaDot());
|
|
4116
|
+
}
|
|
4117
|
+
const countSpan = document.createElement("span");
|
|
4118
|
+
if (maxCount < Infinity) {
|
|
4119
|
+
countSpan.textContent = t("fileCountWithMax", state, {
|
|
4120
|
+
count: ridCount,
|
|
4121
|
+
max: maxCount
|
|
4122
|
+
});
|
|
3739
4123
|
} else {
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
4124
|
+
const countKey = ridCount === 1 ? "fileCountSingle" : "fileCountPlural";
|
|
4125
|
+
countSpan.textContent = t(countKey, state, { count: ridCount });
|
|
4126
|
+
}
|
|
4127
|
+
metaText.appendChild(countSpan);
|
|
4128
|
+
line.appendChild(metaText);
|
|
4129
|
+
if (canClearAll && ridCount > 1) {
|
|
4130
|
+
const clearBtn = document.createElement("button");
|
|
4131
|
+
clearBtn.type = "button";
|
|
4132
|
+
clearBtn.className = "fb-clear-all-btn";
|
|
4133
|
+
clearBtn.textContent = t("clearAll", state);
|
|
4134
|
+
clearBtn.onclick = (e) => {
|
|
4135
|
+
e.stopPropagation();
|
|
4136
|
+
if (window.confirm(t("clearAll", state) + "?")) {
|
|
4137
|
+
onClearAll();
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
line.appendChild(clearBtn);
|
|
3743
4141
|
}
|
|
3744
|
-
|
|
3745
|
-
|
|
4142
|
+
return line;
|
|
4143
|
+
}
|
|
4144
|
+
function buildMetaDot() {
|
|
4145
|
+
const dot = document.createElement("span");
|
|
4146
|
+
dot.className = "fb-meta-dot";
|
|
4147
|
+
return dot;
|
|
3746
4148
|
}
|
|
4149
|
+
var gridResizeObservers = /* @__PURE__ */ new WeakMap();
|
|
3747
4150
|
function renderResourcePills(opts) {
|
|
3748
4151
|
const {
|
|
3749
4152
|
container,
|
|
3750
4153
|
rids,
|
|
3751
4154
|
state,
|
|
3752
4155
|
onRemove,
|
|
3753
|
-
hint,
|
|
3754
|
-
countInfo,
|
|
3755
4156
|
maxCount,
|
|
3756
4157
|
isReadonly = false,
|
|
3757
|
-
onLibraryPick
|
|
4158
|
+
onLibraryPick,
|
|
4159
|
+
element,
|
|
4160
|
+
onClearAll,
|
|
4161
|
+
openPicker: openPickerProp
|
|
3758
4162
|
} = opts;
|
|
3759
4163
|
ensureFileStyles();
|
|
3760
4164
|
const wrapper = container.closest("[data-files-wrapper]");
|
|
3761
4165
|
if (wrapper) {
|
|
3762
4166
|
wrapper.dataset.resourceIds = JSON.stringify(rids ?? []);
|
|
3763
4167
|
}
|
|
4168
|
+
const previousObserver = gridResizeObservers.get(container);
|
|
4169
|
+
if (previousObserver) {
|
|
4170
|
+
previousObserver.disconnect();
|
|
4171
|
+
gridResizeObservers.delete(container);
|
|
4172
|
+
}
|
|
3764
4173
|
while (container.firstChild) container.removeChild(container.firstChild);
|
|
3765
4174
|
const ridList = rids ?? [];
|
|
3766
|
-
const
|
|
4175
|
+
const effectiveMax = maxCount ?? Infinity;
|
|
4176
|
+
const atMax = effectiveMax !== Infinity && ridList.length >= effectiveMax;
|
|
3767
4177
|
const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
|
|
3768
|
-
const
|
|
3769
|
-
const
|
|
3770
|
-
if (
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
const openPicker = () => {
|
|
3775
|
-
const picker = findFilePicker(container);
|
|
3776
|
-
if (picker) picker.click();
|
|
3777
|
-
};
|
|
3778
|
-
if (ridList.length === 0) {
|
|
3779
|
-
if (isReadonly) {
|
|
4178
|
+
const openPicker = openPickerProp ?? (() => {
|
|
4179
|
+
const pickerEl = container.closest("[data-files-wrapper]")?.querySelector('input[type="file"]');
|
|
4180
|
+
if (pickerEl) pickerEl.click();
|
|
4181
|
+
});
|
|
4182
|
+
if (isReadonly) {
|
|
4183
|
+
if (ridList.length === 0) {
|
|
3780
4184
|
const emptyEl = document.createElement("div");
|
|
3781
4185
|
emptyEl.className = "fb-tile-empty-text";
|
|
3782
4186
|
emptyEl.textContent = t("noFilesSelected", state);
|
|
3783
4187
|
container.appendChild(emptyEl);
|
|
3784
|
-
} else if (hasLibrary) {
|
|
3785
|
-
const row = document.createElement("div");
|
|
3786
|
-
row.className = "fb-file-card-row";
|
|
3787
|
-
const dropzone = buildEmptyDropzone(
|
|
3788
|
-
state,
|
|
3789
|
-
t("clickDragTextMultiple", state),
|
|
3790
|
-
buildSubHint(),
|
|
3791
|
-
openPicker
|
|
3792
|
-
);
|
|
3793
|
-
const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
|
|
3794
|
-
row.appendChild(dropzone);
|
|
3795
|
-
row.appendChild(libraryBtn);
|
|
3796
|
-
container.appendChild(row);
|
|
3797
4188
|
} else {
|
|
3798
|
-
const
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4189
|
+
const grid2 = document.createElement("div");
|
|
4190
|
+
grid2.className = "fb-multi-readonly-grid";
|
|
4191
|
+
container.appendChild(grid2);
|
|
4192
|
+
for (const rid of ridList) {
|
|
4193
|
+
const meta = state.resourceIndex.get(rid);
|
|
4194
|
+
const tile = document.createElement("div");
|
|
4195
|
+
tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
|
|
4196
|
+
tile.dataset.resourceId = rid;
|
|
4197
|
+
const actionsEl = createTileActions({
|
|
4198
|
+
canRemove: false,
|
|
4199
|
+
removeHandler: null,
|
|
4200
|
+
state,
|
|
4201
|
+
resourceId: rid,
|
|
4202
|
+
fileName: meta?.name ?? "",
|
|
4203
|
+
meta
|
|
4204
|
+
});
|
|
4205
|
+
fillTileContent(tile, rid, meta, state, actionsEl).catch(console.error);
|
|
4206
|
+
tile.onclick = async () => {
|
|
4207
|
+
let url = null;
|
|
4208
|
+
if (state.config.getDownloadUrl) {
|
|
4209
|
+
url = state.config.getDownloadUrl(rid);
|
|
4210
|
+
} else if (state.config.getThumbnail) {
|
|
4211
|
+
url = await state.config.getThumbnail(rid);
|
|
4212
|
+
} else if (meta?.file instanceof File) {
|
|
4213
|
+
url = URL.createObjectURL(meta.file);
|
|
4214
|
+
}
|
|
4215
|
+
if (url) {
|
|
4216
|
+
window.open(url, "_blank");
|
|
4217
|
+
} else if (state.config.downloadFile) {
|
|
4218
|
+
state.config.downloadFile(rid, meta?.name ?? "");
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
grid2.appendChild(tile);
|
|
4222
|
+
}
|
|
3805
4223
|
}
|
|
3806
4224
|
return;
|
|
3807
4225
|
}
|
|
3808
|
-
const
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
const
|
|
3817
|
-
|
|
3818
|
-
removeHandler: onRemove ? () => onRemove(rid) : null,
|
|
4226
|
+
const outerDiv = document.createElement("div");
|
|
4227
|
+
outerDiv.className = `fb-multi-outer${ridList.length > 0 ? " fb-multi-has-files" : ""}`;
|
|
4228
|
+
const grid = document.createElement("div");
|
|
4229
|
+
grid.className = "fb-multi-grid fb-tiles-wrap";
|
|
4230
|
+
outerDiv.appendChild(grid);
|
|
4231
|
+
container.appendChild(outerDiv);
|
|
4232
|
+
for (let i = 0; i < ridList.length; i++) {
|
|
4233
|
+
const rid = ridList[i];
|
|
4234
|
+
const tile = buildPreviewTile(
|
|
4235
|
+
rid,
|
|
3819
4236
|
state,
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
console.error("Failed to render tile:", err);
|
|
3825
|
-
});
|
|
3826
|
-
tilesWrap.appendChild(tile);
|
|
3827
|
-
}
|
|
3828
|
-
if (!isReadonly && !atMax) {
|
|
3829
|
-
const addTile = document.createElement("div");
|
|
3830
|
-
addTile.className = "fb-tile fb-tile-add";
|
|
3831
|
-
addTile.innerHTML = "+";
|
|
3832
|
-
addTile.onclick = openPicker;
|
|
3833
|
-
tilesWrap.appendChild(addTile);
|
|
3834
|
-
if (hasLibrary) {
|
|
3835
|
-
const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
|
|
3836
|
-
tilesWrap.appendChild(libraryTile);
|
|
3837
|
-
}
|
|
3838
|
-
} else if (!isReadonly && atMax) {
|
|
3839
|
-
const chip = document.createElement("div");
|
|
3840
|
-
chip.className = "fb-tile-counter";
|
|
3841
|
-
chip.textContent = t("filesCounter", state, {
|
|
3842
|
-
count: ridList.length,
|
|
3843
|
-
max: maxCount
|
|
3844
|
-
});
|
|
3845
|
-
tilesWrap.appendChild(chip);
|
|
4237
|
+
onRemove !== null,
|
|
4238
|
+
onRemove ? () => onRemove(rid) : null
|
|
4239
|
+
);
|
|
4240
|
+
grid.appendChild(tile);
|
|
3846
4241
|
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4242
|
+
if (!atMax) {
|
|
4243
|
+
const addTile = buildMultiAddTile(
|
|
4244
|
+
state,
|
|
4245
|
+
hasLibrary,
|
|
4246
|
+
openPicker,
|
|
4247
|
+
onLibraryPick ?? null
|
|
4248
|
+
);
|
|
4249
|
+
grid.appendChild(addTile);
|
|
4250
|
+
}
|
|
4251
|
+
const occupied = ridList.length + (atMax ? 0 : 1);
|
|
4252
|
+
const adjustPlaceholders = () => {
|
|
4253
|
+
const tpl = getComputedStyle(grid).gridTemplateColumns;
|
|
4254
|
+
const cols = tpl ? tpl.split(" ").filter(Boolean).length : 0;
|
|
4255
|
+
if (!cols) return;
|
|
4256
|
+
const remainder = occupied % cols;
|
|
4257
|
+
const rowFill = remainder === 0 ? 0 : cols - remainder;
|
|
4258
|
+
const capacityRemaining = effectiveMax === Infinity ? rowFill : Math.max(0, effectiveMax - occupied);
|
|
4259
|
+
const needed = Math.min(rowFill, capacityRemaining);
|
|
4260
|
+
const existing = grid.querySelectorAll(".fb-multi-placeholder");
|
|
4261
|
+
if (existing.length > needed) {
|
|
4262
|
+
for (let i = existing.length - 1; i >= needed; i--) existing[i].remove();
|
|
4263
|
+
} else if (existing.length < needed) {
|
|
4264
|
+
for (let i = existing.length; i < needed; i++) {
|
|
4265
|
+
grid.appendChild(buildPlaceholderTile());
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
};
|
|
4269
|
+
if (effectiveMax === Infinity || effectiveMax > occupied) {
|
|
4270
|
+
grid.appendChild(buildPlaceholderTile());
|
|
4271
|
+
}
|
|
4272
|
+
requestAnimationFrame(adjustPlaceholders);
|
|
4273
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
4274
|
+
const ro = new ResizeObserver(() => adjustPlaceholders());
|
|
4275
|
+
ro.observe(grid);
|
|
4276
|
+
gridResizeObservers.set(container, ro);
|
|
4277
|
+
}
|
|
4278
|
+
attachDragOverFeedback(outerDiv, {
|
|
4279
|
+
activeClass: "fb-drag-over",
|
|
4280
|
+
onEnter: () => {
|
|
4281
|
+
grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
|
|
4282
|
+
p.classList.add("fb-drag-over");
|
|
4283
|
+
});
|
|
4284
|
+
const addTile = grid.querySelector(".fb-multi-add-tile-js");
|
|
4285
|
+
if (addTile) {
|
|
4286
|
+
addTile.classList.add("fb-drag-over-tile");
|
|
4287
|
+
const label = addTile.querySelector(".fb-multi-add-label");
|
|
4288
|
+
if (label) label.textContent = t("dropToUpload", state);
|
|
4289
|
+
}
|
|
4290
|
+
},
|
|
4291
|
+
onLeave: () => {
|
|
4292
|
+
grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
|
|
4293
|
+
p.classList.remove("fb-drag-over");
|
|
4294
|
+
});
|
|
4295
|
+
const addTile = grid.querySelector(".fb-multi-add-tile-js");
|
|
4296
|
+
if (addTile) {
|
|
4297
|
+
addTile.classList.remove("fb-drag-over-tile");
|
|
4298
|
+
const label = addTile.querySelector(".fb-multi-add-label");
|
|
4299
|
+
if (label) label.textContent = t("clickDragTextMultiple", state);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
if (element) {
|
|
4304
|
+
const metaLine = buildMetaLine(
|
|
4305
|
+
state,
|
|
4306
|
+
element,
|
|
4307
|
+
ridList.length,
|
|
4308
|
+
effectiveMax,
|
|
4309
|
+
Boolean(onClearAll),
|
|
4310
|
+
onClearAll ?? (() => {
|
|
4311
|
+
})
|
|
4312
|
+
);
|
|
4313
|
+
container.appendChild(metaLine);
|
|
3854
4314
|
}
|
|
3855
4315
|
}
|
|
3856
4316
|
function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
3857
4317
|
const state = ctx.state;
|
|
3858
4318
|
const fileWrapper = document.createElement("div");
|
|
3859
4319
|
fileWrapper.className = "space-y-2";
|
|
4320
|
+
fileWrapper.dataset.filesWrapper = pathKey;
|
|
3860
4321
|
const picker = document.createElement("input");
|
|
3861
4322
|
picker.type = "file";
|
|
3862
4323
|
picker.name = pathKey;
|
|
3863
4324
|
picker.style.display = "none";
|
|
3864
|
-
|
|
3865
|
-
picker.accept = typeof element.accept === "string" ? element.accept : [
|
|
3866
|
-
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
3867
|
-
...element.accept.mime ?? []
|
|
3868
|
-
].join(",") || "";
|
|
3869
|
-
}
|
|
4325
|
+
picker.accept = buildAcceptAttribute(element.accept);
|
|
3870
4326
|
const fileContainer = document.createElement("div");
|
|
3871
4327
|
fileContainer.className = "file-preview-container";
|
|
3872
4328
|
const initial = ctx.prefill[element.key];
|
|
@@ -3895,14 +4351,6 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3895
4351
|
setupDrop(container) {
|
|
3896
4352
|
setupDragAndDrop(container, handlers.dragHandler);
|
|
3897
4353
|
},
|
|
3898
|
-
restoreDropzone() {
|
|
3899
|
-
const hint = makeFieldHint(element, state);
|
|
3900
|
-
fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
|
|
3901
|
-
fileContainer.style.height = "128px";
|
|
3902
|
-
setEmptyFileContainer(fileContainer, state, hint);
|
|
3903
|
-
fileContainer.onclick = handlers.fileUploadHandler;
|
|
3904
|
-
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3905
|
-
},
|
|
3906
4354
|
onRemove() {
|
|
3907
4355
|
const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
|
|
3908
4356
|
const currentRid = hiddenInput?.value;
|
|
@@ -3913,34 +4361,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3913
4361
|
renderEmptySingleState();
|
|
3914
4362
|
}
|
|
3915
4363
|
};
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
onRemove: handlers.onRemove
|
|
3922
|
-
});
|
|
3923
|
-
const renderEmptySingleState = () => {
|
|
3924
|
-
if (state.config.pickExistingFiles && !element.disableLibrary) {
|
|
3925
|
-
fileContainer.className = "file-preview-container";
|
|
3926
|
-
fileContainer.removeAttribute("style");
|
|
3927
|
-
fileContainer.onclick = null;
|
|
3928
|
-
while (fileContainer.firstChild) {
|
|
3929
|
-
fileContainer.removeChild(fileContainer.firstChild);
|
|
3930
|
-
}
|
|
3931
|
-
const row = document.createElement("div");
|
|
3932
|
-
row.className = "fb-file-card-row";
|
|
3933
|
-
row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
|
|
3934
|
-
const hint = makeFieldHint(element, state);
|
|
3935
|
-
const uploadCard = buildEmptyDropzone(
|
|
3936
|
-
state,
|
|
3937
|
-
t("clickDragText", state),
|
|
3938
|
-
hint,
|
|
3939
|
-
handlers.fileUploadHandler
|
|
3940
|
-
);
|
|
3941
|
-
uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
|
|
3942
|
-
setupDragAndDrop(uploadCard, handlers.dragHandler);
|
|
3943
|
-
const libraryBtn = buildLibraryButton("card", state, () => {
|
|
4364
|
+
const buildSingleExtras = () => {
|
|
4365
|
+
const hasLibrary = Boolean(state.config.pickExistingFiles && !element.disableLibrary);
|
|
4366
|
+
return {
|
|
4367
|
+
replaceHandler: state.config.uploadFile ? () => picker.click() : null,
|
|
4368
|
+
libraryHandler: hasLibrary ? () => {
|
|
3944
4369
|
handleLibraryPickSingle(
|
|
3945
4370
|
state,
|
|
3946
4371
|
element,
|
|
@@ -3949,20 +4374,41 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3949
4374
|
pathKey,
|
|
3950
4375
|
pathKey,
|
|
3951
4376
|
async (rid) => {
|
|
3952
|
-
|
|
4377
|
+
renderSingleFileFilled(fileContainer, rid, state, buildDeps(), buildSingleExtras());
|
|
3953
4378
|
},
|
|
3954
4379
|
ctx.instance
|
|
3955
4380
|
).catch((err) => {
|
|
3956
4381
|
console.error("Library pick failed:", err);
|
|
3957
4382
|
});
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4383
|
+
} : null
|
|
4384
|
+
};
|
|
4385
|
+
};
|
|
4386
|
+
const buildDeps = () => ({
|
|
4387
|
+
picker,
|
|
4388
|
+
fileUploadHandler: handlers.fileUploadHandler,
|
|
4389
|
+
dragHandler: handlers.dragHandler,
|
|
4390
|
+
setupDrop: handlers.setupDrop,
|
|
4391
|
+
onRemove: handlers.onRemove,
|
|
4392
|
+
onAfterUpload: (container, rid) => {
|
|
4393
|
+
renderSingleFileFilled(container, rid, state, buildDeps(), buildSingleExtras());
|
|
3965
4394
|
}
|
|
4395
|
+
});
|
|
4396
|
+
const renderEmptySingleState = () => {
|
|
4397
|
+
ensureFileStyles();
|
|
4398
|
+
fileContainer.className = "file-preview-container";
|
|
4399
|
+
fileContainer.removeAttribute("style");
|
|
4400
|
+
while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
|
|
4401
|
+
const onLibraryClick = buildSingleExtras().libraryHandler;
|
|
4402
|
+
const wideTile = buildWideTile(
|
|
4403
|
+
state,
|
|
4404
|
+
onLibraryClick !== null,
|
|
4405
|
+
handlers.fileUploadHandler,
|
|
4406
|
+
onLibraryClick,
|
|
4407
|
+
false,
|
|
4408
|
+
makeFieldHint(element, state)
|
|
4409
|
+
);
|
|
4410
|
+
fileContainer.appendChild(wideTile);
|
|
4411
|
+
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3966
4412
|
};
|
|
3967
4413
|
if (initial) {
|
|
3968
4414
|
handleInitialFileData(
|
|
@@ -3971,11 +4417,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3971
4417
|
pathKey,
|
|
3972
4418
|
fileWrapper,
|
|
3973
4419
|
state,
|
|
3974
|
-
buildDeps()
|
|
4420
|
+
buildDeps(),
|
|
4421
|
+
buildSingleExtras()
|
|
3975
4422
|
);
|
|
3976
4423
|
const prefillMeta = state.resourceIndex.get(initial);
|
|
3977
4424
|
if (prefillMeta?.type?.startsWith("video/")) {
|
|
3978
|
-
fileContainer.onclick = handlers.fileUploadHandler;
|
|
3979
4425
|
setupDragAndDrop(fileContainer, handlers.dragHandler);
|
|
3980
4426
|
}
|
|
3981
4427
|
} else {
|
|
@@ -3983,113 +4429,23 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
3983
4429
|
}
|
|
3984
4430
|
picker.onchange = () => {
|
|
3985
4431
|
if (picker.files && picker.files.length > 0) {
|
|
3986
|
-
|
|
3987
|
-
file: picker.files[0],
|
|
3988
|
-
container: fileContainer,
|
|
3989
|
-
fieldName: pathKey,
|
|
3990
|
-
state,
|
|
3991
|
-
deps: buildDeps(),
|
|
3992
|
-
instance: ctx.instance,
|
|
3993
|
-
allowedExtensions: allowedExts,
|
|
3994
|
-
allowedMimes,
|
|
3995
|
-
maxSizeMB
|
|
3996
|
-
});
|
|
4432
|
+
handlers.dragHandler(picker.files);
|
|
3997
4433
|
}
|
|
3998
4434
|
};
|
|
3999
4435
|
fileWrapper.appendChild(fileContainer);
|
|
4000
4436
|
fileWrapper.appendChild(picker);
|
|
4001
4437
|
wrapper.appendChild(fileWrapper);
|
|
4002
4438
|
}
|
|
4003
|
-
function
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
filesPicker.name = pathKey;
|
|
4011
|
-
filesPicker.multiple = true;
|
|
4012
|
-
filesPicker.style.display = "none";
|
|
4013
|
-
if (element.accept) {
|
|
4014
|
-
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4015
|
-
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4016
|
-
...element.accept.mime ?? []
|
|
4017
|
-
].join(",") || "";
|
|
4018
|
-
}
|
|
4019
|
-
const filesContainer = document.createElement("div");
|
|
4020
|
-
filesContainer.className = "files-list-wrapper";
|
|
4021
|
-
filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
|
|
4022
|
-
const list = document.createElement("div");
|
|
4023
|
-
list.className = "files-list";
|
|
4024
|
-
const initialFiles = ctx.prefill[element.key] || [];
|
|
4025
|
-
addPrefillFilesToIndex(initialFiles, state.resourceIndex);
|
|
4026
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
4027
|
-
const filesFieldHint = makeFieldHint(element, state);
|
|
4028
|
-
const filesConstraints = {
|
|
4029
|
-
maxCount: Infinity,
|
|
4030
|
-
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4031
|
-
allowedMimes: getAllowedMimes(element.accept),
|
|
4032
|
-
maxSize: element.maxSize ?? Infinity
|
|
4033
|
-
};
|
|
4034
|
-
filesContainer.appendChild(list);
|
|
4035
|
-
filesWrapper.appendChild(filesPicker);
|
|
4036
|
-
filesWrapper.appendChild(filesContainer);
|
|
4037
|
-
wrapper.appendChild(filesWrapper);
|
|
4038
|
-
const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4039
|
-
handleLibraryPickMulti(
|
|
4040
|
-
state,
|
|
4041
|
-
element,
|
|
4042
|
-
filesWrapper,
|
|
4043
|
-
pathKey,
|
|
4044
|
-
initialFiles,
|
|
4045
|
-
Infinity,
|
|
4046
|
-
updateFilesList,
|
|
4047
|
-
ctx.instance
|
|
4048
|
-
).catch((err) => {
|
|
4049
|
-
console.error("Library pick failed:", err);
|
|
4050
|
-
});
|
|
4051
|
-
} : null;
|
|
4052
|
-
function updateFilesList() {
|
|
4053
|
-
const currentlyReadonly = isElementReadonly(element, state);
|
|
4054
|
-
renderResourcePills({
|
|
4055
|
-
container: list,
|
|
4056
|
-
rids: initialFiles,
|
|
4057
|
-
state,
|
|
4058
|
-
onRemove: currentlyReadonly ? null : (ridToRemove) => {
|
|
4059
|
-
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
4060
|
-
const index = initialFiles.indexOf(ridToRemove);
|
|
4061
|
-
if (index > -1) initialFiles.splice(index, 1);
|
|
4062
|
-
updateFilesList();
|
|
4063
|
-
},
|
|
4064
|
-
hint: filesFieldHint,
|
|
4065
|
-
isReadonly: currentlyReadonly,
|
|
4066
|
-
onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
|
|
4067
|
-
});
|
|
4068
|
-
}
|
|
4069
|
-
updateFilesList();
|
|
4070
|
-
setupFilesDropHandler(
|
|
4071
|
-
filesContainer,
|
|
4072
|
-
initialFiles,
|
|
4073
|
-
state,
|
|
4074
|
-
updateFilesList,
|
|
4075
|
-
filesConstraints,
|
|
4076
|
-
pathKey,
|
|
4077
|
-
ctx.instance
|
|
4078
|
-
);
|
|
4079
|
-
setupFilesPickerHandler(
|
|
4080
|
-
filesPicker,
|
|
4081
|
-
initialFiles,
|
|
4082
|
-
state,
|
|
4083
|
-
updateFilesList,
|
|
4084
|
-
filesConstraints,
|
|
4085
|
-
pathKey,
|
|
4086
|
-
ctx.instance
|
|
4087
|
-
);
|
|
4439
|
+
function buildAcceptAttribute(accept) {
|
|
4440
|
+
if (!accept) return "";
|
|
4441
|
+
if (typeof accept === "string") return accept;
|
|
4442
|
+
return [
|
|
4443
|
+
...accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4444
|
+
...accept.mime ?? []
|
|
4445
|
+
].join(",");
|
|
4088
4446
|
}
|
|
4089
|
-
function
|
|
4447
|
+
function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
4090
4448
|
const state = ctx.state;
|
|
4091
|
-
const minFiles = element.minCount ?? 0;
|
|
4092
|
-
const maxFiles = element.maxCount ?? Infinity;
|
|
4093
4449
|
const filesWrapper = document.createElement("div");
|
|
4094
4450
|
filesWrapper.className = "space-y-2";
|
|
4095
4451
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
@@ -4098,15 +4454,9 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4098
4454
|
filesPicker.name = pathKey;
|
|
4099
4455
|
filesPicker.multiple = true;
|
|
4100
4456
|
filesPicker.style.display = "none";
|
|
4101
|
-
|
|
4102
|
-
filesPicker.accept = typeof element.accept === "string" ? element.accept : [
|
|
4103
|
-
...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
|
|
4104
|
-
...element.accept.mime ?? []
|
|
4105
|
-
].join(",") || "";
|
|
4106
|
-
}
|
|
4457
|
+
filesPicker.accept = buildAcceptAttribute(element.accept);
|
|
4107
4458
|
const filesContainer = document.createElement("div");
|
|
4108
4459
|
filesContainer.className = "files-list-wrapper";
|
|
4109
|
-
filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
|
|
4110
4460
|
const list = document.createElement("div");
|
|
4111
4461
|
list.className = "files-list";
|
|
4112
4462
|
filesWrapper.appendChild(filesPicker);
|
|
@@ -4115,19 +4465,18 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4115
4465
|
const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
|
|
4116
4466
|
addPrefillFilesToIndex(initialFiles, state.resourceIndex);
|
|
4117
4467
|
filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
|
|
4118
|
-
const
|
|
4119
|
-
const multipleConstraints = {
|
|
4468
|
+
const constraints = {
|
|
4120
4469
|
maxCount: maxFiles,
|
|
4121
4470
|
allowedExtensions: getAllowedExtensions(element.accept),
|
|
4122
4471
|
allowedMimes: getAllowedMimes(element.accept),
|
|
4123
|
-
|
|
4472
|
+
// Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
|
|
4473
|
+
// backward compatibility (matches addFileSizeHint in validation.ts).
|
|
4474
|
+
maxSize: element.maxSize ?? element.maxSizeMB ?? Infinity
|
|
4124
4475
|
};
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
|
|
4128
|
-
return countText + minMaxText;
|
|
4476
|
+
const openPicker = () => {
|
|
4477
|
+
filesPicker.click();
|
|
4129
4478
|
};
|
|
4130
|
-
const
|
|
4479
|
+
const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
|
|
4131
4480
|
handleLibraryPickMulti(
|
|
4132
4481
|
state,
|
|
4133
4482
|
element,
|
|
@@ -4141,30 +4490,35 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4141
4490
|
console.error("Library pick failed:", err);
|
|
4142
4491
|
});
|
|
4143
4492
|
} : null;
|
|
4144
|
-
|
|
4493
|
+
function updateFilesDisplay() {
|
|
4145
4494
|
const currentlyReadonly = isElementReadonly(element, state);
|
|
4146
4495
|
renderResourcePills({
|
|
4147
4496
|
container: list,
|
|
4148
4497
|
rids: initialFiles,
|
|
4149
4498
|
state,
|
|
4150
|
-
onRemove: currentlyReadonly ? null : (
|
|
4151
|
-
releaseLocalFileUrl(state.resourceIndex.get(
|
|
4152
|
-
initialFiles.
|
|
4499
|
+
onRemove: currentlyReadonly ? null : (ridToRemove) => {
|
|
4500
|
+
releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
|
|
4501
|
+
const index = initialFiles.indexOf(ridToRemove);
|
|
4502
|
+
if (index > -1) initialFiles.splice(index, 1);
|
|
4153
4503
|
updateFilesDisplay();
|
|
4154
4504
|
},
|
|
4155
|
-
hint: multipleFilesHint,
|
|
4156
|
-
countInfo: buildCountInfo(),
|
|
4157
4505
|
maxCount: maxFiles < Infinity ? maxFiles : void 0,
|
|
4158
4506
|
isReadonly: currentlyReadonly,
|
|
4159
|
-
onLibraryPick: currentlyReadonly ? null :
|
|
4507
|
+
onLibraryPick: currentlyReadonly ? null : onLibraryPick,
|
|
4508
|
+
element,
|
|
4509
|
+
onClearAll: currentlyReadonly ? void 0 : () => {
|
|
4510
|
+
initialFiles.splice(0);
|
|
4511
|
+
updateFilesDisplay();
|
|
4512
|
+
},
|
|
4513
|
+
openPicker
|
|
4160
4514
|
});
|
|
4161
|
-
}
|
|
4515
|
+
}
|
|
4162
4516
|
setupFilesDropHandler(
|
|
4163
4517
|
filesContainer,
|
|
4164
4518
|
initialFiles,
|
|
4165
4519
|
state,
|
|
4166
4520
|
updateFilesDisplay,
|
|
4167
|
-
|
|
4521
|
+
constraints,
|
|
4168
4522
|
pathKey,
|
|
4169
4523
|
ctx.instance
|
|
4170
4524
|
);
|
|
@@ -4173,13 +4527,19 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4173
4527
|
initialFiles,
|
|
4174
4528
|
state,
|
|
4175
4529
|
updateFilesDisplay,
|
|
4176
|
-
|
|
4530
|
+
constraints,
|
|
4177
4531
|
pathKey,
|
|
4178
4532
|
ctx.instance
|
|
4179
4533
|
);
|
|
4180
4534
|
updateFilesDisplay();
|
|
4181
4535
|
wrapper.appendChild(filesWrapper);
|
|
4182
4536
|
}
|
|
4537
|
+
function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
4538
|
+
setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
|
|
4539
|
+
}
|
|
4540
|
+
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
4541
|
+
setupMultiFileEditMode(element, ctx, wrapper, pathKey, element.maxCount ?? Infinity);
|
|
4542
|
+
}
|
|
4183
4543
|
|
|
4184
4544
|
// src/components/file/validate.ts
|
|
4185
4545
|
function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
@@ -4302,33 +4662,36 @@ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
|
|
|
4302
4662
|
hiddenInput.name = pathKey;
|
|
4303
4663
|
hiddenInput.value = initial;
|
|
4304
4664
|
wrapper.appendChild(hiddenInput);
|
|
4305
|
-
renderFilePreviewReadonly(initial, state).then((
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4665
|
+
renderFilePreviewReadonly(initial, state).then((tile) => {
|
|
4666
|
+
tile.classList.add(
|
|
4667
|
+
"fb-single-readonly-filled",
|
|
4668
|
+
"fb-readonly-tile",
|
|
4669
|
+
"fb-checker"
|
|
4670
|
+
);
|
|
4671
|
+
wrapper.appendChild(tile);
|
|
4672
|
+
}).catch(console.error);
|
|
4311
4673
|
} else {
|
|
4312
4674
|
wrapper.appendChild(buildEmptyReadonlyTile(state));
|
|
4313
4675
|
}
|
|
4314
4676
|
}
|
|
4315
4677
|
function buildEmptyReadonlyTile(state) {
|
|
4678
|
+
ensureFileStyles();
|
|
4316
4679
|
const emptyState = document.createElement("div");
|
|
4317
4680
|
emptyState.style.cssText = `
|
|
4318
|
-
|
|
4319
|
-
height:${TILE_SIZE};
|
|
4681
|
+
height: 220px;
|
|
4320
4682
|
display:flex;
|
|
4321
4683
|
align-items:center;
|
|
4322
4684
|
justify-content:center;
|
|
4323
|
-
background:
|
|
4324
|
-
border-radius:
|
|
4325
|
-
border:1px solid
|
|
4685
|
+
background: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
|
|
4686
|
+
border-radius:0.75rem;
|
|
4687
|
+
border:1px solid #e2e8f0;
|
|
4326
4688
|
`;
|
|
4327
4689
|
emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
|
|
4328
4690
|
return emptyState;
|
|
4329
4691
|
}
|
|
4330
|
-
function renderMultiFileReadonly(rids, state, wrapper, pathKey,
|
|
4692
|
+
function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
|
|
4331
4693
|
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
4694
|
+
ensureFileStyles();
|
|
4332
4695
|
const filesWrapper = document.createElement("div");
|
|
4333
4696
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
4334
4697
|
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
@@ -4340,22 +4703,28 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
|
|
|
4340
4703
|
filesWrapper.appendChild(emptyEl);
|
|
4341
4704
|
return;
|
|
4342
4705
|
}
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4345
|
-
filesWrapper.appendChild(
|
|
4706
|
+
const grid = document.createElement("div");
|
|
4707
|
+
grid.className = "fb-multi-readonly-grid";
|
|
4708
|
+
filesWrapper.appendChild(grid);
|
|
4346
4709
|
const placeholders = rids.map(() => {
|
|
4347
|
-
const
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
return
|
|
4710
|
+
const ph = document.createElement("div");
|
|
4711
|
+
ph.className = "fb-readonly-tile fb-checker fb-tile";
|
|
4712
|
+
grid.appendChild(ph);
|
|
4713
|
+
return ph;
|
|
4351
4714
|
});
|
|
4352
4715
|
for (let i = 0; i < rids.length; i++) {
|
|
4353
4716
|
const resourceId = rids[i];
|
|
4354
4717
|
const placeholder = placeholders[i];
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4718
|
+
const meta = state.resourceIndex.get(resourceId);
|
|
4719
|
+
renderFilePreviewReadonly(resourceId, state, meta?.name).then((tile) => {
|
|
4720
|
+
tile.classList.add("fb-readonly-tile", "fb-checker", "fb-tile-resource");
|
|
4721
|
+
tile.dataset.resourceId = resourceId;
|
|
4722
|
+
placeholder.replaceWith(tile);
|
|
4723
|
+
}).catch(() => {
|
|
4724
|
+
const tile = document.createElement("div");
|
|
4725
|
+
tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
|
|
4726
|
+
tile.dataset.resourceId = resourceId;
|
|
4727
|
+
placeholder.replaceWith(tile);
|
|
4359
4728
|
});
|
|
4360
4729
|
}
|
|
4361
4730
|
}
|
|
@@ -4367,7 +4736,7 @@ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
|
|
|
4367
4736
|
function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
|
|
4368
4737
|
const rawPrefill = ctx.prefill[element.key];
|
|
4369
4738
|
const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
|
|
4370
|
-
renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey
|
|
4739
|
+
renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
|
|
4371
4740
|
}
|
|
4372
4741
|
|
|
4373
4742
|
// src/components/file.ts
|
|
@@ -5454,7 +5823,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5454
5823
|
function extractChildDefaults(elements) {
|
|
5455
5824
|
const defaults = {};
|
|
5456
5825
|
for (const child of elements) {
|
|
5457
|
-
if ("default" in child && child.default !== void 0) {
|
|
5826
|
+
if (child.key && "default" in child && child.default !== void 0) {
|
|
5458
5827
|
defaults[child.key] = child.default;
|
|
5459
5828
|
}
|
|
5460
5829
|
}
|
|
@@ -5505,7 +5874,7 @@ function createPrefillHints(element, pathKey) {
|
|
|
5505
5874
|
return null;
|
|
5506
5875
|
}
|
|
5507
5876
|
const hintsContainer = document.createElement("div");
|
|
5508
|
-
hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-
|
|
5877
|
+
hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
|
|
5509
5878
|
element.prefillHints.forEach((hint, index) => {
|
|
5510
5879
|
const hintButton = document.createElement("button");
|
|
5511
5880
|
hintButton.type = "button";
|
|
@@ -5520,14 +5889,14 @@ function createPrefillHints(element, pathKey) {
|
|
|
5520
5889
|
}
|
|
5521
5890
|
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
5522
5891
|
const containerWrap = document.createElement("div");
|
|
5523
|
-
containerWrap.className = "border border-gray-200 rounded-lg p-
|
|
5892
|
+
containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
|
|
5524
5893
|
containerWrap.setAttribute("data-container", pathKey);
|
|
5525
5894
|
const itemsWrap = document.createElement("div");
|
|
5526
5895
|
const columns = element.columns || 1;
|
|
5527
5896
|
if (columns === 1) {
|
|
5528
|
-
itemsWrap.className = "space-y-
|
|
5897
|
+
itemsWrap.className = "space-y-2";
|
|
5529
5898
|
} else {
|
|
5530
|
-
itemsWrap.className = `grid grid-cols-${columns} gap-
|
|
5899
|
+
itemsWrap.className = `grid grid-cols-${columns} gap-2`;
|
|
5531
5900
|
}
|
|
5532
5901
|
const containerIsReadonly = isElementReadonly(element, ctx.state, ctx);
|
|
5533
5902
|
if (!containerIsReadonly) {
|
|
@@ -5549,8 +5918,8 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
5549
5918
|
inheritedReadonly: containerIsReadonly || ctx.inheritedReadonly
|
|
5550
5919
|
};
|
|
5551
5920
|
element.elements.forEach((child) => {
|
|
5552
|
-
if (child.hidden || child.type === "hidden") {
|
|
5553
|
-
const prefillVal = containerPrefill[child.key] ?? child.default ?? null;
|
|
5921
|
+
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
5922
|
+
const prefillVal = containerPrefill[child.key] ?? ("default" in child ? child.default : null) ?? null;
|
|
5554
5923
|
itemsWrap.appendChild(
|
|
5555
5924
|
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
5556
5925
|
);
|
|
@@ -5561,16 +5930,31 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
5561
5930
|
containerWrap.appendChild(itemsWrap);
|
|
5562
5931
|
wrapper.appendChild(containerWrap);
|
|
5563
5932
|
}
|
|
5933
|
+
function getChildWrapperClass(isSlides, columns) {
|
|
5934
|
+
if (isSlides) {
|
|
5935
|
+
return "space-y-2";
|
|
5936
|
+
}
|
|
5937
|
+
const cols = columns || 1;
|
|
5938
|
+
return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
|
|
5939
|
+
}
|
|
5564
5940
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
5565
5941
|
const state = ctx.state;
|
|
5566
5942
|
const containerIsReadonly = isElementReadonly(element, state, ctx);
|
|
5567
5943
|
const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
|
|
5568
5944
|
const containerWrap = document.createElement("div");
|
|
5569
|
-
containerWrap.className = "border border-gray-200 rounded-lg p-
|
|
5945
|
+
containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
|
|
5570
5946
|
const countDisplay = document.createElement("span");
|
|
5571
5947
|
countDisplay.className = "text-sm text-gray-500";
|
|
5572
5948
|
const itemsWrap = document.createElement("div");
|
|
5573
|
-
|
|
5949
|
+
const isSlides = element.displayMode === "slides";
|
|
5950
|
+
if (isSlides) {
|
|
5951
|
+
itemsWrap.className = "fb-container-slides";
|
|
5952
|
+
const slideCols = element.columns;
|
|
5953
|
+
const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
|
|
5954
|
+
itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
|
|
5955
|
+
} else {
|
|
5956
|
+
itemsWrap.className = "space-y-2";
|
|
5957
|
+
}
|
|
5574
5958
|
if (!containerIsReadonly) {
|
|
5575
5959
|
const hintsElement = createPrefillHints(element, element.key);
|
|
5576
5960
|
if (hintsElement) {
|
|
@@ -5614,21 +5998,16 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
5614
5998
|
inheritedReadonly: childInheritedReadonly
|
|
5615
5999
|
};
|
|
5616
6000
|
const item = document.createElement("div");
|
|
5617
|
-
item.className = "containerItem border border-gray-300 rounded-lg p-
|
|
6001
|
+
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
5618
6002
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
5619
6003
|
const childWrapper = document.createElement("div");
|
|
5620
|
-
|
|
5621
|
-
if (columns === 1) {
|
|
5622
|
-
childWrapper.className = "space-y-4";
|
|
5623
|
-
} else {
|
|
5624
|
-
childWrapper.className = `grid grid-cols-${columns} gap-4`;
|
|
5625
|
-
}
|
|
6004
|
+
childWrapper.className = getChildWrapperClass(isSlides, element.columns);
|
|
5626
6005
|
element.elements.forEach((child) => {
|
|
5627
|
-
if (child.hidden || child.type === "hidden") {
|
|
6006
|
+
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
5628
6007
|
childWrapper.appendChild(
|
|
5629
6008
|
createHiddenInput(
|
|
5630
6009
|
pathJoin(subCtx.path, child.key),
|
|
5631
|
-
child.default ?? null
|
|
6010
|
+
("default" in child ? child.default : null) ?? null
|
|
5632
6011
|
)
|
|
5633
6012
|
);
|
|
5634
6013
|
} else {
|
|
@@ -5691,18 +6070,22 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
5691
6070
|
inheritedReadonly: childInheritedReadonly
|
|
5692
6071
|
};
|
|
5693
6072
|
const item = document.createElement("div");
|
|
5694
|
-
item.className = "containerItem border border-gray-300 rounded-lg p-
|
|
6073
|
+
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
5695
6074
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
5696
6075
|
const childWrapper = document.createElement("div");
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
childWrapper.className = "space-y-4";
|
|
6076
|
+
if (isSlides) {
|
|
6077
|
+
childWrapper.className = "space-y-2";
|
|
5700
6078
|
} else {
|
|
5701
|
-
|
|
6079
|
+
const columns = element.columns || 1;
|
|
6080
|
+
if (columns === 1) {
|
|
6081
|
+
childWrapper.className = "space-y-2";
|
|
6082
|
+
} else {
|
|
6083
|
+
childWrapper.className = `grid grid-cols-${columns} gap-2`;
|
|
6084
|
+
}
|
|
5702
6085
|
}
|
|
5703
6086
|
element.elements.forEach((child) => {
|
|
5704
|
-
if (child.hidden || child.type === "hidden") {
|
|
5705
|
-
const prefillVal = prefillObj?.[child.key] ?? child.default ?? null;
|
|
6087
|
+
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6088
|
+
const prefillVal = prefillObj?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
|
|
5706
6089
|
childWrapper.appendChild(
|
|
5707
6090
|
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
5708
6091
|
);
|
|
@@ -5747,21 +6130,25 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
5747
6130
|
inheritedReadonly: childInheritedReadonly
|
|
5748
6131
|
};
|
|
5749
6132
|
const item = document.createElement("div");
|
|
5750
|
-
item.className = "containerItem border border-gray-300 rounded-lg p-
|
|
6133
|
+
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
5751
6134
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
5752
6135
|
const childWrapper = document.createElement("div");
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
childWrapper.className = "space-y-4";
|
|
6136
|
+
if (isSlides) {
|
|
6137
|
+
childWrapper.className = "space-y-2";
|
|
5756
6138
|
} else {
|
|
5757
|
-
|
|
6139
|
+
const columns = element.columns || 1;
|
|
6140
|
+
if (columns === 1) {
|
|
6141
|
+
childWrapper.className = "space-y-2";
|
|
6142
|
+
} else {
|
|
6143
|
+
childWrapper.className = `grid grid-cols-${columns} gap-2`;
|
|
6144
|
+
}
|
|
5758
6145
|
}
|
|
5759
6146
|
element.elements.forEach((child) => {
|
|
5760
|
-
if (child.hidden || child.type === "hidden") {
|
|
6147
|
+
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
5761
6148
|
childWrapper.appendChild(
|
|
5762
6149
|
createHiddenInput(
|
|
5763
6150
|
pathJoin(subCtx.path, child.key),
|
|
5764
|
-
child.default ?? null
|
|
6151
|
+
("default" in child ? child.default : null) ?? null
|
|
5765
6152
|
)
|
|
5766
6153
|
);
|
|
5767
6154
|
} else {
|
|
@@ -5947,6 +6334,7 @@ function updateContainerField(element, fieldPath, value, context) {
|
|
|
5947
6334
|
value.forEach((itemValue, index) => {
|
|
5948
6335
|
if (isPlainObject(itemValue)) {
|
|
5949
6336
|
element.elements.forEach((childElement) => {
|
|
6337
|
+
if (childElement.type === "markdown" || !childElement.key) return;
|
|
5950
6338
|
const childPath = `${fieldPath}[${index}].${childElement.key}`;
|
|
5951
6339
|
if (childElement.type === "richinput" && childElement.flatOutput) {
|
|
5952
6340
|
const richChild = childElement;
|
|
@@ -5986,6 +6374,7 @@ function updateContainerField(element, fieldPath, value, context) {
|
|
|
5986
6374
|
return;
|
|
5987
6375
|
}
|
|
5988
6376
|
element.elements.forEach((childElement) => {
|
|
6377
|
+
if (childElement.type === "markdown" || !childElement.key) return;
|
|
5989
6378
|
const childPath = `${fieldPath}.${childElement.key}`;
|
|
5990
6379
|
if (childElement.type === "richinput" && childElement.flatOutput) {
|
|
5991
6380
|
const richChild = childElement;
|
|
@@ -7780,7 +8169,7 @@ function filterFilesForDropdown(query, files, labels) {
|
|
|
7780
8169
|
});
|
|
7781
8170
|
}
|
|
7782
8171
|
var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
|
|
7783
|
-
var TEXTAREA_PADDING = "padding:
|
|
8172
|
+
var TEXTAREA_PADDING = "padding: 8px 40px 8px 10px;";
|
|
7784
8173
|
function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
7785
8174
|
const state = ctx.state;
|
|
7786
8175
|
const files = [...initialValue.files];
|
|
@@ -7827,7 +8216,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
7827
8216
|
});
|
|
7828
8217
|
const errorEl = document.createElement("div");
|
|
7829
8218
|
errorEl.className = "fb-richinput-error";
|
|
7830
|
-
errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px
|
|
8219
|
+
errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 10px 6px;";
|
|
7831
8220
|
let errorTimer = null;
|
|
7832
8221
|
function showUploadError(message) {
|
|
7833
8222
|
errorEl.textContent = message;
|
|
@@ -7903,7 +8292,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
7903
8292
|
});
|
|
7904
8293
|
const filesRow = document.createElement("div");
|
|
7905
8294
|
filesRow.className = "fb-richinput-files";
|
|
7906
|
-
filesRow.style.cssText = "display: none; flex-wrap: wrap; gap:
|
|
8295
|
+
filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 4px; padding: 6px 10px 0; align-items: center;";
|
|
7907
8296
|
const fileInput = document.createElement("input");
|
|
7908
8297
|
fileInput.type = "file";
|
|
7909
8298
|
fileInput.multiple = true;
|
|
@@ -8033,13 +8422,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
8033
8422
|
paperclipBtn.title = t("richinputAttachFile", state);
|
|
8034
8423
|
paperclipBtn.style.cssText = `
|
|
8035
8424
|
position: absolute;
|
|
8036
|
-
right:
|
|
8037
|
-
bottom:
|
|
8425
|
+
right: 6px;
|
|
8426
|
+
bottom: 6px;
|
|
8038
8427
|
z-index: 2;
|
|
8039
|
-
width:
|
|
8040
|
-
height:
|
|
8428
|
+
width: 28px;
|
|
8429
|
+
height: 28px;
|
|
8041
8430
|
border: none;
|
|
8042
|
-
border-radius:
|
|
8431
|
+
border-radius: 6px;
|
|
8043
8432
|
background: transparent;
|
|
8044
8433
|
cursor: pointer;
|
|
8045
8434
|
display: flex;
|
|
@@ -8393,7 +8782,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
8393
8782
|
outerDiv.appendChild(errorEl);
|
|
8394
8783
|
if (element.minLength != null || element.maxLength != null) {
|
|
8395
8784
|
const counterRow = document.createElement("div");
|
|
8396
|
-
counterRow.style.cssText = "position: relative; padding: 2px
|
|
8785
|
+
counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
|
|
8397
8786
|
const counter = createCharCounter(element, textarea, false);
|
|
8398
8787
|
counter.style.cssText = `
|
|
8399
8788
|
position: static;
|
|
@@ -8685,6 +9074,248 @@ function updateRichInputField(element, fieldPath, value, context) {
|
|
|
8685
9074
|
}
|
|
8686
9075
|
}
|
|
8687
9076
|
|
|
9077
|
+
// src/components/markdown/snarkdown.ts
|
|
9078
|
+
var TAGS = {
|
|
9079
|
+
"": ["<em>", "</em>"],
|
|
9080
|
+
_: ["<strong>", "</strong>"],
|
|
9081
|
+
"*": ["<strong>", "</strong>"],
|
|
9082
|
+
"~": ["<s>", "</s>"],
|
|
9083
|
+
"\n": ["<br />"],
|
|
9084
|
+
" ": ["<br />"],
|
|
9085
|
+
"-": ["<hr />"]
|
|
9086
|
+
};
|
|
9087
|
+
function outdent(str) {
|
|
9088
|
+
return str.replace(
|
|
9089
|
+
RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
|
|
9090
|
+
""
|
|
9091
|
+
);
|
|
9092
|
+
}
|
|
9093
|
+
function encodeAttr(str) {
|
|
9094
|
+
return (str + "").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9095
|
+
}
|
|
9096
|
+
function parse(md, prevLinks) {
|
|
9097
|
+
const tokenizer = /((?:^|\n+)(?:\n---+|\*[ ]\*(?:[ ]\*)+)\n)|(?:^```[ ]*(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t|[ ]{2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:!\[([^\]]*?)\]\(([^)]+?)\))|(\[)|(\](?:\(([^)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,6})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|([ ]{2}\n\n*|\n{2,}|__|\*\*|[_*]|~~)/gm;
|
|
9098
|
+
const context = [];
|
|
9099
|
+
let out = "";
|
|
9100
|
+
const links = prevLinks || {};
|
|
9101
|
+
let last = 0;
|
|
9102
|
+
let chunk;
|
|
9103
|
+
let prev;
|
|
9104
|
+
let token;
|
|
9105
|
+
let inner;
|
|
9106
|
+
let t2;
|
|
9107
|
+
function tag(token2) {
|
|
9108
|
+
const desc = TAGS[token2[1] || ""];
|
|
9109
|
+
const end = context[context.length - 1] === token2;
|
|
9110
|
+
if (!desc) return token2;
|
|
9111
|
+
if (!desc[1]) return desc[0];
|
|
9112
|
+
if (end) context.pop();
|
|
9113
|
+
else context.push(token2);
|
|
9114
|
+
return desc[end ? 1 : 0];
|
|
9115
|
+
}
|
|
9116
|
+
function flush() {
|
|
9117
|
+
let str = "";
|
|
9118
|
+
while (context.length) str += tag(context[context.length - 1]);
|
|
9119
|
+
return str;
|
|
9120
|
+
}
|
|
9121
|
+
md = md.replace(/^\[(.+?)\]:\s*(.+)$/gm, (_s, name, url) => {
|
|
9122
|
+
links[name.toLowerCase()] = url;
|
|
9123
|
+
return "";
|
|
9124
|
+
}).replace(/^\n+|\n+$/g, "");
|
|
9125
|
+
while (token = tokenizer.exec(md)) {
|
|
9126
|
+
prev = md.substring(last, token.index);
|
|
9127
|
+
last = tokenizer.lastIndex;
|
|
9128
|
+
chunk = token[0];
|
|
9129
|
+
if (prev.match(/[^\\](\\\\)*\\$/)) ; else if (t2 = token[3] || token[4]) {
|
|
9130
|
+
chunk = '<pre class="code ' + (token[4] ? "poetry" : token[2].toLowerCase()) + '"><code' + (token[2] ? ` class="language-${token[2].toLowerCase()}"` : "") + ">" + outdent(encodeAttr(t2).replace(/^\n+|\n+$/g, "")) + "</code></pre>";
|
|
9131
|
+
} else if (t2 = token[6]) {
|
|
9132
|
+
if (t2.match(/\./)) {
|
|
9133
|
+
token[5] = token[5].replace(/^\d+/gm, "");
|
|
9134
|
+
}
|
|
9135
|
+
inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, "")));
|
|
9136
|
+
if (t2 === ">") t2 = "blockquote";
|
|
9137
|
+
else {
|
|
9138
|
+
t2 = t2.match(/\./) ? "ol" : "ul";
|
|
9139
|
+
inner = inner.replace(/^(.*)(\n|$)/gm, "<li>$1</li>");
|
|
9140
|
+
}
|
|
9141
|
+
chunk = "<" + t2 + ">" + inner + "</" + t2 + ">";
|
|
9142
|
+
} else if (token[8]) {
|
|
9143
|
+
chunk = `<img src="${encodeAttr(token[8])}" alt="${encodeAttr(token[7])}">`;
|
|
9144
|
+
} else if (token[10]) {
|
|
9145
|
+
out = out.replace(
|
|
9146
|
+
"<a>",
|
|
9147
|
+
`<a href="${encodeAttr(token[11] || links[prev.toLowerCase()])}">`
|
|
9148
|
+
);
|
|
9149
|
+
chunk = flush() + "</a>";
|
|
9150
|
+
} else if (token[9]) {
|
|
9151
|
+
chunk = "<a>";
|
|
9152
|
+
} else if (token[12] || token[14]) {
|
|
9153
|
+
t2 = "h" + (token[14] ? token[14].length : token[13] > "=" ? 1 : 2);
|
|
9154
|
+
chunk = "<" + t2 + ">" + parse(token[12] || token[15], links) + "</" + t2 + ">";
|
|
9155
|
+
} else if (token[16]) {
|
|
9156
|
+
chunk = "<code>" + encodeAttr(token[16]) + "</code>";
|
|
9157
|
+
} else if (token[17] || token[1]) {
|
|
9158
|
+
chunk = tag(token[17] || "--");
|
|
9159
|
+
}
|
|
9160
|
+
out += prev;
|
|
9161
|
+
out += chunk;
|
|
9162
|
+
}
|
|
9163
|
+
return (out + md.substring(last) + flush()).replace(/^\n+|\n+$/g, "");
|
|
9164
|
+
}
|
|
9165
|
+
|
|
9166
|
+
// src/components/markdown/render.ts
|
|
9167
|
+
var STYLE_ID2 = "fb-markdown-styles";
|
|
9168
|
+
function ensureMarkdownStyles() {
|
|
9169
|
+
if (typeof document === "undefined") return;
|
|
9170
|
+
if (document.getElementById(STYLE_ID2)) return;
|
|
9171
|
+
const style = document.createElement("style");
|
|
9172
|
+
style.id = STYLE_ID2;
|
|
9173
|
+
style.setAttribute("data-fb-markdown-styles", "true");
|
|
9174
|
+
style.textContent = `
|
|
9175
|
+
.fb-markdown {
|
|
9176
|
+
font-family: var(--fb-font-family, inherit);
|
|
9177
|
+
font-size: var(--fb-font-size, 1rem);
|
|
9178
|
+
color: var(--fb-text-color, #1f2937);
|
|
9179
|
+
line-height: 1.6;
|
|
9180
|
+
}
|
|
9181
|
+
.fb-markdown h1,
|
|
9182
|
+
.fb-markdown h2,
|
|
9183
|
+
.fb-markdown h3,
|
|
9184
|
+
.fb-markdown h4,
|
|
9185
|
+
.fb-markdown h5,
|
|
9186
|
+
.fb-markdown h6 {
|
|
9187
|
+
font-weight: var(--fb-font-weight-medium, 600);
|
|
9188
|
+
color: var(--fb-text-color, #1f2937);
|
|
9189
|
+
margin-top: 0.75em;
|
|
9190
|
+
margin-bottom: 0.25em;
|
|
9191
|
+
}
|
|
9192
|
+
.fb-markdown h1 { font-size: 1.5rem; }
|
|
9193
|
+
.fb-markdown h2 { font-size: 1.25rem; }
|
|
9194
|
+
.fb-markdown h3 { font-size: 1.1rem; }
|
|
9195
|
+
.fb-markdown h4,
|
|
9196
|
+
.fb-markdown h5,
|
|
9197
|
+
.fb-markdown h6 { font-size: 1rem; }
|
|
9198
|
+
.fb-markdown p {
|
|
9199
|
+
margin-top: 0;
|
|
9200
|
+
margin-bottom: 0.5em;
|
|
9201
|
+
}
|
|
9202
|
+
.fb-markdown ul,
|
|
9203
|
+
.fb-markdown ol {
|
|
9204
|
+
margin: 0.25em 0 0.5em 1.5em;
|
|
9205
|
+
padding: 0;
|
|
9206
|
+
}
|
|
9207
|
+
.fb-markdown li {
|
|
9208
|
+
margin-bottom: 0.15em;
|
|
9209
|
+
}
|
|
9210
|
+
.fb-markdown a {
|
|
9211
|
+
color: var(--fb-primary-color, #2563eb);
|
|
9212
|
+
text-decoration: underline;
|
|
9213
|
+
}
|
|
9214
|
+
.fb-markdown a:hover {
|
|
9215
|
+
opacity: 0.8;
|
|
9216
|
+
}
|
|
9217
|
+
.fb-markdown code {
|
|
9218
|
+
background: var(--fb-input-background-color, #f3f4f6);
|
|
9219
|
+
border-radius: 3px;
|
|
9220
|
+
padding: 0.1em 0.35em;
|
|
9221
|
+
font-size: 0.9em;
|
|
9222
|
+
font-family: monospace;
|
|
9223
|
+
}
|
|
9224
|
+
.fb-markdown pre {
|
|
9225
|
+
background: var(--fb-input-background-color, #f3f4f6);
|
|
9226
|
+
border-radius: var(--fb-border-radius, 0.375rem);
|
|
9227
|
+
padding: 0.75em 1em;
|
|
9228
|
+
overflow-x: auto;
|
|
9229
|
+
margin: 0.5em 0;
|
|
9230
|
+
}
|
|
9231
|
+
.fb-markdown pre code {
|
|
9232
|
+
background: none;
|
|
9233
|
+
padding: 0;
|
|
9234
|
+
border-radius: 0;
|
|
9235
|
+
font-size: inherit;
|
|
9236
|
+
}
|
|
9237
|
+
.fb-markdown blockquote {
|
|
9238
|
+
border-left: 3px solid var(--fb-border-color, #d1d5db);
|
|
9239
|
+
margin: 0.5em 0;
|
|
9240
|
+
padding-left: 1em;
|
|
9241
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
9242
|
+
}
|
|
9243
|
+
.fb-markdown hr {
|
|
9244
|
+
border: none;
|
|
9245
|
+
border-top: 1px solid var(--fb-border-color, #d1d5db);
|
|
9246
|
+
margin: 0.75em 0;
|
|
9247
|
+
}
|
|
9248
|
+
.fb-markdown strong { font-weight: var(--fb-font-weight-medium, 600); }
|
|
9249
|
+
.fb-markdown em { font-style: italic; }
|
|
9250
|
+
.fb-markdown s { text-decoration: line-through; }
|
|
9251
|
+
`;
|
|
9252
|
+
document.head.appendChild(style);
|
|
9253
|
+
}
|
|
9254
|
+
var ANCHOR_DANGEROUS_SCHEMES = [
|
|
9255
|
+
"javascript:",
|
|
9256
|
+
"data:",
|
|
9257
|
+
"vbscript:",
|
|
9258
|
+
"blob:"
|
|
9259
|
+
];
|
|
9260
|
+
var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
|
|
9261
|
+
function isImgSrcDangerous(normalized) {
|
|
9262
|
+
if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
|
|
9263
|
+
return true;
|
|
9264
|
+
}
|
|
9265
|
+
if (normalized.startsWith("data:") && !normalized.startsWith("data:image/")) {
|
|
9266
|
+
return true;
|
|
9267
|
+
}
|
|
9268
|
+
return false;
|
|
9269
|
+
}
|
|
9270
|
+
function escapeRawHtml(content) {
|
|
9271
|
+
return content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
9272
|
+
}
|
|
9273
|
+
function sanitizeElements(container) {
|
|
9274
|
+
const anchors = container.querySelectorAll("a[href]");
|
|
9275
|
+
anchors.forEach((a) => {
|
|
9276
|
+
const href = a.getAttribute("href") ?? "";
|
|
9277
|
+
const normalized = href.trim().toLowerCase();
|
|
9278
|
+
const isDangerous = ANCHOR_DANGEROUS_SCHEMES.some(
|
|
9279
|
+
(scheme) => normalized.startsWith(scheme)
|
|
9280
|
+
);
|
|
9281
|
+
if (isDangerous) {
|
|
9282
|
+
a.setAttribute("href", "#");
|
|
9283
|
+
}
|
|
9284
|
+
a.setAttribute("target", "_blank");
|
|
9285
|
+
a.setAttribute("rel", "noopener noreferrer");
|
|
9286
|
+
});
|
|
9287
|
+
const images = container.querySelectorAll("img[src]");
|
|
9288
|
+
images.forEach((img) => {
|
|
9289
|
+
const src = img.getAttribute("src") ?? "";
|
|
9290
|
+
const normalized = src.trim().toLowerCase();
|
|
9291
|
+
if (isImgSrcDangerous(normalized)) {
|
|
9292
|
+
img.setAttribute("src", "");
|
|
9293
|
+
}
|
|
9294
|
+
});
|
|
9295
|
+
}
|
|
9296
|
+
function renderMarkdown(element, _ctx, parent) {
|
|
9297
|
+
if (typeof element.content !== "string") {
|
|
9298
|
+
throw new Error(
|
|
9299
|
+
`renderMarkdown: markdown element${element.key ? ` "${element.key}"` : ""} requires "content" to be a string (got ${element.content === null ? "null" : typeof element.content})`
|
|
9300
|
+
);
|
|
9301
|
+
}
|
|
9302
|
+
ensureMarkdownStyles();
|
|
9303
|
+
const wrapper = document.createElement("div");
|
|
9304
|
+
wrapper.className = "fb-markdown";
|
|
9305
|
+
const escaped = escapeRawHtml(element.content);
|
|
9306
|
+
wrapper.innerHTML = parse(escaped);
|
|
9307
|
+
sanitizeElements(wrapper);
|
|
9308
|
+
parent.appendChild(wrapper);
|
|
9309
|
+
return wrapper;
|
|
9310
|
+
}
|
|
9311
|
+
|
|
9312
|
+
// src/components/markdown/index.ts
|
|
9313
|
+
function validateMarkdown(_element, _key, _context) {
|
|
9314
|
+
return { value: void 0, errors: [], skip: true };
|
|
9315
|
+
}
|
|
9316
|
+
function updateMarkdown(_element, _fieldPath, _value, _context) {
|
|
9317
|
+
}
|
|
9318
|
+
|
|
8688
9319
|
// src/components/index.ts
|
|
8689
9320
|
function showTooltip(tooltipId, button) {
|
|
8690
9321
|
const tooltip = document.getElementById(tooltipId);
|
|
@@ -8915,7 +9546,7 @@ function setupEnableIfListeners(wrapper, element, ctx) {
|
|
|
8915
9546
|
function createFieldLabel(element) {
|
|
8916
9547
|
const title = document.createElement("label");
|
|
8917
9548
|
title.className = "text-sm font-medium text-gray-900";
|
|
8918
|
-
title.textContent = element.label
|
|
9549
|
+
title.textContent = element.label ?? element.key ?? "";
|
|
8919
9550
|
if (element.required) {
|
|
8920
9551
|
const req = document.createElement("span");
|
|
8921
9552
|
req.className = "text-red-500 ml-1";
|
|
@@ -8945,7 +9576,7 @@ function createInfoButton(element) {
|
|
|
8945
9576
|
}
|
|
8946
9577
|
function createLabelContainer(element) {
|
|
8947
9578
|
const label = document.createElement("div");
|
|
8948
|
-
label.className = "flex items-center mb-
|
|
9579
|
+
label.className = "flex items-center mb-1";
|
|
8949
9580
|
const title = createFieldLabel(element);
|
|
8950
9581
|
label.appendChild(title);
|
|
8951
9582
|
if (element.description || element.hint) {
|
|
@@ -9043,9 +9674,32 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
9043
9674
|
}
|
|
9044
9675
|
}
|
|
9045
9676
|
function renderElement2(element, ctx) {
|
|
9677
|
+
if (element.type === "markdown") {
|
|
9678
|
+
if (element.hidden === true) {
|
|
9679
|
+
const placeholder = document.createElement("div");
|
|
9680
|
+
placeholder.style.display = "none";
|
|
9681
|
+
placeholder.setAttribute("data-fb-hidden-markdown", "true");
|
|
9682
|
+
return placeholder;
|
|
9683
|
+
}
|
|
9684
|
+
const initiallyDisabled2 = shouldDisableElement(element, ctx);
|
|
9685
|
+
const outerWrapper = document.createElement("div");
|
|
9686
|
+
outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
|
|
9687
|
+
outerWrapper.setAttribute(
|
|
9688
|
+
"data-field-key",
|
|
9689
|
+
getElementLookupKey(element, ctx.state)
|
|
9690
|
+
);
|
|
9691
|
+
renderMarkdown(element, ctx, outerWrapper);
|
|
9692
|
+
if (initiallyDisabled2) {
|
|
9693
|
+
outerWrapper.style.display = "none";
|
|
9694
|
+
outerWrapper.classList.add("fb-field-wrapper-disabled");
|
|
9695
|
+
outerWrapper.setAttribute("data-conditionally-disabled", "true");
|
|
9696
|
+
}
|
|
9697
|
+
setupEnableIfListeners(outerWrapper, element, ctx);
|
|
9698
|
+
return outerWrapper;
|
|
9699
|
+
}
|
|
9046
9700
|
const initiallyDisabled = shouldDisableElement(element, ctx);
|
|
9047
9701
|
const wrapper = document.createElement("div");
|
|
9048
|
-
wrapper.className = "mb-
|
|
9702
|
+
wrapper.className = "mb-2 fb-field-wrapper";
|
|
9049
9703
|
wrapper.setAttribute("data-field-key", element.key);
|
|
9050
9704
|
const label = createLabelContainer(element);
|
|
9051
9705
|
wrapper.appendChild(label);
|
|
@@ -9112,12 +9766,16 @@ var defaultConfig = {
|
|
|
9112
9766
|
hintPattern: "Format: {pattern}",
|
|
9113
9767
|
fileCountSingle: "{count} file",
|
|
9114
9768
|
fileCountPlural: "{count} files",
|
|
9769
|
+
fileCountWithMax: "{count} / {max} files",
|
|
9115
9770
|
fileCountRange: "({min}-{max})",
|
|
9116
9771
|
uploadingFile: "Uploading\u2026",
|
|
9117
9772
|
filesCounter: "{count}/{max}",
|
|
9118
9773
|
fromLibrary: "From library",
|
|
9119
9774
|
libraryEmpty: "Library is empty",
|
|
9120
9775
|
libraryHint: "Choose from previously uploaded files",
|
|
9776
|
+
dropToUpload: "Release to upload",
|
|
9777
|
+
replaceFile: "Replace",
|
|
9778
|
+
clearAll: "Clear all",
|
|
9121
9779
|
pickerError: "Failed to load files from library",
|
|
9122
9780
|
// Validation errors
|
|
9123
9781
|
required: "Required",
|
|
@@ -9183,12 +9841,16 @@ var defaultConfig = {
|
|
|
9183
9841
|
hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
|
|
9184
9842
|
fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
|
|
9185
9843
|
fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
9844
|
+
fileCountWithMax: "{count} / {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
9186
9845
|
fileCountRange: "({min}-{max})",
|
|
9187
9846
|
uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
|
|
9188
9847
|
filesCounter: "{count}/{max}",
|
|
9189
9848
|
fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
|
|
9190
9849
|
libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
|
|
9191
9850
|
libraryHint: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0437 \u0440\u0430\u043D\u0435\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
9851
|
+
dropToUpload: "\u041E\u0442\u043F\u0443\u0441\u0442\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C",
|
|
9852
|
+
replaceFile: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
9853
|
+
clearAll: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435",
|
|
9192
9854
|
pickerError: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u0438\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
|
|
9193
9855
|
// Validation errors
|
|
9194
9856
|
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
@@ -9253,7 +9915,9 @@ function createInstanceState(config) {
|
|
|
9253
9915
|
translations: mergedTranslations
|
|
9254
9916
|
},
|
|
9255
9917
|
debounceTimer: null,
|
|
9256
|
-
prefill: {}
|
|
9918
|
+
prefill: {},
|
|
9919
|
+
syntheticElementIds: /* @__PURE__ */ new WeakMap(),
|
|
9920
|
+
syntheticElementIdCounter: 0
|
|
9257
9921
|
};
|
|
9258
9922
|
}
|
|
9259
9923
|
function generateInstanceId() {
|
|
@@ -9327,10 +9991,10 @@ var defaultTheme = {
|
|
|
9327
9991
|
fileUploadHoverBorderColor: "#3b82f6",
|
|
9328
9992
|
// blue-500
|
|
9329
9993
|
// Spacing
|
|
9330
|
-
inputPaddingX: "0.
|
|
9331
|
-
//
|
|
9332
|
-
inputPaddingY: "0.
|
|
9333
|
-
//
|
|
9994
|
+
inputPaddingX: "0.5rem",
|
|
9995
|
+
// 8px (compact density v2)
|
|
9996
|
+
inputPaddingY: "0.25rem",
|
|
9997
|
+
// 4px (compact density v2)
|
|
9334
9998
|
borderRadius: "0.5rem",
|
|
9335
9999
|
// rounded-lg (8px)
|
|
9336
10000
|
borderWidth: "1px",
|
|
@@ -9530,6 +10194,11 @@ var componentRegistry = {
|
|
|
9530
10194
|
// Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
|
|
9531
10195
|
validate: validateHiddenElement,
|
|
9532
10196
|
update: updateHiddenField
|
|
10197
|
+
},
|
|
10198
|
+
markdown: {
|
|
10199
|
+
// Display-only element — no value, no errors, skip from form data
|
|
10200
|
+
validate: validateMarkdown,
|
|
10201
|
+
update: updateMarkdown
|
|
9533
10202
|
}
|
|
9534
10203
|
};
|
|
9535
10204
|
function getComponentOperations(elementType) {
|
|
@@ -9770,7 +10439,7 @@ var FormBuilderInstance = class {
|
|
|
9770
10439
|
existingContainer.remove();
|
|
9771
10440
|
}
|
|
9772
10441
|
const actionsContainer = document.createElement("div");
|
|
9773
|
-
actionsContainer.className = "form-level-actions-container mt-
|
|
10442
|
+
actionsContainer.className = "form-level-actions-container mt-3 pt-2 flex flex-wrap gap-2 justify-center";
|
|
9774
10443
|
actionsContainer.style.cssText = `
|
|
9775
10444
|
border-top: var(--fb-border-width) solid var(--fb-border-color);
|
|
9776
10445
|
`;
|
|
@@ -9911,7 +10580,7 @@ var FormBuilderInstance = class {
|
|
|
9911
10580
|
*/
|
|
9912
10581
|
createRootPrefillHints(hints) {
|
|
9913
10582
|
const hintsContainer = document.createElement("div");
|
|
9914
|
-
hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-
|
|
10583
|
+
hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
|
|
9915
10584
|
hints.forEach((hint) => {
|
|
9916
10585
|
const hintButton = document.createElement("button");
|
|
9917
10586
|
hintButton.type = "button";
|
|
@@ -9944,7 +10613,7 @@ var FormBuilderInstance = class {
|
|
|
9944
10613
|
root.setAttribute("data-fb-root", "true");
|
|
9945
10614
|
injectThemeVariables(root, this.state.config.theme);
|
|
9946
10615
|
const rootContainer = document.createElement("div");
|
|
9947
|
-
rootContainer.className = "space-y-
|
|
10616
|
+
rootContainer.className = "space-y-2";
|
|
9948
10617
|
if (schema.prefillHints && !this.state.config.readonly) {
|
|
9949
10618
|
const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
|
|
9950
10619
|
rootContainer.appendChild(hintsContainer);
|
|
@@ -9952,12 +10621,12 @@ var FormBuilderInstance = class {
|
|
|
9952
10621
|
const fieldsWrapper = document.createElement("div");
|
|
9953
10622
|
const columns = schema.columns || 1;
|
|
9954
10623
|
if (columns === 1) {
|
|
9955
|
-
fieldsWrapper.className = "space-y-
|
|
10624
|
+
fieldsWrapper.className = "space-y-2";
|
|
9956
10625
|
} else {
|
|
9957
|
-
fieldsWrapper.className = `grid grid-cols-${columns} gap-
|
|
10626
|
+
fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
|
|
9958
10627
|
}
|
|
9959
10628
|
schema.elements.forEach((element) => {
|
|
9960
|
-
if (element.hidden || element.type === "hidden") {
|
|
10629
|
+
if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
|
|
9961
10630
|
const val = prefill?.[element.key] ?? element.default ?? null;
|
|
9962
10631
|
fieldsWrapper.appendChild(createHiddenInput(element.key, val));
|
|
9963
10632
|
return;
|
|
@@ -9992,7 +10661,7 @@ var FormBuilderInstance = class {
|
|
|
9992
10661
|
const errors = [];
|
|
9993
10662
|
const data = {};
|
|
9994
10663
|
const validateElement2 = (element, ctx, customScopeRoot = null) => {
|
|
9995
|
-
const key = element.key;
|
|
10664
|
+
const key = element.key ?? "";
|
|
9996
10665
|
const scopeRoot = customScopeRoot || this.state.formRoot;
|
|
9997
10666
|
const componentContext = {
|
|
9998
10667
|
scopeRoot,
|
|
@@ -10010,7 +10679,8 @@ var FormBuilderInstance = class {
|
|
|
10010
10679
|
errors.push(...componentResult.errors);
|
|
10011
10680
|
return {
|
|
10012
10681
|
value: componentResult.value,
|
|
10013
|
-
spread: !!componentResult.spread
|
|
10682
|
+
spread: !!componentResult.spread,
|
|
10683
|
+
skip: !!componentResult.skip
|
|
10014
10684
|
};
|
|
10015
10685
|
}
|
|
10016
10686
|
console.warn(`Unknown field type "${element.type}" for key "${key}"`);
|
|
@@ -10031,6 +10701,9 @@ var FormBuilderInstance = class {
|
|
|
10031
10701
|
);
|
|
10032
10702
|
}
|
|
10033
10703
|
}
|
|
10704
|
+
if (element.type === "markdown") {
|
|
10705
|
+
return;
|
|
10706
|
+
}
|
|
10034
10707
|
if (element.hidden || element.type === "hidden") {
|
|
10035
10708
|
const hiddenInput = this.state.formRoot.querySelector(
|
|
10036
10709
|
`input[type="hidden"][data-hidden-field="true"][name="${element.key}"]`
|
|
@@ -10043,9 +10716,10 @@ var FormBuilderInstance = class {
|
|
|
10043
10716
|
}
|
|
10044
10717
|
} else {
|
|
10045
10718
|
const result = validateElement2(element, { path: "" });
|
|
10719
|
+
if (result.skip) return;
|
|
10046
10720
|
if (result.spread && result.value !== null && typeof result.value === "object") {
|
|
10047
10721
|
Object.assign(data, result.value);
|
|
10048
|
-
} else {
|
|
10722
|
+
} else if (element.key) {
|
|
10049
10723
|
data[element.key] = result.value;
|
|
10050
10724
|
}
|
|
10051
10725
|
}
|
|
@@ -10121,6 +10795,7 @@ var FormBuilderInstance = class {
|
|
|
10121
10795
|
buildHiddenFieldsData(elements) {
|
|
10122
10796
|
const data = {};
|
|
10123
10797
|
for (const element of elements) {
|
|
10798
|
+
if (element.type === "markdown" || !element.key) continue;
|
|
10124
10799
|
const key = element.key;
|
|
10125
10800
|
if (element.hidden && element.default !== void 0) {
|
|
10126
10801
|
data[key] = element.default;
|
|
@@ -10241,6 +10916,71 @@ var FormBuilderInstance = class {
|
|
|
10241
10916
|
);
|
|
10242
10917
|
}
|
|
10243
10918
|
}
|
|
10919
|
+
/**
|
|
10920
|
+
* Find the field wrapper DOM element for a given element+path combo.
|
|
10921
|
+
* Used by reevaluateConditionalFields.
|
|
10922
|
+
*/
|
|
10923
|
+
findFieldWrapper(element, currentPath) {
|
|
10924
|
+
const formRoot = this.state.formRoot;
|
|
10925
|
+
const lookupKey = getElementLookupKey(element, this.state);
|
|
10926
|
+
if (!currentPath) {
|
|
10927
|
+
return formRoot.querySelector(`[data-field-key="${lookupKey}"]`);
|
|
10928
|
+
}
|
|
10929
|
+
const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
|
|
10930
|
+
if (pathMatch) {
|
|
10931
|
+
const containerEl2 = formRoot.querySelector(
|
|
10932
|
+
`[data-container-item="${pathMatch[1]}[${pathMatch[2]}]"]`
|
|
10933
|
+
);
|
|
10934
|
+
return containerEl2 ? containerEl2.querySelector(`[data-field-key="${lookupKey}"]`) : null;
|
|
10935
|
+
}
|
|
10936
|
+
const containerEl = formRoot.querySelector(
|
|
10937
|
+
`[data-container="${currentPath}"]`
|
|
10938
|
+
);
|
|
10939
|
+
return containerEl ? containerEl.querySelector(`[data-field-key="${lookupKey}"]`) : null;
|
|
10940
|
+
}
|
|
10941
|
+
/**
|
|
10942
|
+
* Apply enableIf show/hide logic to a single field wrapper.
|
|
10943
|
+
* Extracted to reduce cyclomatic complexity of checkElements.
|
|
10944
|
+
*/
|
|
10945
|
+
applyEnableIfVisibility(element, wrapper, currentPath, fullPath, formData) {
|
|
10946
|
+
try {
|
|
10947
|
+
const scope = element.enableIf.scope ?? "relative";
|
|
10948
|
+
const containerData = scope === "relative" && currentPath ? getValueByPath(formData, currentPath) : void 0;
|
|
10949
|
+
const shouldEnable = evaluateEnableCondition(
|
|
10950
|
+
element.enableIf,
|
|
10951
|
+
formData,
|
|
10952
|
+
containerData
|
|
10953
|
+
);
|
|
10954
|
+
const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
|
|
10955
|
+
if (shouldEnable && isCurrentlyDisabled) {
|
|
10956
|
+
const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
|
|
10957
|
+
const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
|
|
10958
|
+
const newWrapper = renderElement2(element, {
|
|
10959
|
+
path: currentPath,
|
|
10960
|
+
prefill: prefillContext,
|
|
10961
|
+
formData,
|
|
10962
|
+
state: this.state,
|
|
10963
|
+
instance: this
|
|
10964
|
+
});
|
|
10965
|
+
wrapper.parentNode?.replaceChild(newWrapper, wrapper);
|
|
10966
|
+
} else if (!shouldEnable && !isCurrentlyDisabled) {
|
|
10967
|
+
const disabledWrapper = document.createElement("div");
|
|
10968
|
+
disabledWrapper.className = "fb-field-wrapper-disabled";
|
|
10969
|
+
disabledWrapper.style.display = "none";
|
|
10970
|
+
disabledWrapper.setAttribute(
|
|
10971
|
+
"data-field-key",
|
|
10972
|
+
getElementLookupKey(element, this.state)
|
|
10973
|
+
);
|
|
10974
|
+
disabledWrapper.setAttribute("data-conditionally-disabled", "true");
|
|
10975
|
+
wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
|
|
10976
|
+
}
|
|
10977
|
+
} catch (error) {
|
|
10978
|
+
console.error(
|
|
10979
|
+
`Error re-evaluating enableIf for field "${element.key ?? "<no key>"}" at path "${fullPath}":`,
|
|
10980
|
+
error
|
|
10981
|
+
);
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10244
10984
|
/**
|
|
10245
10985
|
* Re-evaluate all conditional fields (enableIf) based on current form data
|
|
10246
10986
|
* This is called automatically when form data changes (via onChange events)
|
|
@@ -10250,97 +10990,31 @@ var FormBuilderInstance = class {
|
|
|
10250
10990
|
const formData = this.validateForm(true).data;
|
|
10251
10991
|
const checkElements = (elements, currentPath) => {
|
|
10252
10992
|
elements.forEach((element) => {
|
|
10253
|
-
const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
|
|
10993
|
+
const fullPath = currentPath ? `${currentPath}.${element.key ?? ""}` : element.key ?? "";
|
|
10254
10994
|
if (element.enableIf) {
|
|
10255
|
-
|
|
10256
|
-
if (currentPath) {
|
|
10257
|
-
const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
|
|
10258
|
-
if (pathMatch) {
|
|
10259
|
-
const containerKey = pathMatch[1];
|
|
10260
|
-
const containerIndex = pathMatch[2];
|
|
10261
|
-
const containerElement = this.state.formRoot.querySelector(
|
|
10262
|
-
`[data-container-item="${containerKey}[${containerIndex}]"]`
|
|
10263
|
-
);
|
|
10264
|
-
if (containerElement) {
|
|
10265
|
-
fieldWrapper = containerElement.querySelector(
|
|
10266
|
-
`[data-field-key="${element.key}"]`
|
|
10267
|
-
);
|
|
10268
|
-
}
|
|
10269
|
-
} else {
|
|
10270
|
-
const containerElement = this.state.formRoot.querySelector(
|
|
10271
|
-
`[data-container="${currentPath}"]`
|
|
10272
|
-
);
|
|
10273
|
-
if (containerElement) {
|
|
10274
|
-
fieldWrapper = containerElement.querySelector(
|
|
10275
|
-
`[data-field-key="${element.key}"]`
|
|
10276
|
-
);
|
|
10277
|
-
}
|
|
10278
|
-
}
|
|
10279
|
-
} else {
|
|
10280
|
-
fieldWrapper = this.state.formRoot.querySelector(
|
|
10281
|
-
`[data-field-key="${element.key}"]`
|
|
10282
|
-
);
|
|
10283
|
-
}
|
|
10995
|
+
const fieldWrapper = this.findFieldWrapper(element, currentPath);
|
|
10284
10996
|
if (fieldWrapper) {
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
const shouldEnable = evaluateEnableCondition(
|
|
10293
|
-
element.enableIf,
|
|
10294
|
-
formData,
|
|
10295
|
-
// Use complete formData for absolute scope
|
|
10296
|
-
containerData
|
|
10297
|
-
// Use container-specific data for relative scope
|
|
10298
|
-
);
|
|
10299
|
-
const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
|
|
10300
|
-
if (shouldEnable && isCurrentlyDisabled) {
|
|
10301
|
-
const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
|
|
10302
|
-
const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
|
|
10303
|
-
const newWrapper = renderElement2(element, {
|
|
10304
|
-
path: currentPath,
|
|
10305
|
-
// Use container path (empty string for root-level)
|
|
10306
|
-
prefill: prefillContext,
|
|
10307
|
-
formData,
|
|
10308
|
-
// Pass complete formData for enableIf evaluation
|
|
10309
|
-
state: this.state,
|
|
10310
|
-
instance: this
|
|
10311
|
-
});
|
|
10312
|
-
wrapper.parentNode?.replaceChild(newWrapper, wrapper);
|
|
10313
|
-
} else if (!shouldEnable && !isCurrentlyDisabled) {
|
|
10314
|
-
const disabledWrapper = document.createElement("div");
|
|
10315
|
-
disabledWrapper.className = "fb-field-wrapper-disabled";
|
|
10316
|
-
disabledWrapper.style.display = "none";
|
|
10317
|
-
disabledWrapper.setAttribute("data-field-key", element.key);
|
|
10318
|
-
disabledWrapper.setAttribute(
|
|
10319
|
-
"data-conditionally-disabled",
|
|
10320
|
-
"true"
|
|
10321
|
-
);
|
|
10322
|
-
wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
|
|
10323
|
-
}
|
|
10324
|
-
} catch (error) {
|
|
10325
|
-
console.error(
|
|
10326
|
-
`Error re-evaluating enableIf for field "${element.key}" at path "${fullPath}":`,
|
|
10327
|
-
error
|
|
10328
|
-
);
|
|
10329
|
-
}
|
|
10997
|
+
this.applyEnableIfVisibility(
|
|
10998
|
+
element,
|
|
10999
|
+
fieldWrapper,
|
|
11000
|
+
currentPath,
|
|
11001
|
+
fullPath,
|
|
11002
|
+
formData
|
|
11003
|
+
);
|
|
10330
11004
|
}
|
|
10331
11005
|
}
|
|
10332
11006
|
if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
|
|
10333
|
-
const containerData = formData?.[element.key];
|
|
11007
|
+
const containerData = element.key ? formData?.[element.key] : void 0;
|
|
10334
11008
|
if (Array.isArray(containerData)) {
|
|
10335
11009
|
const containerItems = this.state.formRoot.querySelectorAll(
|
|
10336
11010
|
`[data-container-item]`
|
|
10337
11011
|
);
|
|
10338
|
-
const directItems = Array.from(containerItems).filter((el) => {
|
|
11012
|
+
const directItems = fullPath ? Array.from(containerItems).filter((el) => {
|
|
10339
11013
|
const attr = el.getAttribute("data-container-item") || "";
|
|
10340
11014
|
if (!attr.startsWith(`${fullPath}[`)) return false;
|
|
10341
11015
|
const suffix = attr.slice(fullPath.length);
|
|
10342
11016
|
return /^\[\d+\]$/.test(suffix);
|
|
10343
|
-
});
|
|
11017
|
+
}) : [];
|
|
10344
11018
|
directItems.forEach((el) => {
|
|
10345
11019
|
const attr = el.getAttribute("data-container-item") || "";
|
|
10346
11020
|
checkElements(element.elements, attr);
|