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.
- checksums.yaml +7 -0
- data/.nvmrc +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +17 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/app/assets/javascripts/input-attachment.js +6021 -0
- data/app/controllers/bard/attachment_field/blobs_controller.rb +11 -0
- data/bard-attachment_field.gemspec +56 -0
- data/config/cucumber.yml +1 -0
- data/config/routes.rb +6 -0
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/gemfiles/rails_7.2.gemfile +7 -0
- data/gemfiles/rails_8.0.gemfile +7 -0
- data/gemfiles/rails_8.1.gemfile +7 -0
- data/input-attachment/.editorconfig +15 -0
- data/input-attachment/.github/workflows/test.yml +21 -0
- data/input-attachment/.gitignore +27 -0
- data/input-attachment/.prettierrc.json +13 -0
- data/input-attachment/CLAUDE.md +63 -0
- data/input-attachment/LICENSE +21 -0
- data/input-attachment/README.md +288 -0
- data/input-attachment/bin/log +2 -0
- data/input-attachment/bin/server +1 -0
- data/input-attachment/bin/setup +4 -0
- data/input-attachment/bun.lockb +0 -0
- data/input-attachment/bundle.js +3 -0
- data/input-attachment/jest-setup.js +24 -0
- data/input-attachment/package.json +56 -0
- data/input-attachment/src/components/attachment-file/accepts.ts +32 -0
- data/input-attachment/src/components/attachment-file/attachment-file.css +89 -0
- data/input-attachment/src/components/attachment-file/attachment-file.e2e.ts +11 -0
- data/input-attachment/src/components/attachment-file/attachment-file.spec.tsx +20 -0
- data/input-attachment/src/components/attachment-file/attachment-file.tsx +157 -0
- data/input-attachment/src/components/attachment-file/direct-upload-controller.tsx +100 -0
- data/input-attachment/src/components/attachment-file/extensions.ts +13 -0
- data/input-attachment/src/components/attachment-file/max.ts +46 -0
- data/input-attachment/src/components/attachment-file/readme.md +55 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.css +8 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.e2e.ts +11 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.spec.tsx +19 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.tsx +42 -0
- data/input-attachment/src/components/attachment-preview/readme.md +31 -0
- data/input-attachment/src/components/input-attachment/form-controller.tsx +146 -0
- data/input-attachment/src/components/input-attachment/input-attachment.css +100 -0
- data/input-attachment/src/components/input-attachment/input-attachment.e2e.ts +11 -0
- data/input-attachment/src/components/input-attachment/input-attachment.spec.tsx +37 -0
- data/input-attachment/src/components/input-attachment/input-attachment.tsx +353 -0
- data/input-attachment/src/components/input-attachment/readme.md +45 -0
- data/input-attachment/src/components.d.ts +175 -0
- data/input-attachment/src/global.d.ts +3 -0
- data/input-attachment/src/images/example.jpg +0 -0
- data/input-attachment/src/index.html +36 -0
- data/input-attachment/src/index.ts +1 -0
- data/input-attachment/src/utils/utils.spec.ts +19 -0
- data/input-attachment/src/utils/utils.ts +14 -0
- data/input-attachment/stencil.config.ts +43 -0
- data/input-attachment/test-mocks/file-drop.cjs +7 -0
- data/input-attachment/test-mocks/progress-bar.cjs +9 -0
- data/input-attachment/tsconfig.json +32 -0
- data/lib/bard/attachment_field/cucumber.rb +277 -0
- data/lib/bard/attachment_field/field.rb +33 -0
- data/lib/bard/attachment_field/form_builder.rb +12 -0
- data/lib/bard/attachment_field/version.rb +7 -0
- data/lib/bard/attachment_field.rb +20 -0
- data/lib/bard-attachment_field.rb +1 -0
- 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,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/)*
|