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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f060d98d9b6c3c4e0494845d868b7f702009df53e1cd39ac0d659402a8c28b97
4
- data.tar.gz: a2e2afa88506420373c09e818612a037eb111a910370c87a2f41c69968d84151
3
+ metadata.gz: 77372a4e55bd97d9a4c52f8089427717d9181d225d58b18814194a938a1c1631
4
+ data.tar.gz: 2eaa2f26f3a3f6fbf57523527923e21694596a13ab1de7968f4c401a5d368f88
5
5
  SHA512:
6
- metadata.gz: 1480f10a1aaf61dc8f14e7c0e97062b033e85aa8d97d47e6df857cf1d254ad3b3adbee192d3de2156dc19dc3d2de16f244c9b6561933483c70a797ba63a0eecc
7
- data.tar.gz: c78324d6d01030eeb10180287c14c6218e122ad865b511c89c7613ce8515b04aa3f138d4cdf8e0aab35216b66443478fafb6e6314955cfe9b01d06b1320493fb
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.directUpload.url = null;
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) && !name.includes("[]")) {
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 fetch = window.Turbo ? window.Turbo.fetch : window.fetch;
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.isScript) {
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: this.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(baseHeaders, this.additionalHeaders)
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
- this.element.insertAdjacentHTML("beforeend", `<dialog id="form-controller-dialog">
5488
- <div class="direct-upload-wrapper">
5489
- <div class="direct-upload-content">
5490
- <h3>Uploading your media</h3>
5491
- <div id="progress-container"></div>
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
- </div>
5494
- </dialog>`);
5495
- this.dialog = this.element.querySelector("#form-controller-dialog");
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.showModal();
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.setInputAttachmentsDisabled(true);
5501
+ if (this.submitted) {
5502
+ this.setInputAttachmentsDisabled(true);
5503
+ } else {
5504
+ this.setControllerInputDisabled(controller, true);
5505
+ }
5531
5506
  controller.start((error) => {
5532
- if (error) {
5533
- this.setInputAttachmentsDisabled(false);
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
- if (this.hasUploadErrors()) {
5548
- this.dialog.close();
5549
- this.setInputAttachmentsDisabled(false);
5550
- return;
5551
- }
5552
- this.setInputAttachmentsDisabled(true);
5553
- window.setTimeout(() => {
5554
- this.element.submit();
5555
- }, 10);
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.insertAdjacentHTML("beforebegin", `
5566
- <progress-bar id="direct-upload-${id2}" class="direct-upload--pending">${file?.name || "Uploading..."}</progress-bar>
5567
- `);
5568
- const progressTarget = document.getElementById(`direct-upload-${id2}`);
5569
- this.progressTargetMap[id2] = progressTarget;
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].classList.remove("direct-upload--pending");
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].percent = progress;
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.classList.add("direct-upload--error");
5585
- target.title = error;
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].classList.add("direct-upload--complete");
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}`).remove();
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: "da0ceb39e5024d0dfe50511a4aa1139188dcb4a2" }, h("input", { key: "b9197faa99dcd580bf7c0fb7f356daa715d901d1", 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: {
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: "ce0608a99601202d437b7228e87437b510ba639f", onClick: () => this.fileInput?.click(), onDrop: this.handleDrop }, h("p", { key: "658d85fa352e7517789d95ad0c25756dafc79c08", part: "title" }, h("strong", { key: "7a160aa3132cdc8159e4319262cca69aa0b60d63" }, "Choose ", this.multiple ? "files" : "file", " "), h("span", { key: "51ab69b28833c8d442d1907ead5ddc79a6820f53" }, "or drag ", this.multiple ? "them" : "it", " here.")), h("div", { key: "a5bbee999e0575e27f7fc058640d8ee281ee0728", class: `media-preview ${this.multiple ? "-stacked" : ""}` }, h("slot", { key: "3a07d4e5aeaaf80fd5e37e9b135eb820dfcef757" }))));
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": [".ts", ".tsx"],
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.directUpload.url = null
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
- this.element.insertAdjacentHTML("beforeend",
25
- `<dialog id="form-controller-dialog">
26
- <div class="direct-upload-wrapper">
27
- <div class="direct-upload-content">
28
- <h3>Uploading your media</h3>
29
- <div id="progress-container"></div>
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
- </div>
32
- </dialog>`)
33
+ </dialog>`)
33
34
 
34
- this.dialog = this.element.querySelector("#form-controller-dialog")
35
- this.progressContainerTarget = this.dialog.querySelector("#progress-container")
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.showModal()
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.setInputAttachmentsDisabled(true)
75
+ if (this.submitted) {
76
+ this.setInputAttachmentsDisabled(true)
77
+ } else {
78
+ this.setControllerInputDisabled(controller, true)
79
+ }
75
80
  controller.start(error => {
76
- if(error) {
77
- this.setInputAttachmentsDisabled(false)
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
- if(this.hasUploadErrors()) {
95
- this.dialog.close()
96
- this.setInputAttachmentsDisabled(false)
97
- return
98
- }
99
- this.setInputAttachmentsDisabled(true)
100
- window.setTimeout(() => { // allow other async tasks to complete
101
- this.element.submit()
102
- }, 10)
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.insertAdjacentHTML("beforebegin", `
117
- <progress-bar id="direct-upload-${id}" class="direct-upload--pending">${file?.name || 'Uploading...'}</progress-bar>
118
- `)
119
- const progressTarget = document.getElementById(`direct-upload-${id}`)
120
- this.progressTargetMap[id] = progressTarget
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].classList.remove("direct-upload--pending")
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].percent = progress
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.classList.add("direct-upload--error")
140
- target.title = error
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].classList.add("direct-upload--complete")
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}`).remove()
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 | 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` |
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module AttachmentField
5
- VERSION = "0.2.4"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
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.2.4
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-04 00:00:00.000000000 Z
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