bard-attachment_field 0.6.3 → 0.6.5
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/Appraisals +1 -0
- data/app/assets/javascripts/input-attachment.js +43 -22
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/input-attachment/README.md +35 -3
- data/input-attachment/package.json +1 -1
- data/input-attachment/src/components/attachment-file/attachment-file.tsx +2 -0
- data/input-attachment/src/components/attachment-file/not-empty.ts +20 -0
- data/input-attachment/src/components/input-attachment/input-attachment.spec.tsx +41 -0
- data/input-attachment/src/components/input-attachment/input-attachment.tsx +24 -1
- 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: f2ae64b317884629f1640e9e60042f4fd6483a5f76aa27a541e8cb090a9ab513
|
|
4
|
+
data.tar.gz: 18b7d64724a970afb9a8bc9c9dfe63fa07531033929b8ce25f8b8072d3243949
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5dba5578a41090328f203826e7032b41557ee5e7d552809c7958d15af8ddecf7c7a7dd3467319ed8aca8ffb31a5487ed76472201d12053832c69fe55165f4f4
|
|
7
|
+
data.tar.gz: f061a728c792c4f1d42882fdf791617e3fe020461642f8560fe83908a407d565de2e33ba73e8e35488bc9bddb5b22a65d1aaf38ac0f0f72e308aa4e5f2e63e7c
|
data/Appraisals
CHANGED
|
@@ -4252,10 +4252,10 @@ var l = class _l {
|
|
|
4252
4252
|
return false;
|
|
4253
4253
|
}
|
|
4254
4254
|
};
|
|
4255
|
-
function
|
|
4255
|
+
function u(t2, e3) {
|
|
4256
4256
|
return "string" == typeof t2 && (e3 = t2, t2 = document), t2.querySelector(e3);
|
|
4257
4257
|
}
|
|
4258
|
-
function
|
|
4258
|
+
function d(t2, e3, i2 = {}) {
|
|
4259
4259
|
const { disabled: s } = t2, { bubbles: r4, cancelable: n3, detail: o3 } = i2, a3 = document.createEvent("Event");
|
|
4260
4260
|
a3.initEvent(e3, r4 || true, n3 || true), a3.detail = o3 || {};
|
|
4261
4261
|
try {
|
|
@@ -4274,7 +4274,7 @@ var f = class {
|
|
|
4274
4274
|
this.xhr.setRequestHeader(t3, s[t3]);
|
|
4275
4275
|
}));
|
|
4276
4276
|
const r4 = (function() {
|
|
4277
|
-
const t3 =
|
|
4277
|
+
const t3 = u(document.head, 'meta[name="csrf-token"]');
|
|
4278
4278
|
if (t3) return t3.getAttribute("content");
|
|
4279
4279
|
})();
|
|
4280
4280
|
null != r4 && this.xhr.setRequestHeader("X-CSRF-Token", r4), this.xhr.addEventListener("load", ((t3) => this.requestDidLoad(t3))), this.xhr.addEventListener("error", ((t3) => this.requestDidError(t3)));
|
|
@@ -4365,7 +4365,7 @@ var v = class {
|
|
|
4365
4365
|
return this.input.getAttribute("data-direct-upload-url");
|
|
4366
4366
|
}
|
|
4367
4367
|
dispatch(t2, e3 = {}) {
|
|
4368
|
-
return e3.file = this.file, e3.id = this.directUpload.id,
|
|
4368
|
+
return e3.file = this.file, e3.id = this.directUpload.id, d(this.input, `direct-upload:${t2}`, { detail: e3 });
|
|
4369
4369
|
}
|
|
4370
4370
|
dispatchError(t2) {
|
|
4371
4371
|
this.dispatch("error", { error: t2 }).defaultPrevented || alert(t2);
|
|
@@ -4417,7 +4417,7 @@ var y = class {
|
|
|
4417
4417
|
})), t2;
|
|
4418
4418
|
}
|
|
4419
4419
|
dispatch(t2, e3 = {}) {
|
|
4420
|
-
return
|
|
4420
|
+
return d(this.form, `direct-uploads:${t2}`, { detail: e3 });
|
|
4421
4421
|
}
|
|
4422
4422
|
};
|
|
4423
4423
|
var x = "data-direct-uploads-processing";
|
|
@@ -4439,7 +4439,7 @@ function $(t2) {
|
|
|
4439
4439
|
const i2 = new y(e3), { inputs: s } = i2;
|
|
4440
4440
|
s.length && (t2.preventDefault(), e3.setAttribute(x, ""), s.forEach(E), i2.start(((t3) => {
|
|
4441
4441
|
e3.removeAttribute(x), t3 ? s.forEach(U) : (function(t4) {
|
|
4442
|
-
let e4 = k.get(t4) ||
|
|
4442
|
+
let e4 = k.get(t4) || u(t4, "input[type=submit], button[type=submit]");
|
|
4443
4443
|
if (e4) {
|
|
4444
4444
|
const { disabled: t5 } = e4;
|
|
4445
4445
|
e4.disabled = false, e4.focus(), e4.click(), e4.disabled = t5;
|
|
@@ -4532,6 +4532,16 @@ var q = class {
|
|
|
4532
4532
|
return parseFloat((t2 / Math.pow(1024, s)).toFixed(i2)) + ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][s];
|
|
4533
4533
|
}
|
|
4534
4534
|
};
|
|
4535
|
+
var F = class {
|
|
4536
|
+
uploadedFile;
|
|
4537
|
+
constructor(t2) {
|
|
4538
|
+
this.uploadedFile = t2;
|
|
4539
|
+
}
|
|
4540
|
+
get errors() {
|
|
4541
|
+
return this.#t || (this.#t = [], 0 === this.uploadedFile.size && this.#t.push("Must be larger than 0 bytes.")), this.#t;
|
|
4542
|
+
}
|
|
4543
|
+
#t;
|
|
4544
|
+
};
|
|
4535
4545
|
var B = class {
|
|
4536
4546
|
uploadedFile;
|
|
4537
4547
|
constructor(t2) {
|
|
@@ -4548,11 +4558,11 @@ var B = class {
|
|
|
4548
4558
|
return t2.length >= 3 ? (t2.slice(0, -1) + [`or ${t2.at(-1)}`]).join(", ") : t2.join(" or ");
|
|
4549
4559
|
}
|
|
4550
4560
|
};
|
|
4551
|
-
var
|
|
4561
|
+
var S = { image: ["ase", "art", "bmp", "blp", "cd5", "cit", "cpt", "cr2", "cut", "dds", "dib", "djvu", "egt", "exif", "gif", "gpl", "grf", "icns", "ico", "iff", "jng", "jpeg", "jpg", "jfif", "jp2", "jps", "lbm", "max", "miff", "mng", "msp", "nef", "nitf", "ota", "pbm", "pc1", "pc2", "pc3", "pcf", "pcx", "pdn", "pgm", "PI1", "PI2", "PI3", "pict", "pct", "pnm", "pns", "ppm", "psb", "psd", "pdd", "psp", "px", "pxm", "pxr", "qfx", "raw", "rle", "sct", "sgi", "rgb", "int", "bw", "tga", "tiff", "tif", "vtf", "xbm", "xcf", "xpm", "3dv", "amf", "ai", "awg", "cgm", "cdr", "cmx", "dxf", "e2d", "egt", "eps", "fs", "gbr", "odg", "svg", "stl", "vrml", "x3d", "sxd", "v2d", "vnd", "wmf", "emf", "art", "xar", "png", "webp", "jxr", "hdp", "wdp", "cur", "ecw", "iff", "lbm", "liff", "nrrd", "pam", "pcx", "pgf", "sgi", "rgb", "rgba", "bw", "int", "inta", "sid", "ras", "sun", "tga", "heic", "heif"], video: ["3g2", "3gp", "3gpp", "aaf", "asf", "avchd", "avi", "drc", "flv", "m2v", "m3u8", "m4p", "m4v", "mkv", "mng", "mov", "mp2", "mp4", "mpe", "mpeg", "mpg", "mpv", "mxf", "nsv", "ogg", "ogv", "qt", "rm", "rmvb", "roq", "svi", "vob", "webm", "wmv", "yuv"], pdf: ["pdf"], getFileType: function(t2) {
|
|
4552
4562
|
const e3 = t2.toString().split(".").at(-1).toLowerCase().trim();
|
|
4553
4563
|
return this.video.includes(e3) ? "video" : this.image.includes(e3) ? "image" : this.pdf.includes(e3) ? "pdf" : "unknown";
|
|
4554
4564
|
} };
|
|
4555
|
-
var
|
|
4565
|
+
var T = class {
|
|
4556
4566
|
constructor(t2) {
|
|
4557
4567
|
this.response = t2;
|
|
4558
4568
|
}
|
|
@@ -4597,7 +4607,7 @@ var S = class {
|
|
|
4597
4607
|
window.Turbo ? await window.Turbo.renderStreamMessage(await this.text) : console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
|
|
4598
4608
|
}
|
|
4599
4609
|
};
|
|
4600
|
-
var
|
|
4610
|
+
var W = class {
|
|
4601
4611
|
static register(t2) {
|
|
4602
4612
|
this.interceptor = t2;
|
|
4603
4613
|
}
|
|
@@ -4608,22 +4618,22 @@ var T = class {
|
|
|
4608
4618
|
this.interceptor = void 0;
|
|
4609
4619
|
}
|
|
4610
4620
|
};
|
|
4611
|
-
function
|
|
4621
|
+
function X(t2) {
|
|
4612
4622
|
const e3 = document.head.querySelector(`meta[name="${t2}"]`);
|
|
4613
4623
|
return e3 && e3.content;
|
|
4614
4624
|
}
|
|
4615
|
-
var
|
|
4625
|
+
var H2 = class {
|
|
4616
4626
|
constructor(t2, e3, i2 = {}) {
|
|
4617
4627
|
this.method = t2, this.options = i2, this.originalUrl = e3.toString();
|
|
4618
4628
|
}
|
|
4619
4629
|
async perform() {
|
|
4620
4630
|
try {
|
|
4621
|
-
const t3 =
|
|
4631
|
+
const t3 = W.get();
|
|
4622
4632
|
t3 && await t3(this);
|
|
4623
4633
|
} catch (t3) {
|
|
4624
4634
|
console.error(t3);
|
|
4625
4635
|
}
|
|
4626
|
-
const t2 = new
|
|
4636
|
+
const t2 = new T(await window.fetch(this.url, this.fetchOptions));
|
|
4627
4637
|
return t2.unauthenticated && t2.authenticationURL ? Promise.reject(window.location.href = t2.authenticationURL) : (t2.ok && t2.isTurboStream && await t2.renderTurboStream(), t2);
|
|
4628
4638
|
}
|
|
4629
4639
|
addHeader(t2, e3) {
|
|
@@ -4650,7 +4660,7 @@ var X = class {
|
|
|
4650
4660
|
const t3 = s.split("=").slice(1).join("=");
|
|
4651
4661
|
if (t3) return decodeURIComponent(t3);
|
|
4652
4662
|
}
|
|
4653
|
-
})(
|
|
4663
|
+
})(X("csrf-param")) || X("csrf-token");
|
|
4654
4664
|
}
|
|
4655
4665
|
get contentType() {
|
|
4656
4666
|
return this.options.contentType ? this.options.contentType : null == this.body || this.body instanceof window.FormData ? void 0 : this.body instanceof window.File ? this.body.type : "application/json";
|
|
@@ -4700,7 +4710,7 @@ var X = class {
|
|
|
4700
4710
|
return "application/json" !== this.headers["Content-Type"] || t2 ? this.body : JSON.stringify(this.body);
|
|
4701
4711
|
}
|
|
4702
4712
|
};
|
|
4703
|
-
var
|
|
4713
|
+
var z = proxyCustomElement(class extends H {
|
|
4704
4714
|
constructor(t2) {
|
|
4705
4715
|
super(), false !== t2 && this.__registerHost(), this.__attachShadow(), this.removeEvent = createEvent(this, "attachment-file:remove", 7), this.validationEvent = createEvent(this, "attachment-file:validation", 7), this.readyEvent = createEvent(this, "attachment-file:ready", 7);
|
|
4706
4716
|
}
|
|
@@ -4745,12 +4755,12 @@ var H2 = proxyCustomElement(class extends H {
|
|
|
4745
4755
|
this.src = URL.createObjectURL(t2), this.filename = t2.name, this.size = t2.size, this.state = "pending", this.percent = 0, this._file = t2;
|
|
4746
4756
|
}
|
|
4747
4757
|
set signedId(t2) {
|
|
4748
|
-
this.value !== t2 && ((t3, e3 = {}, i2 = {}) => ((t4, e4, i3, s) => new
|
|
4758
|
+
this.value !== t2 && ((t3, e3 = {}, i2 = {}) => ((t4, e4, i3, s) => new H2("get", e4, { headers: { Accept: "application/json", ...s }, query: i3 }).perform().then(((t5) => t5.response.ok ? t5.json : t5)))(0, t3, e3, i2))(`/rails/active_storage/blobs/info/${t2}`).then(((e3) => {
|
|
4749
4759
|
this.src = `/rails/active_storage/blobs/redirect/${t2}/${e3.filename}`, this.filename = e3.filename, this.size = e3.byte_size, this.state = "complete", this.percent = 100, this.value = t2, this.readyEvent.emit(this);
|
|
4750
4760
|
}));
|
|
4751
4761
|
}
|
|
4752
4762
|
setMissingFiletype(t2, e3) {
|
|
4753
|
-
!this.filetype && this.filename && (this.filetype =
|
|
4763
|
+
!this.filetype && this.filename && (this.filetype = S.getFileType(this.filename));
|
|
4754
4764
|
}
|
|
4755
4765
|
start(t2) {
|
|
4756
4766
|
this.state = "pending", this.percent = 0;
|
|
@@ -4768,14 +4778,14 @@ var H2 = proxyCustomElement(class extends H {
|
|
|
4768
4778
|
"error" !== this.state && (this.state = "complete", this.percent = 100);
|
|
4769
4779
|
}
|
|
4770
4780
|
render() {
|
|
4771
|
-
return h(Host, { key: "
|
|
4781
|
+
return h(Host, { key: "58b4ee95bbb2847b70e0eb7273a5da05c9f65243" }, h("slot", { key: "1a50d5449d200b72f25b2824c2f0130cf09b5c9d" }), h("figure", { key: "a2ebf2d116499c28a4f83a2168df42e6fd0a450b" }, h("div", { key: "0d8ac23f1961b46affef09a23265da0c5e57fb33", class: "progress-details" }, h("progress-bar", { key: "2d6af9889678f6ae07f6deed914cddc4107a0a2d", percent: this.percent, class: this.state, error: "error" === this.state }, h("a", { key: "ebfcdd0382a04d1921447b2d659a88eb7522c1ce", class: "download-link", href: this.href || this.src, download: this.download || this.filename, onClick: (t2) => t2.stopPropagation() }, this.filename)), h("span", { key: "42dfc08c869e2cbb475d05e2f316c79733743e05", class: "progress-icon" }), h("a", { key: "f2286259bb36fb3d32036459e7383fe1e29be962", class: "remove-media", onClick: this.removeClicked, href: "#" }, h("span", { key: "2a36a5856faae7292ed2ac0572e180f3892af728" }, "Remove media")), this.uploadError && this._file ? h("a", { class: "retry-media", onClick: this.retryClicked, href: "#" }, h("span", null, "Retry upload")) : ""), this.validationError || this.uploadError ? h("p", { class: "validation-error" }, this.validationError || this.uploadError) : "", this.preview ? h("attachment-preview", { src: this.src, filetype: this.filetype }) : ""));
|
|
4772
4782
|
}
|
|
4773
4783
|
componentDidLoad() {
|
|
4774
4784
|
"pending" == this.state && this._file && (this.checkValidity() ? (this.controller = new C(this.el, this._file), this.controller.dispatch("initialize", { controller: this.controller })) : this.state = "error");
|
|
4775
4785
|
}
|
|
4776
4786
|
checkValidity() {
|
|
4777
4787
|
let t2 = [];
|
|
4778
|
-
return t2.push(...new B(this).errors), t2.push(...new q(this).errors), this.validationError = t2.join(" "), this.validationEvent.emit({ valid: 0 === t2.length, error: this.validationError }), 0 === t2.length;
|
|
4788
|
+
return t2.push(...new B(this).errors), t2.push(...new q(this).errors), t2.push(...new F(this).errors), this.validationError = t2.join(" "), this.validationEvent.emit({ valid: 0 === t2.length, error: this.validationError }), 0 === t2.length;
|
|
4779
4789
|
}
|
|
4780
4790
|
static get watchers() {
|
|
4781
4791
|
return { filename: [{ setMissingFiletype: 0 }] };
|
|
@@ -4786,7 +4796,7 @@ var H2 = proxyCustomElement(class extends H {
|
|
|
4786
4796
|
}, [769, "attachment-file", { name: [1537], accepts: [1537], max: [1538], url: [1537], value: [1537], filename: [1537], src: [1537], href: [1537], download: [1537], filetype: [1537], size: [1538], state: [1537], percent: [1538], preview: [1540], validationMessage: [1, "validation-message"] }, [[0, "direct-upload:initialize", "start"], [0, "direct-upload:start", "start"], [0, "direct-upload:progress", "progress"], [0, "direct-upload:error", "error"], [0, "direct-upload:end", "end"]], { filename: [{ setMissingFiletype: 0 }] }]);
|
|
4787
4797
|
|
|
4788
4798
|
// dist/components/attachment-file.js
|
|
4789
|
-
var o =
|
|
4799
|
+
var o = z;
|
|
4790
4800
|
|
|
4791
4801
|
// dist/components/attachment-preview.js
|
|
4792
4802
|
var e = a;
|
|
@@ -5179,7 +5189,7 @@ var c3 = proxyCustomElement(class extends H {
|
|
|
5179
5189
|
updateFormValue() {
|
|
5180
5190
|
if (!this.name || !this.internals?.setFormValue) return;
|
|
5181
5191
|
const t2 = new FormData(), e3 = this.files.map(((t3) => t3.value)).filter(((t3) => t3 && "string" != typeof t3 ? (console.error("[input-attachment] Non-string value detected on attachment-file:", typeof t3, t3, Error().stack), false) : "string" == typeof t3 && t3.length > 0));
|
|
5182
|
-
if (this.multiple ? (e3.forEach(((e4) => t2.append(this.name, e4))), 0 === e3.length && t2.append(this.name, "")) : t2.set(this.name, e3[0] || ""), this.internals.setFormValue(t2), this.required && 0 === this.files.length) this.internals.setValidity({ valueMissing: true }, "Please select a file.", this.fileInput);
|
|
5192
|
+
if (this.multiple ? (e3.forEach(((e4) => t2.append(this.name, e4))), 0 === e3.length && t2.append(this.name, "")) : t2.set(this.name, e3[0] || ""), this.internals.setFormValue(t2, this.value), this.required && 0 === this.files.length) this.internals.setValidity({ valueMissing: true }, "Please select a file.", this.fileInput);
|
|
5183
5193
|
else {
|
|
5184
5194
|
const t3 = this.files.map(((t4) => t4.validationError)).filter(((t4) => t4 && t4.length > 0));
|
|
5185
5195
|
t3.length > 0 ? this.internals.setValidity({ customError: true }, t3[0], this.fileInput) : this.internals.setValidity({});
|
|
@@ -5188,6 +5198,17 @@ var c3 = proxyCustomElement(class extends H {
|
|
|
5188
5198
|
reset() {
|
|
5189
5199
|
this.files = [];
|
|
5190
5200
|
}
|
|
5201
|
+
formStateRestoreCallback(t2) {
|
|
5202
|
+
this.restorable(t2) ? this.value = t2 : this.updateFormValue();
|
|
5203
|
+
}
|
|
5204
|
+
restorable(t2) {
|
|
5205
|
+
if ("string" != typeof t2) return false;
|
|
5206
|
+
try {
|
|
5207
|
+
return Array.isArray(JSON.parse(t2));
|
|
5208
|
+
} catch {
|
|
5209
|
+
return false;
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5191
5212
|
handleFileInputChange = () => {
|
|
5192
5213
|
this.fileInput?.files?.length && (this.addFiles(this.fileInput.files), this.fileInput.value = null);
|
|
5193
5214
|
};
|
|
@@ -5215,7 +5236,7 @@ var c3 = proxyCustomElement(class extends H {
|
|
|
5215
5236
|
return this.disabled || !!this.el.closest("fieldset[disabled]");
|
|
5216
5237
|
}
|
|
5217
5238
|
render() {
|
|
5218
|
-
return h(Host, { key: "
|
|
5239
|
+
return h(Host, { key: "9b1ce57edcd31be2885c5923e65c8f4d0ac31002" }, h("input", { key: "a498cdd103d4b3385bfbfb4c2158ae2c15ed3b55", ref: (t2) => this.fileInput = t2, type: "file", "aria-label": "Choose " + (this.multiple ? "files" : "file"), multiple: this.multiple, accept: this.accepts, required: this.required && 0 === this.files.length, disabled: this.isDisabled, onChange: () => this.handleFileInputChange() }), h("file-drop", { key: "9edc0c1e22d0a6385515f6dff44965c9f3e7a40c", onClick: () => this.fileInput?.click(), onDrop: this.handleDrop }, h("p", { key: "ae06d2f6be38f25eb977e66b1c6c565ca0e3c97d", part: "title" }, h("strong", { key: "c53fc410ec761ca7f044c25cceb1be12c6e37b5c" }, "Choose ", this.multiple ? "files" : "file", " "), h("span", { key: "227a48d7ed21443ec97f646b58611cbf611145bc" }, "or drag ", this.multiple ? "them" : "it", " here.")), h("div", { key: "25fce75fb15b66bd13bea450054d22ddabd05824", class: "media-preview " + (this.multiple ? "-stacked" : "") }, h("slot", { key: "86f6db9bef3f2d6c16a72679ec33b751aef4eec5" }))));
|
|
5219
5240
|
}
|
|
5220
5241
|
componentDidRender() {
|
|
5221
5242
|
if (0 === this.files.length) {
|
data/gemfiles/rails_8.1.gemfile
CHANGED
data/input-attachment/README.md
CHANGED
|
@@ -77,9 +77,9 @@ The main component providing file upload functionality.
|
|
|
77
77
|
get files(): AttachmentFile[]
|
|
78
78
|
set files(val: AttachmentFile[])
|
|
79
79
|
|
|
80
|
-
// Get/set
|
|
81
|
-
get value(): string
|
|
82
|
-
set value(val: string
|
|
80
|
+
// Get/set the attachments as a JSON string of { value: signed_id, filename, src, ... } objects
|
|
81
|
+
get value(): string
|
|
82
|
+
set value(val: string)
|
|
83
83
|
|
|
84
84
|
// Clear all files
|
|
85
85
|
reset(): void
|
|
@@ -137,6 +137,38 @@ set signedId(val: string)
|
|
|
137
137
|
checkValidity(): boolean
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
## Outside Rails (cross-origin, token auth)
|
|
141
|
+
|
|
142
|
+
`directupload` can be an absolute URL on another origin, as long as that endpoint speaks the
|
|
143
|
+
`@rails/activestorage` protocol and the storage bucket's CORS allows your origin. Add
|
|
144
|
+
credentials from the `direct-upload:before-blob-request` event, which carries the XHR for the
|
|
145
|
+
blob-reserving request:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const el = document.querySelector("input-attachment")
|
|
149
|
+
el.addEventListener("direct-upload:before-blob-request", event => {
|
|
150
|
+
event.detail.xhr.setRequestHeader("Authorization", `Bearer ${token}`)
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The element registers itself only after `defineCustomElements()` runs, so in a framework call it
|
|
155
|
+
once on the client:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import { defineCustomElements } from "@botandrose/input-attachment"
|
|
159
|
+
defineCustomElements()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Without a `<form>` submit, read the signed id from the `change` event: `value` is a JSON string
|
|
163
|
+
of the current attachments, and each one's `value` is its signed id.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
el.addEventListener("change", () => {
|
|
167
|
+
const [attachment] = JSON.parse(el.value)
|
|
168
|
+
console.log(attachment?.value) // the signed id, or undefined once removed
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
140
172
|
## Form Submission Flow
|
|
141
173
|
|
|
142
174
|
1. User selects/drops files into `<input-attachment>`
|
|
@@ -2,6 +2,7 @@ import { Component, Prop, Element, Watch, Host, h } from '@stencil/core'
|
|
|
2
2
|
import { Listen, Event, EventEmitter } from '@stencil/core'
|
|
3
3
|
import DirectUploadController from './direct-upload-controller'
|
|
4
4
|
import Max from './max'
|
|
5
|
+
import NotEmpty from './not-empty'
|
|
5
6
|
import Accepts from './accepts'
|
|
6
7
|
import Extensions from './extensions'
|
|
7
8
|
import { get } from 'rails-request-json'
|
|
@@ -170,6 +171,7 @@ export class AttachmentFile {
|
|
|
170
171
|
let errors = []
|
|
171
172
|
errors.push(...new Accepts(this).errors)
|
|
172
173
|
errors.push(...new Max(this).errors)
|
|
174
|
+
errors.push(...new NotEmpty(this).errors)
|
|
173
175
|
this.validationError = errors.join(" ")
|
|
174
176
|
// Notify parent of validation state change
|
|
175
177
|
this.validationEvent.emit({ valid: errors.length === 0, error: this.validationError })
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AttachmentFile } from './attachment-file'
|
|
2
|
+
|
|
3
|
+
export default class NotEmpty {
|
|
4
|
+
uploadedFile: AttachmentFile
|
|
5
|
+
|
|
6
|
+
constructor(uploadedFile) {
|
|
7
|
+
this.uploadedFile = uploadedFile
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get errors() {
|
|
11
|
+
if(this.#errors) return this.#errors
|
|
12
|
+
this.#errors = []
|
|
13
|
+
if(this.uploadedFile.size === 0) {
|
|
14
|
+
this.#errors.push("Must be larger than 0 bytes.")
|
|
15
|
+
}
|
|
16
|
+
return this.#errors
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#errors: Array<string>
|
|
20
|
+
}
|
|
@@ -34,4 +34,45 @@ describe('input-attachment', () => {
|
|
|
34
34
|
</input-attachment>
|
|
35
35
|
`);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
describe('formStateRestoreCallback (Firefox session restore)', () => {
|
|
39
|
+
async function newPage() {
|
|
40
|
+
const page = await newSpecPage({
|
|
41
|
+
components: [InputAttachment],
|
|
42
|
+
html: `<form><input-attachment name="attachments[]" multiple></input-attachment></form>`,
|
|
43
|
+
});
|
|
44
|
+
const instance: any = page.rootInstance;
|
|
45
|
+
const calls: any[] = [];
|
|
46
|
+
instance.internals = {
|
|
47
|
+
setFormValue: (value: any, state: any) => calls.push({ value, state }),
|
|
48
|
+
setValidity: () => {},
|
|
49
|
+
};
|
|
50
|
+
return { page, instance, calls };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it('saves the files as a JSON string state, not the FormData Firefox would flatten', async () => {
|
|
54
|
+
const { instance, calls } = await newPage();
|
|
55
|
+
instance.files = [{ value: 'signed-one' }, { value: 'signed-two' }];
|
|
56
|
+
instance.updateFormValue();
|
|
57
|
+
const last = calls[calls.length - 1];
|
|
58
|
+
expect(typeof last.state).toBe('string');
|
|
59
|
+
expect(JSON.parse(last.state).map((f: any) => f.value)).toEqual(['signed-one', 'signed-two']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rebuilds the attachments from the restored state string', async () => {
|
|
63
|
+
const { instance, calls } = await newPage();
|
|
64
|
+
const state = JSON.stringify([{ value: 'signed-one', filename: 'a.png', state: 'complete' }]);
|
|
65
|
+
instance.formStateRestoreCallback(state);
|
|
66
|
+
expect(instance.files.map((f: any) => f.value)).toEqual(['signed-one']);
|
|
67
|
+
instance.updateFormValue();
|
|
68
|
+
expect(calls[calls.length - 1].value.getAll('attachments[]')).toEqual(['signed-one']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('falls back to the current files when the restored state is the coerced "[object Object]"', async () => {
|
|
72
|
+
const { instance, calls } = await newPage();
|
|
73
|
+
instance.files = [{ value: 'signed-one' }];
|
|
74
|
+
instance.formStateRestoreCallback('[object Object]');
|
|
75
|
+
expect(calls[calls.length - 1].value.getAll('attachments[]')).toEqual(['signed-one']);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
37
78
|
});
|
|
@@ -125,7 +125,9 @@ export class InputAttachment {
|
|
|
125
125
|
// For has_one_attached: set single value (or empty string if none)
|
|
126
126
|
formData.set(this.name, values[0] || '')
|
|
127
127
|
}
|
|
128
|
-
this.
|
|
128
|
+
// State is a JSON string (this.value), not the FormData: Firefox flattens a FormData to {}
|
|
129
|
+
// in the session store and restores it as "[object Object]", but a string survives intact.
|
|
130
|
+
this.internals.setFormValue(formData, this.value)
|
|
129
131
|
|
|
130
132
|
// Update validity state - check for required and child validation errors
|
|
131
133
|
if (this.required && this.files.length === 0) {
|
|
@@ -147,6 +149,27 @@ export class InputAttachment {
|
|
|
147
149
|
this.files = []
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
// On session restore Firefox hands back the state we saved (this.value, a JSON string of the
|
|
153
|
+
// files). Rebuild the attachments from it so they survive a browser restart; fall back to
|
|
154
|
+
// re-deriving from the current files if the state is missing or malformed (e.g. the coerced
|
|
155
|
+
// "[object Object]", which is not valid JSON).
|
|
156
|
+
formStateRestoreCallback(state) {
|
|
157
|
+
if (this.restorable(state)) {
|
|
158
|
+
this.value = state
|
|
159
|
+
} else {
|
|
160
|
+
this.updateFormValue()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
restorable(state): boolean {
|
|
165
|
+
if (typeof state !== "string") return false
|
|
166
|
+
try {
|
|
167
|
+
return Array.isArray(JSON.parse(state))
|
|
168
|
+
} catch {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
150
173
|
handleFileInputChange = () => {
|
|
151
174
|
if (!this.fileInput?.files?.length) return
|
|
152
175
|
this.addFiles(this.fileInput.files)
|
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.6.
|
|
4
|
+
version: 0.6.5
|
|
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-
|
|
11
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activestorage
|
|
@@ -327,6 +327,7 @@ files:
|
|
|
327
327
|
- input-attachment/src/components/attachment-file/direct-upload-controller.tsx
|
|
328
328
|
- input-attachment/src/components/attachment-file/extensions.ts
|
|
329
329
|
- input-attachment/src/components/attachment-file/max.ts
|
|
330
|
+
- input-attachment/src/components/attachment-file/not-empty.ts
|
|
330
331
|
- input-attachment/src/components/attachment-file/readme.md
|
|
331
332
|
- input-attachment/src/components/attachment-preview/attachment-preview.css
|
|
332
333
|
- input-attachment/src/components/attachment-preview/attachment-preview.e2e.ts
|