bard-attachment_field 0.2.4 → 0.3.0
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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/input-attachment.js +97 -105
- data/input-attachment/package.json +8 -2
- data/input-attachment/src/components/attachment-file/direct-upload-controller.tsx +8 -4
- data/input-attachment/src/components/input-attachment/form-controller.spec.ts +63 -0
- data/input-attachment/src/components/input-attachment/form-controller.tsx +61 -42
- data/input-attachment/src/components/input-attachment/input-attachment.tsx +2 -1
- data/input-attachment/src/components/input-attachment/readme.md +11 -10
- data/input-attachment/src/components.d.ts +8 -0
- data/lib/bard/attachment_field/cucumber.rb +16 -0
- data/lib/bard/attachment_field/field.rb +3 -0
- data/lib/bard/attachment_field/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77372a4e55bd97d9a4c52f8089427717d9181d225d58b18814194a938a1c1631
|
|
4
|
+
data.tar.gz: 2eaa2f26f3a3f6fbf57523527923e21694596a13ab1de7968f4c401a5d368f88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd9cb5858441b97a1fde1189fad046b5c0d1295f5da2b7d5134bd84113cc9be32e419217a910ce716ce2702ec71d40586a1bf59bbd0cfa2fdeed41d0eafd0b57
|
|
7
|
+
data.tar.gz: 4881721450377ab4e55467c6ea7dae84ca371186a66ef3f77d50ffcbac921204d8dab282a0668a0997c5b5c36c9fae1d5136f33d4c530980bc5b279c2a25c4b5
|
|
@@ -4864,22 +4864,22 @@ var DirectUploadController2 = class {
|
|
|
4864
4864
|
recordXHR;
|
|
4865
4865
|
uploadXHR;
|
|
4866
4866
|
callback = null;
|
|
4867
|
+
cancelled = false;
|
|
4868
|
+
completed = false;
|
|
4867
4869
|
constructor(uploadedFile, file) {
|
|
4868
4870
|
this.uploadedFile = uploadedFile;
|
|
4869
4871
|
this.file = file;
|
|
4870
4872
|
this.directUpload = new DirectUpload(this.file, this.uploadedFile.url, this);
|
|
4871
4873
|
}
|
|
4872
4874
|
cancel() {
|
|
4873
|
-
this.
|
|
4875
|
+
this.cancelled = true;
|
|
4874
4876
|
this.abortXHR(this.recordXHR);
|
|
4875
4877
|
this.abortXHR(this.uploadXHR);
|
|
4878
|
+
this.complete("aborted", {});
|
|
4876
4879
|
}
|
|
4877
4880
|
abortXHR(xhr) {
|
|
4878
4881
|
if (!xhr)
|
|
4879
4882
|
return;
|
|
4880
|
-
xhr.addEventListener("abort", () => {
|
|
4881
|
-
this.complete("aborted", {});
|
|
4882
|
-
});
|
|
4883
4883
|
xhr.abort();
|
|
4884
4884
|
}
|
|
4885
4885
|
start(callback) {
|
|
@@ -4890,6 +4890,9 @@ var DirectUploadController2 = class {
|
|
|
4890
4890
|
});
|
|
4891
4891
|
}
|
|
4892
4892
|
complete(error, _attributes) {
|
|
4893
|
+
if (this.completed)
|
|
4894
|
+
return;
|
|
4895
|
+
this.completed = true;
|
|
4893
4896
|
if (error) {
|
|
4894
4897
|
this.dispatchError(error);
|
|
4895
4898
|
}
|
|
@@ -4920,12 +4923,22 @@ var DirectUploadController2 = class {
|
|
|
4920
4923
|
}
|
|
4921
4924
|
directUploadWillCreateBlobWithXHR(xhr) {
|
|
4922
4925
|
this.recordXHR = xhr;
|
|
4926
|
+
if (this.cancelled) {
|
|
4927
|
+
xhr.send = () => {
|
|
4928
|
+
};
|
|
4929
|
+
return;
|
|
4930
|
+
}
|
|
4923
4931
|
this.dispatch("before-blob-request", {
|
|
4924
4932
|
xhr
|
|
4925
4933
|
});
|
|
4926
4934
|
}
|
|
4927
4935
|
directUploadWillStoreFileWithXHR(xhr) {
|
|
4928
4936
|
this.uploadXHR = xhr;
|
|
4937
|
+
if (this.cancelled) {
|
|
4938
|
+
xhr.send = () => {
|
|
4939
|
+
};
|
|
4940
|
+
return;
|
|
4941
|
+
}
|
|
4929
4942
|
this.uploadedFile.value = this.recordXHR.response.signed_id;
|
|
4930
4943
|
this.dispatch("before-storage-request", {
|
|
4931
4944
|
xhr
|
|
@@ -5070,9 +5083,6 @@ var FetchResponse = class {
|
|
|
5070
5083
|
get isTurboStream() {
|
|
5071
5084
|
return this.contentType.match(/^text\/vnd\.turbo-stream\.html/);
|
|
5072
5085
|
}
|
|
5073
|
-
get isScript() {
|
|
5074
|
-
return this.contentType.match(/\b(?:java|ecma)script\b/);
|
|
5075
|
-
}
|
|
5076
5086
|
async renderTurboStream() {
|
|
5077
5087
|
if (this.isTurboStream) {
|
|
5078
5088
|
if (window.Turbo) {
|
|
@@ -5084,22 +5094,6 @@ var FetchResponse = class {
|
|
|
5084
5094
|
return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
|
|
5085
5095
|
}
|
|
5086
5096
|
}
|
|
5087
|
-
async activeScript() {
|
|
5088
|
-
if (this.isScript) {
|
|
5089
|
-
const script = document.createElement("script");
|
|
5090
|
-
const metaTag = document.querySelector("meta[name=csp-nonce]");
|
|
5091
|
-
if (metaTag) {
|
|
5092
|
-
const nonce = metaTag.nonce === "" ? metaTag.content : metaTag.nonce;
|
|
5093
|
-
if (nonce) {
|
|
5094
|
-
script.setAttribute("nonce", nonce);
|
|
5095
|
-
}
|
|
5096
|
-
}
|
|
5097
|
-
script.innerHTML = await this.text;
|
|
5098
|
-
document.body.appendChild(script);
|
|
5099
|
-
} else {
|
|
5100
|
-
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`));
|
|
5101
|
-
}
|
|
5102
|
-
}
|
|
5103
5097
|
};
|
|
5104
5098
|
var RequestInterceptor = class {
|
|
5105
5099
|
static register(interceptor) {
|
|
@@ -5145,7 +5139,7 @@ function stringEntriesFromFormData(formData) {
|
|
|
5145
5139
|
function mergeEntries(searchParams, entries) {
|
|
5146
5140
|
for (const [name, value] of entries) {
|
|
5147
5141
|
if (value instanceof window.File) continue;
|
|
5148
|
-
if (searchParams.has(name)
|
|
5142
|
+
if (searchParams.has(name)) {
|
|
5149
5143
|
searchParams.delete(name);
|
|
5150
5144
|
searchParams.set(name, value);
|
|
5151
5145
|
} else {
|
|
@@ -5168,16 +5162,11 @@ var FetchRequest = class {
|
|
|
5168
5162
|
} catch (error) {
|
|
5169
5163
|
console.error(error);
|
|
5170
5164
|
}
|
|
5171
|
-
const
|
|
5172
|
-
const response = new FetchResponse(await fetch(this.url, this.fetchOptions));
|
|
5165
|
+
const response = new FetchResponse(await window.fetch(this.url, this.fetchOptions));
|
|
5173
5166
|
if (response.unauthenticated && response.authenticationURL) {
|
|
5174
5167
|
return Promise.reject(window.location.href = response.authenticationURL);
|
|
5175
5168
|
}
|
|
5176
|
-
if (response.
|
|
5177
|
-
await response.activeScript();
|
|
5178
|
-
}
|
|
5179
|
-
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
|
|
5180
|
-
if (responseStatusIsTurboStreamable && response.isTurboStream) {
|
|
5169
|
+
if (response.ok && response.isTurboStream) {
|
|
5181
5170
|
await response.renderTurboStream();
|
|
5182
5171
|
}
|
|
5183
5172
|
return response;
|
|
@@ -5187,38 +5176,27 @@ var FetchRequest = class {
|
|
|
5187
5176
|
headers[key] = value;
|
|
5188
5177
|
this.options.headers = headers;
|
|
5189
5178
|
}
|
|
5190
|
-
sameHostname() {
|
|
5191
|
-
if (!this.originalUrl.startsWith("http:") && !this.originalUrl.startsWith("https:")) {
|
|
5192
|
-
return true;
|
|
5193
|
-
}
|
|
5194
|
-
try {
|
|
5195
|
-
return new URL(this.originalUrl).hostname === window.location.hostname;
|
|
5196
|
-
} catch (_) {
|
|
5197
|
-
return true;
|
|
5198
|
-
}
|
|
5199
|
-
}
|
|
5200
5179
|
get fetchOptions() {
|
|
5201
5180
|
return {
|
|
5202
5181
|
method: this.method.toUpperCase(),
|
|
5203
5182
|
headers: this.headers,
|
|
5204
5183
|
body: this.formattedBody,
|
|
5205
5184
|
signal: this.signal,
|
|
5206
|
-
credentials:
|
|
5207
|
-
redirect: this.redirect
|
|
5208
|
-
keepalive: this.keepalive
|
|
5185
|
+
credentials: "same-origin",
|
|
5186
|
+
redirect: this.redirect
|
|
5209
5187
|
};
|
|
5210
5188
|
}
|
|
5211
5189
|
get headers() {
|
|
5212
|
-
const baseHeaders = {
|
|
5213
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
5214
|
-
"Content-Type": this.contentType,
|
|
5215
|
-
Accept: this.accept
|
|
5216
|
-
};
|
|
5217
|
-
if (this.sameHostname()) {
|
|
5218
|
-
baseHeaders["X-CSRF-Token"] = this.csrfToken;
|
|
5219
|
-
}
|
|
5220
5190
|
return compact(
|
|
5221
|
-
Object.assign(
|
|
5191
|
+
Object.assign(
|
|
5192
|
+
{
|
|
5193
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
5194
|
+
"X-CSRF-Token": this.csrfToken,
|
|
5195
|
+
"Content-Type": this.contentType,
|
|
5196
|
+
Accept: this.accept
|
|
5197
|
+
},
|
|
5198
|
+
this.additionalHeaders
|
|
5199
|
+
)
|
|
5222
5200
|
);
|
|
5223
5201
|
}
|
|
5224
5202
|
get csrfToken() {
|
|
@@ -5242,8 +5220,6 @@ var FetchRequest = class {
|
|
|
5242
5220
|
return "text/vnd.turbo-stream.html, text/html, application/xhtml+xml";
|
|
5243
5221
|
case "json":
|
|
5244
5222
|
return "application/json, application/vnd.api+json";
|
|
5245
|
-
case "script":
|
|
5246
|
-
return "text/javascript, application/javascript";
|
|
5247
5223
|
default:
|
|
5248
5224
|
return "*/*";
|
|
5249
5225
|
}
|
|
@@ -5278,12 +5254,6 @@ var FetchRequest = class {
|
|
|
5278
5254
|
get redirect() {
|
|
5279
5255
|
return this.options.redirect || "follow";
|
|
5280
5256
|
}
|
|
5281
|
-
get credentials() {
|
|
5282
|
-
return this.options.credentials || "same-origin";
|
|
5283
|
-
}
|
|
5284
|
-
get keepalive() {
|
|
5285
|
-
return this.options.keepalive || false;
|
|
5286
|
-
}
|
|
5287
5257
|
get additionalHeaders() {
|
|
5288
5258
|
return this.options.headers || {};
|
|
5289
5259
|
}
|
|
@@ -5468,8 +5438,8 @@ var AttachmentPreview3 = AttachmentPreview;
|
|
|
5468
5438
|
|
|
5469
5439
|
// dist/components/input-attachment.js
|
|
5470
5440
|
var FormController = class _FormController {
|
|
5471
|
-
static instance(form) {
|
|
5472
|
-
return form.inputAttachmentFormController ||= new _FormController(form);
|
|
5441
|
+
static instance(form, options = {}) {
|
|
5442
|
+
return form.inputAttachmentFormController ||= new _FormController(form, options);
|
|
5473
5443
|
}
|
|
5474
5444
|
progressContainerTarget;
|
|
5475
5445
|
dialog;
|
|
@@ -5478,25 +5448,25 @@ var FormController = class _FormController {
|
|
|
5478
5448
|
controllers;
|
|
5479
5449
|
submitted;
|
|
5480
5450
|
processing;
|
|
5481
|
-
constructor(form) {
|
|
5451
|
+
constructor(form, { uploadDialog = true } = {}) {
|
|
5482
5452
|
this.element = form;
|
|
5483
5453
|
this.progressTargetMap = {};
|
|
5484
5454
|
this.controllers = [];
|
|
5485
5455
|
this.submitted = false;
|
|
5486
5456
|
this.processing = false;
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
<div class="direct-upload-
|
|
5490
|
-
<
|
|
5491
|
-
|
|
5457
|
+
if (uploadDialog) {
|
|
5458
|
+
this.element.insertAdjacentHTML("beforeend", `<dialog id="form-controller-dialog">
|
|
5459
|
+
<div class="direct-upload-wrapper">
|
|
5460
|
+
<div class="direct-upload-content">
|
|
5461
|
+
<h3>Uploading your media</h3>
|
|
5462
|
+
<div id="progress-container"></div>
|
|
5463
|
+
</div>
|
|
5492
5464
|
</div>
|
|
5493
|
-
</
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
this.progressContainerTarget = this.dialog.querySelector("#progress-container");
|
|
5497
|
-
if (this.element.dataset.remote !== "true" && (this.element.dataset.turbo == "false" || !window.Turbo?.session?.enabled)) {
|
|
5498
|
-
this.element.addEventListener("submit", (event) => this.submit(event));
|
|
5465
|
+
</dialog>`);
|
|
5466
|
+
this.dialog = this.element.querySelector("#form-controller-dialog");
|
|
5467
|
+
this.progressContainerTarget = this.dialog.querySelector("#progress-container");
|
|
5499
5468
|
}
|
|
5469
|
+
this.element.addEventListener("submit", (event) => this.submit(event));
|
|
5500
5470
|
window.addEventListener("beforeunload", (event) => this.beforeUnload(event));
|
|
5501
5471
|
this.element.addEventListener("direct-upload:initialize", (event) => this.init(event));
|
|
5502
5472
|
this.element.addEventListener("direct-upload:start", (event) => this.start(event));
|
|
@@ -5512,13 +5482,14 @@ var FormController = class _FormController {
|
|
|
5512
5482
|
}
|
|
5513
5483
|
}
|
|
5514
5484
|
submit(event) {
|
|
5515
|
-
if (this.controllers.length === 0 && !this.hasUploadErrors())
|
|
5485
|
+
if (this.controllers.length === 0 && !this.hasUploadErrors() && !this.processing)
|
|
5516
5486
|
return;
|
|
5517
5487
|
event.preventDefault();
|
|
5518
5488
|
this.submitted = true;
|
|
5489
|
+
this.setInputAttachmentsDisabled(true);
|
|
5519
5490
|
this.startNextController();
|
|
5520
5491
|
if (this.processing) {
|
|
5521
|
-
this.dialog
|
|
5492
|
+
this.dialog?.showModal();
|
|
5522
5493
|
}
|
|
5523
5494
|
}
|
|
5524
5495
|
startNextController() {
|
|
@@ -5527,10 +5498,18 @@ var FormController = class _FormController {
|
|
|
5527
5498
|
const controller = this.controllers.shift();
|
|
5528
5499
|
if (controller) {
|
|
5529
5500
|
this.processing = true;
|
|
5530
|
-
this.
|
|
5501
|
+
if (this.submitted) {
|
|
5502
|
+
this.setInputAttachmentsDisabled(true);
|
|
5503
|
+
} else {
|
|
5504
|
+
this.setControllerInputDisabled(controller, true);
|
|
5505
|
+
}
|
|
5531
5506
|
controller.start((error) => {
|
|
5532
|
-
if (
|
|
5533
|
-
|
|
5507
|
+
if (this.submitted) {
|
|
5508
|
+
if (error) {
|
|
5509
|
+
this.setInputAttachmentsDisabled(false);
|
|
5510
|
+
}
|
|
5511
|
+
} else {
|
|
5512
|
+
this.setControllerInputDisabled(controller, false);
|
|
5534
5513
|
}
|
|
5535
5514
|
this.processing = false;
|
|
5536
5515
|
this.startNextController();
|
|
@@ -5543,16 +5522,22 @@ var FormController = class _FormController {
|
|
|
5543
5522
|
return Array.from(this.element.querySelectorAll("attachment-file")).some((el) => el.state === "error");
|
|
5544
5523
|
}
|
|
5545
5524
|
submitForm() {
|
|
5546
|
-
if (this.submitted)
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5525
|
+
if (!this.submitted)
|
|
5526
|
+
return;
|
|
5527
|
+
if (this.hasUploadErrors()) {
|
|
5528
|
+
this.dialog?.close();
|
|
5529
|
+
this.setInputAttachmentsDisabled(false);
|
|
5530
|
+
return;
|
|
5531
|
+
}
|
|
5532
|
+
this.setInputAttachmentsDisabled(true);
|
|
5533
|
+
requestAnimationFrame(() => {
|
|
5534
|
+
this.element.submit();
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
setControllerInputDisabled(controller, disabled) {
|
|
5538
|
+
const inputAttachment = controller.uploadedFile.closest("input-attachment");
|
|
5539
|
+
if (inputAttachment) {
|
|
5540
|
+
inputAttachment.disabled = disabled;
|
|
5556
5541
|
}
|
|
5557
5542
|
}
|
|
5558
5543
|
setInputAttachmentsDisabled(disabled) {
|
|
@@ -5562,36 +5547,41 @@ var FormController = class _FormController {
|
|
|
5562
5547
|
}
|
|
5563
5548
|
init(event) {
|
|
5564
5549
|
const { id: id2, file, controller } = event.detail;
|
|
5565
|
-
this.progressContainerTarget
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5550
|
+
if (this.progressContainerTarget) {
|
|
5551
|
+
this.progressContainerTarget.insertAdjacentHTML("beforebegin", `
|
|
5552
|
+
<progress-bar id="direct-upload-${id2}" class="direct-upload--pending">${file?.name || "Uploading..."}</progress-bar>
|
|
5553
|
+
`);
|
|
5554
|
+
this.progressTargetMap[id2] = document.getElementById(`direct-upload-${id2}`);
|
|
5555
|
+
}
|
|
5570
5556
|
this.controllers.push(controller);
|
|
5571
5557
|
this.startNextController();
|
|
5572
5558
|
}
|
|
5573
5559
|
start(event) {
|
|
5574
|
-
this.progressTargetMap[event.detail.id]
|
|
5560
|
+
this.progressTargetMap[event.detail.id]?.classList.remove("direct-upload--pending");
|
|
5575
5561
|
}
|
|
5576
5562
|
progress(event) {
|
|
5577
5563
|
const { id: id2, progress } = event.detail;
|
|
5578
|
-
this.progressTargetMap[id2]
|
|
5564
|
+
const target = this.progressTargetMap[id2];
|
|
5565
|
+
if (target)
|
|
5566
|
+
target.percent = progress;
|
|
5579
5567
|
}
|
|
5580
5568
|
error(event) {
|
|
5581
5569
|
event.preventDefault();
|
|
5582
5570
|
const { id: id2, error } = event.detail;
|
|
5583
5571
|
const target = this.progressTargetMap[id2];
|
|
5584
|
-
target
|
|
5585
|
-
|
|
5572
|
+
if (target) {
|
|
5573
|
+
target.classList.add("direct-upload--error");
|
|
5574
|
+
target.title = error;
|
|
5575
|
+
}
|
|
5586
5576
|
}
|
|
5587
5577
|
end(event) {
|
|
5588
|
-
this.progressTargetMap[event.detail.id]
|
|
5578
|
+
this.progressTargetMap[event.detail.id]?.classList.add("direct-upload--complete");
|
|
5589
5579
|
}
|
|
5590
5580
|
removeUploadedFile(event) {
|
|
5591
5581
|
const uploadedFile = event.detail;
|
|
5592
5582
|
const id2 = uploadedFile.controller?.directUpload?.id;
|
|
5593
5583
|
if (id2) {
|
|
5594
|
-
document.getElementById(`direct-upload-${id2}`)
|
|
5584
|
+
document.getElementById(`direct-upload-${id2}`)?.remove();
|
|
5595
5585
|
delete this.progressTargetMap[id2];
|
|
5596
5586
|
}
|
|
5597
5587
|
this.setInputAttachmentsDisabled(false);
|
|
@@ -5776,6 +5766,7 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
5776
5766
|
max;
|
|
5777
5767
|
preview = true;
|
|
5778
5768
|
disabled = false;
|
|
5769
|
+
uploadDialog = true;
|
|
5779
5770
|
form;
|
|
5780
5771
|
internals;
|
|
5781
5772
|
fileInput;
|
|
@@ -5792,7 +5783,7 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
5792
5783
|
this.form = this.internals.form;
|
|
5793
5784
|
if (this.form) {
|
|
5794
5785
|
this.form.addEventListener("reset", () => this.reset());
|
|
5795
|
-
FormController.instance(this.form);
|
|
5786
|
+
FormController.instance(this.form, { uploadDialog: this.uploadDialog });
|
|
5796
5787
|
}
|
|
5797
5788
|
const existingFiles = Array.from(this.el.children).filter((e) => e.tagName == "ATTACHMENT-FILE");
|
|
5798
5789
|
if (existingFiles.length > 0)
|
|
@@ -5965,12 +5956,12 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
5965
5956
|
return this.disabled || !!this.el.closest("fieldset[disabled]");
|
|
5966
5957
|
}
|
|
5967
5958
|
render() {
|
|
5968
|
-
return h(Host, { key: "
|
|
5959
|
+
return h(Host, { key: "e63c2624a4d88232adacc7d1610983fac89eb9db" }, h("input", { key: "baea544d4d62e31e1228d92ba8b0356d40e75ce5", ref: (el) => this.fileInput = el, type: "file", multiple: this.multiple, accept: this.accepts, required: this.required && this.files.length === 0, disabled: this.isDisabled, onChange: () => this.handleFileInputChange(), style: {
|
|
5969
5960
|
opacity: "0.01",
|
|
5970
5961
|
width: "1px",
|
|
5971
5962
|
height: "1px",
|
|
5972
5963
|
zIndex: "-999"
|
|
5973
|
-
} }), h("file-drop", { key: "
|
|
5964
|
+
} }), h("file-drop", { key: "0b5621010445988462b8056cb360bf18ac6b0b59", onClick: () => this.fileInput?.click(), onDrop: this.handleDrop }, h("p", { key: "718fe6b91ed9cdc5c5a2b54317fad8f1f25b3cd4", part: "title" }, h("strong", { key: "f28b9ffc58bb086bb1941ee01e943fe5068482a4" }, "Choose ", this.multiple ? "files" : "file", " "), h("span", { key: "eeca7b2f619a7a4d7863f98ba37218d6a66fdcfd" }, "or drag ", this.multiple ? "them" : "it", " here.")), h("div", { key: "2a1d0a824139f5c26833611a5523354965be0d9d", class: `media-preview ${this.multiple ? "-stacked" : ""}` }, h("slot", { key: "59455a857d01dfabd7aa5e697616f4be72cbbb20" }))));
|
|
5974
5965
|
}
|
|
5975
5966
|
componentDidRender() {
|
|
5976
5967
|
if (this.files.length === 0) {
|
|
@@ -6046,7 +6037,8 @@ var InputAttachment$1 = /* @__PURE__ */ proxyCustomElement(class InputAttachment
|
|
|
6046
6037
|
"accepts": [1],
|
|
6047
6038
|
"max": [2],
|
|
6048
6039
|
"preview": [4],
|
|
6049
|
-
"disabled": [4]
|
|
6040
|
+
"disabled": [4],
|
|
6041
|
+
"uploadDialog": [4, "upload-dialog"]
|
|
6050
6042
|
}, [[0, "attachment-file:remove", "removeUploadedFile"], [0, "attachment-file:validation", "handleChildValidation"], [0, "attachment-file:ready", "handleChildReady"], [0, "direct-upload:end", "fireChangeEvent"]]]);
|
|
6051
6043
|
var InputAttachment2 = InputAttachment$1;
|
|
6052
6044
|
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
},
|
|
22
22
|
"jest": {
|
|
23
23
|
"testEnvironment": "jsdom",
|
|
24
|
-
"extensionsToTreatAsEsm": [
|
|
24
|
+
"extensionsToTreatAsEsm": [
|
|
25
|
+
".ts",
|
|
26
|
+
".tsx"
|
|
27
|
+
],
|
|
25
28
|
"globals": {
|
|
26
29
|
"ts-jest": {
|
|
27
30
|
"useESM": true
|
|
@@ -53,5 +56,8 @@
|
|
|
53
56
|
"import": "./dist/components/index.js"
|
|
54
57
|
}
|
|
55
58
|
},
|
|
56
|
-
"license": "MIT"
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"volta": {
|
|
61
|
+
"node": "22.22.1"
|
|
62
|
+
}
|
|
57
63
|
}
|
|
@@ -8,6 +8,8 @@ export default class DirectUploadController {
|
|
|
8
8
|
recordXHR: XMLHttpRequest
|
|
9
9
|
uploadXHR: XMLHttpRequest
|
|
10
10
|
callback = null
|
|
11
|
+
cancelled = false
|
|
12
|
+
completed = false
|
|
11
13
|
|
|
12
14
|
constructor(uploadedFile, file: File) {
|
|
13
15
|
this.uploadedFile = uploadedFile
|
|
@@ -16,16 +18,14 @@ export default class DirectUploadController {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
cancel() {
|
|
19
|
-
this.
|
|
21
|
+
this.cancelled = true
|
|
20
22
|
this.abortXHR(this.recordXHR)
|
|
21
23
|
this.abortXHR(this.uploadXHR)
|
|
24
|
+
this.complete("aborted", {})
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
abortXHR(xhr) {
|
|
25
28
|
if(!xhr) return
|
|
26
|
-
xhr.addEventListener("abort", () => {
|
|
27
|
-
this.complete("aborted", {})
|
|
28
|
-
})
|
|
29
29
|
xhr.abort()
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -38,6 +38,8 @@ export default class DirectUploadController {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
complete(error, _attributes) {
|
|
41
|
+
if (this.completed) return
|
|
42
|
+
this.completed = true
|
|
41
43
|
if (error) {
|
|
42
44
|
this.dispatchError(error)
|
|
43
45
|
}
|
|
@@ -69,12 +71,14 @@ export default class DirectUploadController {
|
|
|
69
71
|
}
|
|
70
72
|
directUploadWillCreateBlobWithXHR(xhr) {
|
|
71
73
|
this.recordXHR = xhr
|
|
74
|
+
if (this.cancelled) { xhr.send = () => {}; return }
|
|
72
75
|
this.dispatch("before-blob-request", {
|
|
73
76
|
xhr: xhr
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
directUploadWillStoreFileWithXHR(xhr) {
|
|
77
80
|
this.uploadXHR = xhr
|
|
81
|
+
if (this.cancelled) { xhr.send = () => {}; return }
|
|
78
82
|
this.uploadedFile.value = this.recordXHR.response.signed_id
|
|
79
83
|
this.dispatch("before-storage-request", {
|
|
80
84
|
xhr: xhr
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import FormController from "./form-controller"
|
|
2
|
+
|
|
3
|
+
function createForm() {
|
|
4
|
+
const form = document.createElement("form")
|
|
5
|
+
document.body.appendChild(form)
|
|
6
|
+
return form
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
document.body.innerHTML = ""
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe("FormController", () => {
|
|
14
|
+
describe("default (uploadDialog: true)", () => {
|
|
15
|
+
it("creates the dialog and progress container", () => {
|
|
16
|
+
const form = createForm()
|
|
17
|
+
const controller = FormController.instance(form)
|
|
18
|
+
expect(controller.dialog).toBeTruthy()
|
|
19
|
+
expect(controller.progressContainerTarget).toBeTruthy()
|
|
20
|
+
expect(form.querySelector("#form-controller-dialog")).toBeTruthy()
|
|
21
|
+
expect(form.querySelector("#progress-container")).toBeTruthy()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("uploadDialog: false", () => {
|
|
26
|
+
it("does not create the dialog or progress container", () => {
|
|
27
|
+
const form = createForm()
|
|
28
|
+
const controller = FormController.instance(form, { uploadDialog: false })
|
|
29
|
+
expect(controller.dialog).toBeFalsy()
|
|
30
|
+
expect(controller.progressContainerTarget).toBeFalsy()
|
|
31
|
+
expect(form.querySelector("#form-controller-dialog")).toBeNull()
|
|
32
|
+
expect(form.querySelector("#progress-container")).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("still registers event listeners and queues controllers", () => {
|
|
36
|
+
const form = createForm()
|
|
37
|
+
FormController.instance(form, { uploadDialog: false })
|
|
38
|
+
|
|
39
|
+
const uploadedFile = document.createElement("div")
|
|
40
|
+
form.appendChild(uploadedFile)
|
|
41
|
+
const mockController = { start: jest.fn(cb => cb(null)), uploadedFile }
|
|
42
|
+
form.dispatchEvent(new CustomEvent("direct-upload:initialize", {
|
|
43
|
+
detail: { id: 1, file: { name: "test.jpg" }, controller: mockController },
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
expect(mockController.start).toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("does not create progress bars on init", () => {
|
|
50
|
+
const form = createForm()
|
|
51
|
+
FormController.instance(form, { uploadDialog: false })
|
|
52
|
+
|
|
53
|
+
const uploadedFile = document.createElement("div")
|
|
54
|
+
form.appendChild(uploadedFile)
|
|
55
|
+
const mockController = { start: jest.fn(cb => cb(null)), uploadedFile }
|
|
56
|
+
form.dispatchEvent(new CustomEvent("direct-upload:initialize", {
|
|
57
|
+
detail: { id: 1, file: { name: "test.jpg" }, controller: mockController },
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
expect(form.querySelector("progress-bar")).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import DirectUploadController from "../attachment-file/direct-upload-controller"
|
|
2
2
|
|
|
3
3
|
export default class FormController {
|
|
4
|
-
static instance(form) {
|
|
5
|
-
return form.inputAttachmentFormController ||= new FormController(form)
|
|
4
|
+
static instance(form, options = {}) {
|
|
5
|
+
return form.inputAttachmentFormController ||= new FormController(form, options)
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
progressContainerTarget: HTMLElement
|
|
@@ -14,29 +14,29 @@ export default class FormController {
|
|
|
14
14
|
submitted: boolean
|
|
15
15
|
processing: boolean
|
|
16
16
|
|
|
17
|
-
constructor(form) {
|
|
17
|
+
constructor(form, { uploadDialog = true } = {}) {
|
|
18
18
|
this.element = form
|
|
19
19
|
this.progressTargetMap = {}
|
|
20
20
|
this.controllers = []
|
|
21
21
|
this.submitted = false
|
|
22
22
|
this.processing = false
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<div class="direct-upload-
|
|
28
|
-
<
|
|
29
|
-
|
|
24
|
+
if (uploadDialog) {
|
|
25
|
+
this.element.insertAdjacentHTML("beforeend",
|
|
26
|
+
`<dialog id="form-controller-dialog">
|
|
27
|
+
<div class="direct-upload-wrapper">
|
|
28
|
+
<div class="direct-upload-content">
|
|
29
|
+
<h3>Uploading your media</h3>
|
|
30
|
+
<div id="progress-container"></div>
|
|
31
|
+
</div>
|
|
30
32
|
</div>
|
|
31
|
-
</
|
|
32
|
-
</dialog>`)
|
|
33
|
+
</dialog>`)
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if(this.element.dataset.remote !== "true" && (this.element.dataset.turbo == "false" || !(window as any).Turbo?.session?.enabled)) {
|
|
38
|
-
this.element.addEventListener("submit", event => this.submit(event))
|
|
35
|
+
this.dialog = this.element.querySelector("#form-controller-dialog")
|
|
36
|
+
this.progressContainerTarget = this.dialog.querySelector("#progress-container")
|
|
39
37
|
}
|
|
38
|
+
|
|
39
|
+
this.element.addEventListener("submit", event => this.submit(event))
|
|
40
40
|
window.addEventListener("beforeunload", event => this.beforeUnload(event))
|
|
41
41
|
|
|
42
42
|
this.element.addEventListener("direct-upload:initialize", event => this.init(event))
|
|
@@ -56,12 +56,13 @@ export default class FormController {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
submit(event) {
|
|
59
|
-
if(this.controllers.length === 0 && !this.hasUploadErrors()) return
|
|
59
|
+
if(this.controllers.length === 0 && !this.hasUploadErrors() && !this.processing) return
|
|
60
60
|
event.preventDefault()
|
|
61
61
|
this.submitted = true
|
|
62
|
+
this.setInputAttachmentsDisabled(true)
|
|
62
63
|
this.startNextController()
|
|
63
64
|
if(this.processing) {
|
|
64
|
-
this.dialog
|
|
65
|
+
this.dialog?.showModal()
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -71,10 +72,18 @@ export default class FormController {
|
|
|
71
72
|
const controller = this.controllers.shift()
|
|
72
73
|
if(controller) {
|
|
73
74
|
this.processing = true
|
|
74
|
-
this.
|
|
75
|
+
if (this.submitted) {
|
|
76
|
+
this.setInputAttachmentsDisabled(true)
|
|
77
|
+
} else {
|
|
78
|
+
this.setControllerInputDisabled(controller, true)
|
|
79
|
+
}
|
|
75
80
|
controller.start(error => {
|
|
76
|
-
if(
|
|
77
|
-
|
|
81
|
+
if (this.submitted) {
|
|
82
|
+
if(error) {
|
|
83
|
+
this.setInputAttachmentsDisabled(false)
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
this.setControllerInputDisabled(controller, false)
|
|
78
87
|
}
|
|
79
88
|
this.processing = false
|
|
80
89
|
this.startNextController()
|
|
@@ -90,16 +99,22 @@ export default class FormController {
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
submitForm() {
|
|
93
|
-
if(this.submitted)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
if(!this.submitted) return
|
|
103
|
+
if(this.hasUploadErrors()) {
|
|
104
|
+
this.dialog?.close()
|
|
105
|
+
this.setInputAttachmentsDisabled(false)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
this.setInputAttachmentsDisabled(true)
|
|
109
|
+
requestAnimationFrame(() => { // run after pending rAF callbacks (e.g. updateFormValue)
|
|
110
|
+
this.element.submit()
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setControllerInputDisabled(controller: DirectUploadController, disabled: boolean) {
|
|
115
|
+
const inputAttachment = (controller.uploadedFile as any).closest('input-attachment')
|
|
116
|
+
if (inputAttachment) {
|
|
117
|
+
inputAttachment.disabled = disabled
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
120
|
|
|
@@ -113,42 +128,46 @@ export default class FormController {
|
|
|
113
128
|
init(event) {
|
|
114
129
|
const { id, file, controller } = event.detail
|
|
115
130
|
|
|
116
|
-
this.progressContainerTarget
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
if (this.progressContainerTarget) {
|
|
132
|
+
this.progressContainerTarget.insertAdjacentHTML("beforebegin", `
|
|
133
|
+
<progress-bar id="direct-upload-${id}" class="direct-upload--pending">${file?.name || 'Uploading...'}</progress-bar>
|
|
134
|
+
`)
|
|
135
|
+
this.progressTargetMap[id] = document.getElementById(`direct-upload-${id}`)
|
|
136
|
+
}
|
|
121
137
|
|
|
122
138
|
this.controllers.push(controller)
|
|
123
139
|
this.startNextController()
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
start(event) {
|
|
127
|
-
this.progressTargetMap[event.detail.id]
|
|
143
|
+
this.progressTargetMap[event.detail.id]?.classList.remove("direct-upload--pending")
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
progress(event) {
|
|
131
147
|
const { id, progress } = event.detail
|
|
132
|
-
this.progressTargetMap[id]
|
|
148
|
+
const target = this.progressTargetMap[id]
|
|
149
|
+
if (target) target.percent = progress
|
|
133
150
|
}
|
|
134
151
|
|
|
135
152
|
error(event) {
|
|
136
153
|
event.preventDefault()
|
|
137
154
|
const { id, error } = event.detail
|
|
138
155
|
const target = this.progressTargetMap[id]
|
|
139
|
-
target
|
|
140
|
-
|
|
156
|
+
if (target) {
|
|
157
|
+
target.classList.add("direct-upload--error")
|
|
158
|
+
target.title = error
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
end(event) {
|
|
144
|
-
this.progressTargetMap[event.detail.id]
|
|
163
|
+
this.progressTargetMap[event.detail.id]?.classList.add("direct-upload--complete")
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
removeUploadedFile(event) {
|
|
148
167
|
const uploadedFile = event.detail
|
|
149
168
|
const id = uploadedFile.controller?.directUpload?.id
|
|
150
169
|
if(id) {
|
|
151
|
-
document.getElementById(`direct-upload-${id}`)
|
|
170
|
+
document.getElementById(`direct-upload-${id}`)?.remove()
|
|
152
171
|
delete this.progressTargetMap[id]
|
|
153
172
|
}
|
|
154
173
|
this.setInputAttachmentsDisabled(false)
|
|
@@ -22,6 +22,7 @@ export class InputAttachment {
|
|
|
22
22
|
@Prop() max: number
|
|
23
23
|
@Prop() preview: boolean = true
|
|
24
24
|
@Prop() disabled: boolean = false
|
|
25
|
+
@Prop({ attribute: "upload-dialog" }) uploadDialog: boolean = true
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
form: HTMLFormElement
|
|
@@ -37,7 +38,7 @@ export class InputAttachment {
|
|
|
37
38
|
this.form = this.internals.form
|
|
38
39
|
if (this.form) {
|
|
39
40
|
this.form.addEventListener("reset", () => this.reset())
|
|
40
|
-
FormController.instance(this.form)
|
|
41
|
+
FormController.instance(this.form, { uploadDialog: this.uploadDialog })
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// Note: Server-rendered children may not be available yet during componentWillLoad
|
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
## Properties
|
|
9
9
|
|
|
10
|
-
| Property | Attribute
|
|
11
|
-
| -------------- |
|
|
12
|
-
| `accepts` | `accepts`
|
|
13
|
-
| `directupload` | `directupload`
|
|
14
|
-
| `disabled` | `disabled`
|
|
15
|
-
| `max` | `max`
|
|
16
|
-
| `multiple` | `multiple`
|
|
17
|
-
| `name` | `name`
|
|
18
|
-
| `preview` | `preview`
|
|
19
|
-
| `required` | `required`
|
|
10
|
+
| Property | Attribute | Description | Type | Default |
|
|
11
|
+
| -------------- | --------------- | ----------- | --------- | ----------- |
|
|
12
|
+
| `accepts` | `accepts` | | `string` | `undefined` |
|
|
13
|
+
| `directupload` | `directupload` | | `string` | `undefined` |
|
|
14
|
+
| `disabled` | `disabled` | | `boolean` | `false` |
|
|
15
|
+
| `max` | `max` | | `number` | `undefined` |
|
|
16
|
+
| `multiple` | `multiple` | | `boolean` | `false` |
|
|
17
|
+
| `name` | `name` | | `string` | `undefined` |
|
|
18
|
+
| `preview` | `preview` | | `boolean` | `true` |
|
|
19
|
+
| `required` | `required` | | `boolean` | `false` |
|
|
20
|
+
| `uploadDialog` | `upload-dialog` | | `boolean` | `true` |
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
## Shadow Parts
|
|
@@ -58,6 +58,10 @@ export namespace Components {
|
|
|
58
58
|
* @default false
|
|
59
59
|
*/
|
|
60
60
|
"required": boolean;
|
|
61
|
+
/**
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
"uploadDialog": boolean;
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
export interface AttachmentFileCustomEvent<T> extends CustomEvent<T> {
|
|
@@ -158,6 +162,10 @@ declare namespace LocalJSX {
|
|
|
158
162
|
* @default false
|
|
159
163
|
*/
|
|
160
164
|
"required"?: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* @default true
|
|
167
|
+
*/
|
|
168
|
+
"uploadDialog"?: boolean;
|
|
161
169
|
}
|
|
162
170
|
interface IntrinsicElements {
|
|
163
171
|
"attachment-file": AttachmentFile;
|
|
@@ -309,6 +309,22 @@ Then "the {string} attachment field should be disabled" do |field|
|
|
|
309
309
|
expect(is_disabled).to be true
|
|
310
310
|
end
|
|
311
311
|
|
|
312
|
+
Then "the {string} attachment field should not be disabled" do |field|
|
|
313
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
314
|
+
|
|
315
|
+
is_disabled = page.evaluate_script(<<~JS)
|
|
316
|
+
(function() {
|
|
317
|
+
const element = document.getElementById('#{element[:id]}');
|
|
318
|
+
if (element.hasAttribute('disabled')) return true;
|
|
319
|
+
if (element.closest('fieldset[disabled]')) return true;
|
|
320
|
+
const fileInput = element.shadowRoot?.querySelector('input[type="file"]');
|
|
321
|
+
return fileInput?.disabled || false;
|
|
322
|
+
})()
|
|
323
|
+
JS
|
|
324
|
+
|
|
325
|
+
expect(is_disabled).to be false
|
|
326
|
+
end
|
|
327
|
+
|
|
312
328
|
Then "the {string} attachment field should have a validation error containing {string}" do |field, message|
|
|
313
329
|
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
314
330
|
messages = Bard::AttachmentField::TestHelper.validation_messages(page, element)
|
|
@@ -6,6 +6,9 @@ module Bard
|
|
|
6
6
|
"directupload" => "/rails/active_storage/direct_uploads",
|
|
7
7
|
"preview" => true,
|
|
8
8
|
})
|
|
9
|
+
if options.key?("upload-dialog") && options["upload-dialog"] == false
|
|
10
|
+
options["upload-dialog"] = "false"
|
|
11
|
+
end
|
|
9
12
|
add_default_name_and_id(options)
|
|
10
13
|
|
|
11
14
|
content_tag("input-attachment", options) do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bard-attachment_field
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activestorage
|
|
@@ -361,6 +361,7 @@ files:
|
|
|
361
361
|
- input-attachment/src/components/attachment-preview/attachment-preview.spec.tsx
|
|
362
362
|
- input-attachment/src/components/attachment-preview/attachment-preview.tsx
|
|
363
363
|
- input-attachment/src/components/attachment-preview/readme.md
|
|
364
|
+
- input-attachment/src/components/input-attachment/form-controller.spec.ts
|
|
364
365
|
- input-attachment/src/components/input-attachment/form-controller.tsx
|
|
365
366
|
- input-attachment/src/components/input-attachment/input-attachment.css
|
|
366
367
|
- input-attachment/src/components/input-attachment/input-attachment.e2e.ts
|