bard-attachment_field 0.1.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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.nvmrc +1 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Appraisals +17 -0
  6. data/CHANGELOG.md +15 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE +21 -0
  9. data/README.md +39 -0
  10. data/Rakefile +10 -0
  11. data/app/assets/javascripts/input-attachment.js +6021 -0
  12. data/app/controllers/bard/attachment_field/blobs_controller.rb +11 -0
  13. data/bard-attachment_field.gemspec +56 -0
  14. data/config/cucumber.yml +1 -0
  15. data/config/routes.rb +6 -0
  16. data/gemfiles/rails_7.1.gemfile +7 -0
  17. data/gemfiles/rails_7.2.gemfile +7 -0
  18. data/gemfiles/rails_8.0.gemfile +7 -0
  19. data/gemfiles/rails_8.1.gemfile +7 -0
  20. data/input-attachment/.editorconfig +15 -0
  21. data/input-attachment/.github/workflows/test.yml +21 -0
  22. data/input-attachment/.gitignore +27 -0
  23. data/input-attachment/.prettierrc.json +13 -0
  24. data/input-attachment/CLAUDE.md +63 -0
  25. data/input-attachment/LICENSE +21 -0
  26. data/input-attachment/README.md +288 -0
  27. data/input-attachment/bin/log +2 -0
  28. data/input-attachment/bin/server +1 -0
  29. data/input-attachment/bin/setup +4 -0
  30. data/input-attachment/bun.lockb +0 -0
  31. data/input-attachment/bundle.js +3 -0
  32. data/input-attachment/jest-setup.js +24 -0
  33. data/input-attachment/package.json +56 -0
  34. data/input-attachment/src/components/attachment-file/accepts.ts +32 -0
  35. data/input-attachment/src/components/attachment-file/attachment-file.css +89 -0
  36. data/input-attachment/src/components/attachment-file/attachment-file.e2e.ts +11 -0
  37. data/input-attachment/src/components/attachment-file/attachment-file.spec.tsx +20 -0
  38. data/input-attachment/src/components/attachment-file/attachment-file.tsx +157 -0
  39. data/input-attachment/src/components/attachment-file/direct-upload-controller.tsx +100 -0
  40. data/input-attachment/src/components/attachment-file/extensions.ts +13 -0
  41. data/input-attachment/src/components/attachment-file/max.ts +46 -0
  42. data/input-attachment/src/components/attachment-file/readme.md +55 -0
  43. data/input-attachment/src/components/attachment-preview/attachment-preview.css +8 -0
  44. data/input-attachment/src/components/attachment-preview/attachment-preview.e2e.ts +11 -0
  45. data/input-attachment/src/components/attachment-preview/attachment-preview.spec.tsx +19 -0
  46. data/input-attachment/src/components/attachment-preview/attachment-preview.tsx +42 -0
  47. data/input-attachment/src/components/attachment-preview/readme.md +31 -0
  48. data/input-attachment/src/components/input-attachment/form-controller.tsx +146 -0
  49. data/input-attachment/src/components/input-attachment/input-attachment.css +100 -0
  50. data/input-attachment/src/components/input-attachment/input-attachment.e2e.ts +11 -0
  51. data/input-attachment/src/components/input-attachment/input-attachment.spec.tsx +37 -0
  52. data/input-attachment/src/components/input-attachment/input-attachment.tsx +353 -0
  53. data/input-attachment/src/components/input-attachment/readme.md +45 -0
  54. data/input-attachment/src/components.d.ts +175 -0
  55. data/input-attachment/src/global.d.ts +3 -0
  56. data/input-attachment/src/images/example.jpg +0 -0
  57. data/input-attachment/src/index.html +36 -0
  58. data/input-attachment/src/index.ts +1 -0
  59. data/input-attachment/src/utils/utils.spec.ts +19 -0
  60. data/input-attachment/src/utils/utils.ts +14 -0
  61. data/input-attachment/stencil.config.ts +43 -0
  62. data/input-attachment/test-mocks/file-drop.cjs +7 -0
  63. data/input-attachment/test-mocks/progress-bar.cjs +9 -0
  64. data/input-attachment/tsconfig.json +32 -0
  65. data/lib/bard/attachment_field/cucumber.rb +277 -0
  66. data/lib/bard/attachment_field/field.rb +33 -0
  67. data/lib/bard/attachment_field/form_builder.rb +12 -0
  68. data/lib/bard/attachment_field/version.rb +7 -0
  69. data/lib/bard/attachment_field.rb +20 -0
  70. data/lib/bard-attachment_field.rb +1 -0
  71. metadata +409 -0
@@ -0,0 +1,32 @@
1
+ import { AttachmentFile } from './attachment-file'
2
+
3
+ export default class Accepts {
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
+
14
+ const accepts = this.uploadedFile.accepts ? this.uploadedFile.accepts.split(/,\s*/) : []
15
+
16
+ if(accepts.length > 0 && !accepts.includes(this.uploadedFile.filetype)) {
17
+ this.#errors.push(`Must be a ${this.joinWords(accepts)}.`)
18
+ }
19
+ return this.#errors
20
+ }
21
+
22
+ #errors: Array<string>
23
+
24
+ private joinWords(words) {
25
+ if(words.length >= 3) {
26
+ return (words.slice(0, -1) + [`or ${words.at(-1)}`]).join(", ")
27
+ } else {
28
+ return words.join(" or ")
29
+ }
30
+ }
31
+ }
32
+
@@ -0,0 +1,89 @@
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ max-width: 100%;
5
+ font-size: 13px;
6
+ }
7
+
8
+ figure{
9
+ margin: 0;
10
+ }
11
+
12
+ .progress-details{
13
+ position: relative;
14
+ display: flex;
15
+ align-items: center;
16
+ }
17
+
18
+ progress-bar{
19
+ flex: 1 0;
20
+ padding: 0 10px;
21
+ }
22
+
23
+ progress-bar.pending{
24
+ opacity: 0.5;
25
+ }
26
+
27
+ progress-bar.complete {
28
+ opacity: 0.8;
29
+ }
30
+
31
+ progress-bar:not(.complete) + .progress-icon{
32
+ display: none;
33
+ }
34
+
35
+ progress-bar.complete + .progress-icon{
36
+ content: url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"><g><path d="M6.3,9.1c0.2,0,0.5,0.1,0.7,0.4c0.5,0.5,1,1,1.4,1.4c0.3,0.3,0.3,0.3,0.6,0c1.4-1.3,2.7-2.6,4-3.9c0.3-0.3,0.6-0.4,1-0.4 c0.5,0.1,0.9,0.6,0.7,1.1c-0.1,0.2-0.2,0.4-0.3,0.6c-1.6,1.6-3.2,3.2-4.8,4.8c-0.5,0.5-1,0.5-1.6,0c-0.8-0.7-1.5-1.5-2.3-2.3 c-0.3-0.3-0.5-0.6-0.3-1.1C5.5,9.3,5.8,9.1,6.3,9.1z"/></g></svg>');
37
+ filter: invert(100%);
38
+ }
39
+
40
+ .progress-icon{
41
+ display: inline-block;
42
+ flex: 0 0 20px;
43
+ width: 28px;
44
+ height: 28px;
45
+ background-size: contain;
46
+ position: absolute;
47
+ right: 30px;
48
+ z-index: 1;
49
+ }
50
+
51
+ progress-bar.error{
52
+ background: #f8b3b1;
53
+ background: rgba(74, 70, 70, 0.25);
54
+ opacity: 1;
55
+ }
56
+
57
+ .progress-bar a{color: #fff;}
58
+
59
+ .download-link{
60
+ padding-right: 20px;
61
+ color: #fff;
62
+ }
63
+
64
+ .remove-media{
65
+ display: inline-block;
66
+ content: url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><g><path d="M0,19.9C0.2,8.5,9.2-0.1,20.1,0C31.8,0.1,40.2,9.5,40,20.4c-0.2,11-8.9,19.7-20.1,19.6C8,39.9,0,30.5,0,19.9z M20,3.7 c-9,0-16.3,7-16.3,16.2C3.7,29,10.9,36.3,20,36.3c9,0,16.3-7.1,16.4-16.3C36.3,11,29.2,3.8,20,3.7z"/><path d="M17.3,20c-0.2-0.2-0.3-0.4-0.5-0.6c-1-1-2-1.9-2.9-2.9c-0.5-0.5-0.8-1.1-0.7-1.9c0.1-0.7,0.5-1.2,1.2-1.4 c0.8-0.2,1.5,0,2.1,0.6c1,1,2,2,3,3.1c0.3,0.4,0.6,0.3,0.9,0c1-1,2-2,3-3c0.3-0.3,0.7-0.5,1.1-0.6c0.8-0.2,1.6,0.1,2,0.8 c0.4,0.8,0.3,1.7-0.4,2.4c-1,1-2,2-3,3c-0.2,0.2-0.3,0.4-0.5,0.6c1.2,1.2,2.3,2.3,3.4,3.4c0.6,0.6,0.9,1.3,0.6,2.2 c-0.4,1.1-1.7,1.6-2.6,1c-0.3-0.2-0.5-0.4-0.8-0.6c-1-1-1.9-1.9-2.9-2.9c-0.3-0.3-0.5-0.3-0.9,0c-1,1-2,2.1-3,3 c-0.4,0.4-1,0.6-1.5,0.8c-0.6,0.1-1.2-0.2-1.5-0.8c-0.4-0.6-0.5-1.3-0.1-1.9c0.2-0.3,0.4-0.5,0.6-0.7C15.1,22.3,16.2,21.2,17.3,20z "/></g></svg>');
67
+ flex: 0 0 25px;
68
+ width: 25px;
69
+ height: 20px;
70
+ align-items: center;
71
+ opacity: 0.25;
72
+ }
73
+
74
+ .remove-media:hover{
75
+ opacity: 1;
76
+ filter: invert(50%)sepia(100%)saturate(10000%);
77
+ }
78
+
79
+ .remove-media span{
80
+ display: inline-block;
81
+ text-indent: -9999px;
82
+ color: transparent;
83
+ }
84
+
85
+ .validation-error{
86
+ color: #c00;
87
+ font-size: 12px;
88
+ margin: 4px 0 0 10px;
89
+ }
@@ -0,0 +1,11 @@
1
+ import { newE2EPage } from '@stencil/core/testing';
2
+
3
+ describe('attachment-file', () => {
4
+ it('renders', async () => {
5
+ const page = await newE2EPage();
6
+ await page.setContent('<attachment-file></attachment-file>');
7
+
8
+ const element = await page.find('attachment-file');
9
+ expect(element).toHaveClass('hydrated');
10
+ });
11
+ });
@@ -0,0 +1,20 @@
1
+ // Mock rails-request-json to avoid ES module issues
2
+ jest.mock('rails-request-json', () => ({
3
+ get: jest.fn(() => Promise.resolve({}))
4
+ }));
5
+
6
+ import { newSpecPage } from '@stencil/core/testing';
7
+ import { AttachmentFile } from './attachment-file';
8
+
9
+ describe('attachment-file', () => {
10
+
11
+ it('renders', async () => {
12
+ const page = await newSpecPage({
13
+ components: [AttachmentFile],
14
+ html: `<attachment-file></attachment-file>`,
15
+ });
16
+
17
+ // Just check that it renders without error
18
+ expect(page.root.tagName).toBe('ATTACHMENT-FILE');
19
+ });
20
+ });
@@ -0,0 +1,157 @@
1
+ import { Component, Prop, Element, Watch, Host, h } from '@stencil/core'
2
+ import { Listen, Event, EventEmitter } from '@stencil/core'
3
+ import DirectUploadController from './direct-upload-controller'
4
+ import Max from './max'
5
+ import Accepts from './accepts'
6
+ import Extensions from './extensions'
7
+ import { get } from 'rails-request-json'
8
+
9
+ @Component({
10
+ tag: 'attachment-file',
11
+ styleUrl: 'attachment-file.css',
12
+ shadow: true,
13
+ })
14
+ export class AttachmentFile {
15
+ @Element() el
16
+
17
+ @Prop({ reflect: true, mutable: true }) name: string
18
+ @Prop({ reflect: true, mutable: true }) accepts: string
19
+ @Prop({ reflect: true, mutable: true }) max: number
20
+ @Prop({ reflect: true, mutable: true }) url: string
21
+
22
+ @Prop({ reflect: true, mutable: true }) value: string = ""
23
+ @Prop({ reflect: true, mutable: true }) filename: string
24
+ @Prop({ reflect: true, mutable: true }) src: string
25
+ @Prop({ reflect: true, mutable: true }) filetype: string
26
+ @Prop({ reflect: true, mutable: true }) size: number
27
+ @Prop({ reflect: true, mutable: true }) state: string = "complete"
28
+ @Prop({ reflect: true, mutable: true }) percent: number = 100
29
+ @Prop({ reflect: true, mutable: true }) preview: boolean = true
30
+
31
+ @Prop() validationMessage: string
32
+
33
+ @Event({ eventName: "attachment-file:remove" }) removeEvent: EventEmitter
34
+ @Event({ eventName: "attachment-file:validation" }) validationEvent: EventEmitter
35
+
36
+ private removeClicked = event => {
37
+ event.stopPropagation()
38
+ event.preventDefault()
39
+ this.controller?.cancel()
40
+ this.removeEvent.emit(this)
41
+ }
42
+
43
+ controller: DirectUploadController
44
+ _file: File
45
+ validationError: string = ""
46
+
47
+ componentWillLoad() {
48
+ this.setMissingFiletype()
49
+ }
50
+
51
+ get file() {
52
+ return this._file
53
+ }
54
+
55
+ set file(file: any) {
56
+ this.src = URL.createObjectURL(file)
57
+ this.filename = file.name
58
+ this.size = file.size
59
+ this.state = "pending"
60
+ this.percent = 0
61
+ this._file = file
62
+ }
63
+
64
+ set signedId(val) {
65
+ if(this.value !== val) {
66
+ get(`/rails/active_storage/blobs/info/${val}`).then(blob => {
67
+ this.src = `/rails/active_storage/blobs/redirect/${val}/${blob.filename}`
68
+ this.filename = blob.filename
69
+ this.size = blob.byte_size
70
+ this.state = "complete"
71
+ this.percent = 100
72
+ this.value = val
73
+ })
74
+ }
75
+ }
76
+
77
+ @Watch("filename")
78
+ setMissingFiletype(_value?, _previousValue?) {
79
+ if(!this.filetype && this.filename) {
80
+ this.filetype = Extensions.getFileType(this.filename)
81
+ }
82
+ }
83
+
84
+ @Listen("direct-upload:initialize")
85
+ @Listen("direct-upload:start")
86
+ start(_event) {
87
+ this.state = "pending"
88
+ this.percent = 0
89
+ }
90
+
91
+ @Listen("direct-upload:progress")
92
+ progress(event) {
93
+ const { progress } = event.detail
94
+ this.percent = progress
95
+ }
96
+
97
+ @Listen("direct-upload:error")
98
+ error(event) {
99
+ event.preventDefault()
100
+ const { error } = event.detail
101
+ this.state = "error"
102
+ this.validationError = error
103
+ }
104
+
105
+ @Listen("direct-upload:end")
106
+ end(_event) {
107
+ if(this.state !== "error") {
108
+ this.state = "complete"
109
+ this.percent = 100
110
+ }
111
+ }
112
+
113
+ render() {
114
+ return (
115
+ <Host>
116
+ <slot>
117
+ </slot>
118
+ <figure>
119
+ <div class="progress-details">
120
+ <progress-bar percent={this.percent} class={this.state}>
121
+ <a class="download-link" href={this.src} download={this.filename} onClick={e => e.stopPropagation()}>
122
+ {this.filename}
123
+ </a>
124
+ </progress-bar>
125
+ <span class="progress-icon"></span>
126
+ <a class="remove-media" onClick={this.removeClicked} href="#">
127
+ <span>Remove media</span>
128
+ </a>
129
+ </div>
130
+ {this.validationError ? <p class="validation-error">{this.validationError}</p> : ''}
131
+ {this.preview ? <attachment-preview src={this.src} filetype={this.filetype}></attachment-preview> : ''}
132
+ </figure>
133
+ </Host>
134
+ )
135
+ }
136
+
137
+ componentDidLoad() {
138
+ if(this.state == "pending" && this._file) {
139
+ if(this.checkValidity()) {
140
+ this.controller = new DirectUploadController(this.el, this._file)
141
+ this.controller.dispatch("initialize", { controller: this.controller })
142
+ } else {
143
+ this.state = "error"
144
+ }
145
+ }
146
+ }
147
+
148
+ checkValidity() {
149
+ let errors = []
150
+ errors.push(...new Accepts(this).errors)
151
+ errors.push(...new Max(this).errors)
152
+ this.validationError = errors.join(" ")
153
+ // Notify parent of validation state change
154
+ this.validationEvent.emit({ valid: errors.length === 0, error: this.validationError })
155
+ return errors.length === 0
156
+ }
157
+ }
@@ -0,0 +1,100 @@
1
+ import { DirectUpload } from "@rails/activestorage"
2
+ import { AttachmentFile } from "./attachment-file"
3
+
4
+ export default class DirectUploadController {
5
+ uploadedFile: AttachmentFile
6
+ file: File
7
+ directUpload: any
8
+ recordXHR: XMLHttpRequest
9
+ uploadXHR: XMLHttpRequest
10
+ callback = null
11
+
12
+ constructor(uploadedFile, file: File) {
13
+ this.uploadedFile = uploadedFile
14
+ this.file = file
15
+ this.directUpload = new DirectUpload(this.file, this.uploadedFile.url, this)
16
+ }
17
+
18
+ cancel() {
19
+ this.directUpload.url = null
20
+ this.abortXHR(this.recordXHR)
21
+ this.abortXHR(this.uploadXHR)
22
+ }
23
+
24
+ abortXHR(xhr) {
25
+ if(!xhr) return
26
+ xhr.addEventListener("abort", () => {
27
+ this.complete("aborted", {})
28
+ })
29
+ xhr.abort()
30
+ }
31
+
32
+ start(callback) {
33
+ this.callback = callback
34
+ this.dispatch("start")
35
+ this.directUpload.create((error, attributes) => {
36
+ this.complete(error, attributes)
37
+ })
38
+ }
39
+
40
+ complete(error, _attributes) {
41
+ if (error) {
42
+ this.dispatchError(error)
43
+ }
44
+ this.dispatch("end")
45
+ this.callback(error)
46
+ }
47
+
48
+ uploadRequestDidProgress(event) {
49
+ const progress = event.loaded / event.total * 100;
50
+ if (progress) {
51
+ this.dispatch("progress", {
52
+ progress: progress
53
+ });
54
+ }
55
+ }
56
+ dispatch(name, detail = {}) {
57
+ return dispatchEvent(this.uploadedFile, `direct-upload:${name}`, {
58
+ detail: {
59
+ ...detail,
60
+ file: this.file,
61
+ id: this.directUpload.id,
62
+ }
63
+ });
64
+ }
65
+ dispatchError(error) {
66
+ this.dispatch("error", {
67
+ error: error
68
+ })
69
+ }
70
+ directUploadWillCreateBlobWithXHR(xhr) {
71
+ this.recordXHR = xhr
72
+ this.dispatch("before-blob-request", {
73
+ xhr: xhr
74
+ });
75
+ }
76
+ directUploadWillStoreFileWithXHR(xhr) {
77
+ this.uploadXHR = xhr
78
+ this.uploadedFile.value = this.recordXHR.response.signed_id
79
+ this.dispatch("before-storage-request", {
80
+ xhr: xhr
81
+ });
82
+ xhr.upload.addEventListener("progress", (event => this.uploadRequestDidProgress(event)));
83
+ }
84
+ }
85
+
86
+ function dispatchEvent(element: any, type, eventInit = {} as any) {
87
+ const {disabled: disabled} = element;
88
+ const {bubbles: bubbles, cancelable: cancelable, detail: detail} = eventInit;
89
+ const event = document.createEvent("Event") as any;
90
+ event.initEvent(type, bubbles || true, cancelable || true);
91
+ event.detail = detail || {};
92
+ try {
93
+ element.disabled = false;
94
+ element.dispatchEvent(event);
95
+ } finally {
96
+ element.disabled = disabled;
97
+ }
98
+ return event;
99
+ }
100
+
@@ -0,0 +1,13 @@
1
+ export default {
2
+ 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"],
3
+ 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"],
4
+ pdf: ["pdf"],
5
+
6
+ getFileType: function(filename) {
7
+ const normalized = filename.toString().split(".").at(-1).toLowerCase().trim()
8
+ if(this.video.includes(normalized)) return "video"
9
+ if(this.image.includes(normalized)) return "image"
10
+ if(this.pdf.includes(normalized)) return "pdf"
11
+ return "unknown"
12
+ }
13
+ }
@@ -0,0 +1,46 @@
1
+ import { AttachmentFile } from './attachment-file'
2
+
3
+ export default class Max {
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.checkValidity()) {
14
+ this.#errors.push(this.errorMessage)
15
+ }
16
+ return this.#errors
17
+ }
18
+
19
+ #errors: Array<string>
20
+
21
+ checkValidity() {
22
+ if(!this.uploadedFile.max) return true
23
+ return this.uploadedFile.size <= this.uploadedFile.max
24
+ }
25
+
26
+ get errorMessage() {
27
+ return [
28
+ `Must be smaller than ${this.formatBytes(this.uploadedFile.max)},`,
29
+ `and "${this.uploadedFile.filename}" is ${this.formatBytes(this.uploadedFile.size)}.`,
30
+ `Please attach a smaller file.`,
31
+ ].join(" ")
32
+ }
33
+
34
+ formatBytes(bytes, decimals = 2) {
35
+ if (bytes === 0) return '0 Bytes';
36
+
37
+ const k = 1024;
38
+ const dm = decimals < 0 ? 0 : decimals;
39
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
40
+
41
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
42
+
43
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i];
44
+ }
45
+ }
46
+
@@ -0,0 +1,55 @@
1
+ # attachment-file
2
+
3
+
4
+
5
+ <!-- Auto Generated Below -->
6
+
7
+
8
+ ## Properties
9
+
10
+ | Property | Attribute | Description | Type | Default |
11
+ | ------------------- | -------------------- | ----------- | --------- | ------------ |
12
+ | `accepts` | `accepts` | | `string` | `undefined` |
13
+ | `filename` | `filename` | | `string` | `undefined` |
14
+ | `filetype` | `filetype` | | `string` | `undefined` |
15
+ | `max` | `max` | | `number` | `undefined` |
16
+ | `name` | `name` | | `string` | `undefined` |
17
+ | `percent` | `percent` | | `number` | `100` |
18
+ | `preview` | `preview` | | `boolean` | `true` |
19
+ | `size` | `size` | | `number` | `undefined` |
20
+ | `src` | `src` | | `string` | `undefined` |
21
+ | `state` | `state` | | `string` | `"complete"` |
22
+ | `url` | `url` | | `string` | `undefined` |
23
+ | `validationMessage` | `validation-message` | | `string` | `undefined` |
24
+ | `value` | `value` | | `string` | `""` |
25
+
26
+
27
+ ## Events
28
+
29
+ | Event | Description | Type |
30
+ | ---------------------------- | ----------- | ------------------ |
31
+ | `attachment-file:remove` | | `CustomEvent<any>` |
32
+ | `attachment-file:validation` | | `CustomEvent<any>` |
33
+
34
+
35
+ ## Dependencies
36
+
37
+ ### Used by
38
+
39
+ - [input-attachment](../input-attachment)
40
+
41
+ ### Depends on
42
+
43
+ - [attachment-preview](../attachment-preview)
44
+
45
+ ### Graph
46
+ ```mermaid
47
+ graph TD;
48
+ attachment-file --> attachment-preview
49
+ input-attachment --> attachment-file
50
+ style attachment-file fill:#f9f,stroke:#333,stroke-width:4px
51
+ ```
52
+
53
+ ----------------------------------------------
54
+
55
+ *Built with [StencilJS](https://stenciljs.com/)*
@@ -0,0 +1,8 @@
1
+ :host {
2
+ display: block;
3
+ font-size: 13px;
4
+ }
5
+ img, video {
6
+ max-width: 100%;
7
+ margin-top: 10px;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { newE2EPage } from '@stencil/core/testing';
2
+
3
+ describe('attachment-preview', () => {
4
+ it('renders', async () => {
5
+ const page = await newE2EPage();
6
+ await page.setContent('<attachment-preview></attachment-preview>');
7
+
8
+ const element = await page.find('attachment-preview');
9
+ expect(element).toHaveClass('hydrated');
10
+ });
11
+ });
@@ -0,0 +1,19 @@
1
+ import { newSpecPage } from '@stencil/core/testing';
2
+ import { AttachmentPreview } from './attachment-preview';
3
+
4
+ describe('attachment-preview', () => {
5
+ it('renders', async () => {
6
+ const page = await newSpecPage({
7
+ components: [AttachmentPreview],
8
+ html: `<attachment-preview></attachment-preview>`,
9
+ });
10
+ expect(page.root).toEqualHtml(`
11
+ <attachment-preview class="other">
12
+ <mock:shadow-root>
13
+ This file does not offer a preview
14
+ <slot></slot>
15
+ </mock:shadow-root>
16
+ </attachment-preview>
17
+ `);
18
+ });
19
+ });
@@ -0,0 +1,42 @@
1
+ import { Component, Prop, Host, h } from '@stencil/core';
2
+
3
+ @Component({
4
+ tag: 'attachment-preview',
5
+ styleUrl: 'attachment-preview.css',
6
+ shadow: true,
7
+ })
8
+ export class AttachmentPreview {
9
+ @Prop({ reflect: true }) src: string
10
+ @Prop({ reflect: true }) filetype: string
11
+
12
+ render() {
13
+ return (
14
+ <Host class={this.computeClass()}>
15
+ {this.isImage() && <img src={this.src} />}
16
+ {this.isVideo() && <video src={this.src} onClick={toggle} />}
17
+ {this.isOther() && "This file does not offer a preview"}
18
+ <slot></slot>
19
+ </Host>
20
+ );
21
+ }
22
+
23
+ private computeClass() {
24
+ if(this.isImage()) return "image"
25
+ if(this.isVideo()) return "video"
26
+ return "other"
27
+ }
28
+
29
+ private isImage() {
30
+ return this.filetype == "image"
31
+ }
32
+
33
+ private isVideo() {
34
+ return this.filetype == "video"
35
+ }
36
+
37
+ private isOther() {
38
+ return !this.isImage() && !this.isVideo()
39
+ }
40
+ }
41
+
42
+ const toggle = function() { this.paused ? this.play() : this.pause(); return false }
@@ -0,0 +1,31 @@
1
+ # attachment-preview
2
+
3
+
4
+
5
+ <!-- Auto Generated Below -->
6
+
7
+
8
+ ## Properties
9
+
10
+ | Property | Attribute | Description | Type | Default |
11
+ | ---------- | ---------- | ----------- | -------- | ----------- |
12
+ | `filetype` | `filetype` | | `string` | `undefined` |
13
+ | `src` | `src` | | `string` | `undefined` |
14
+
15
+
16
+ ## Dependencies
17
+
18
+ ### Used by
19
+
20
+ - [attachment-file](../attachment-file)
21
+
22
+ ### Graph
23
+ ```mermaid
24
+ graph TD;
25
+ attachment-file --> attachment-preview
26
+ style attachment-preview fill:#f9f,stroke:#333,stroke-width:4px
27
+ ```
28
+
29
+ ----------------------------------------------
30
+
31
+ *Built with [StencilJS](https://stenciljs.com/)*