active_storage_drag_and_drop 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,91 @@
1
- .asdndzone {
2
- min-height: 100px;
3
- border: 2px solid rgba(0, 0, 0, 0.3);
4
- background: rgba(0,0,0,0.1);
5
- padding-top: 60px;
6
- text-align: center;
1
+ label.asdndzone * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ label.asdndzone {
6
+ box-sizing: border-box;
7
+ display: block;
8
+ border: 3px solid #c8ce3e;
9
+ border-radius: 6px;
10
+ min-height: 220px;
11
+ max-width: 310px;
7
12
  width: 100%;
13
+ padding: 99px 20px 20px;
14
+ text-align: center;
15
+ font-family: 'Nunito Sans', sans-serif;
16
+ font-size: 21px;
17
+ letter-spacing: 0.6px;
18
+ }
19
+
20
+ label.asdndzone .asdndz-highlight {
21
+ color: #8dc63f;
22
+ }
23
+
24
+ label.asdndzone .asdndz__icon-container {
25
+ text-align: left;
26
+ }
27
+
28
+ label.asdndzone .direct-upload {
29
+ display: inline-block;
30
+ position: relative;
31
+ border-radius: 10px;
32
+ height: 18px;
33
+ min-width: 126px;
34
+ background-color: #f5f5f5;
35
+ font-family: 'Nunito Sans', sans-serif;
36
+ font-size: 11px;
37
+ letter-spacing: 0.3px;
38
+ color: #212121;
39
+ margin-top: 8px;
40
+ padding: 5px 10px;
41
+ }
42
+
43
+ label.asdndzone .direct-upload__complete {
44
+ background-color: #c8ce3e;
45
+ }
46
+
47
+ label.asdndzone .direct-upload__progress {
48
+ opacity: 0.57;
49
+ background-color: #c8ce3e;
50
+ border-radius: 10px;
51
+ position: absolute;
52
+ top: 0;
53
+ left: 0;
54
+ bottom: 0;
55
+ height: 100%;
56
+ transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
57
+ transform: translate3d(0, 0, 0);
58
+ }
59
+
60
+ label.asdndzone .direct-upload__remove {
8
61
  display: inline-block;
62
+ vertical-align: top;
63
+ margin-top: 13px;
64
+ font-family: 'Nunito Sans', sans-serif;
65
+ font-size: 11px;
66
+ font-weight: bold;
67
+ text-decoration: none;
68
+ color: #212121;
69
+ }
70
+
71
+ label.asdndzone .direct-upload__filename {
72
+ position: relative;
73
+ max-width: 190px;
74
+ overflow: hidden;
75
+ display: inline-block;
76
+ text-overflow: ellipsis;
77
+ white-space: nowrap;
78
+ }
79
+
80
+ label.asdndzone .direct-upload__filesize {
81
+ position: relative;
82
+ float: right;
83
+ font-size: 10px;
84
+ letter-spacing: 0.1px;
85
+ color: #707e87;
86
+ padding-left: 7px;
87
+ }
88
+
89
+ label.asdndzone .direct-upload[data-direct-upload-id='error'] {
90
+ border: 1px solid red;
9
91
  }
@@ -0,0 +1,77 @@
1
+ // @flow
2
+
3
+ import { fileSizeSI } from './helpers'
4
+
5
+ export function errorUI (event: CustomEvent) {
6
+ let { id } = event.detail
7
+ const { error, iconContainer, file } = event.detail
8
+ if (event.defaultPrevented) return
9
+
10
+ if (!id) {
11
+ id = 'error'
12
+ window.ActiveStorageDragAndDrop.paintUploadIcon(iconContainer, id, file, false)
13
+ }
14
+ const element = document.querySelector(`[data-direct-upload-id="${id}"] .direct-upload`)
15
+ if (!element) return
16
+
17
+ element.setAttribute('title', error)
18
+ element.classList.add('direct-upload--error')
19
+ }
20
+
21
+ export function endUI (event: CustomEvent) {
22
+ const { id } = event.detail
23
+ if (!id || event.defaultPrevented) return
24
+
25
+ const element = document.querySelector(`[data-direct-upload-id="${id}"] .direct-upload`)
26
+ if (!element) return
27
+
28
+ const classes = element.classList
29
+ classes.remove('direct-upload--pending')
30
+ classes.add('direct-upload--complete')
31
+ }
32
+
33
+ export function initializeUI (event: CustomEvent) {
34
+ if (event.defaultPrevented) return
35
+
36
+ const { id, file, iconContainer } = event.detail
37
+ window.ActiveStorageDragAndDrop.paintUploadIcon(iconContainer, id, file, false)
38
+ }
39
+
40
+ export function progressUI (event: CustomEvent) {
41
+ if (event.defaultPrevented) return
42
+
43
+ const { id, progress } = event.detail
44
+ const progressElement = document.querySelector(`[data-direct-upload-id="${id}"] .direct-upload__progress`)
45
+ if (progressElement) progressElement.style.width = `${progress}%`
46
+ }
47
+
48
+ export function placeholderUI (event: CustomEvent) {
49
+ if (event.defaultPrevented) return
50
+
51
+ const { id, file, iconContainer } = event.detail
52
+ window.ActiveStorageDragAndDrop.paintUploadIcon(iconContainer, id, file, true)
53
+ }
54
+
55
+ export function cancelUI (event: CustomEvent) {
56
+ if (event.defaultPrevented) return
57
+
58
+ const { id } = event.detail
59
+ document.querySelectorAll(`[data-direct-upload-id="${id}"]`).forEach(element => {
60
+ element.remove()
61
+ })
62
+ }
63
+
64
+ export function paintUploadIcon (iconContainer: HTMLElement, id: string | number, file: File, complete: boolean) {
65
+ const uploadStatus = (complete ? 'complete' : 'pending')
66
+ const progress = (complete ? 100 : 0)
67
+ iconContainer.insertAdjacentHTML('beforeend', `
68
+ <div data-direct-upload-id="${id}">
69
+ <div class="direct-upload direct-upload--${uploadStatus}">
70
+ <div class="direct-upload__progress" style="width: ${progress}%"></div>
71
+ <span class="direct-upload__filename">${file.name}</span>
72
+ <span class="direct-upload__filesize">${fileSizeSI(file.size)}</span>
73
+ </div>
74
+ <a href='remove' class='direct-upload__remove'>X</a>
75
+ </div>
76
+ `)
77
+ }
@@ -0,0 +1,87 @@
1
+ // @flow
2
+
3
+ import { dispatchEvent } from './helpers'
4
+ import { endUI, errorUI, cancelUI } from './default_ui'
5
+ import { DragAndDropUploadController } from './drag_and_drop_upload_controller'
6
+
7
+ export class DragAndDropFormController {
8
+ form: HTMLFormElement;
9
+ uploadControllers: Array<DragAndDropUploadController>;
10
+
11
+ constructor (form: HTMLFormElement) {
12
+ this.form = form
13
+ this.uploadControllers = []
14
+ }
15
+
16
+ start (callback: Function) {
17
+ const startUploadControllers = () => {
18
+ const nextUploadController = this.uploadControllers.shift()
19
+ if (nextUploadController)
20
+ nextUploadController.start(error => {
21
+ if (error) {
22
+ this.dispatchError(error, nextUploadController)
23
+ callback(error)
24
+ } else startUploadControllers()
25
+ })
26
+ else {
27
+ callback()
28
+ const event = this.dispatch('end')
29
+ endUI(event)
30
+ }
31
+ }
32
+ this.dispatch('start')
33
+ startUploadControllers()
34
+ }
35
+
36
+ dispatch (name: string, detail: {} = {}) {
37
+ return dispatchEvent(this.form, `dnd-uploads:${name}`, { detail })
38
+ }
39
+
40
+ dispatchError (error: Error, uploadController: DragAndDropUploadController) {
41
+ const { file, iconContainer } = uploadController
42
+ const event = dispatchEvent(
43
+ this.form,
44
+ 'dnd-upload:error',
45
+ { detail: { error, file, iconContainer } }
46
+ )
47
+ errorUI(event)
48
+ }
49
+
50
+ queueUpload (input: HTMLInputElement, file: File) {
51
+ if (!input.getAttribute('multiple')) this.unqueueUploadsPerInput(input)
52
+ try {
53
+ this.uploadControllers.push(new DragAndDropUploadController(input, file))
54
+ } catch (error) {
55
+ const detail = {
56
+ id: null,
57
+ file: file,
58
+ error: error,
59
+ iconContainer: document.getElementById(input.dataset.iconContainerId)
60
+ }
61
+ dispatchErrorWithoutAttachment(input, detail)
62
+ }
63
+ }
64
+
65
+ unqueueUploadsPerInput (input: HTMLInputElement) {
66
+ const zone = input.closest('label.asdndzone')
67
+ if (!zone) return
68
+
69
+ zone.querySelectorAll('[data-direct-upload-id]').forEach(element => { element.remove() })
70
+ this.uploadControllers.splice(0, this.uploadControllers.length)
71
+ }
72
+
73
+ unqueueUpload (id: number) {
74
+ const index = this.uploadControllers.findIndex(uploader => (uploader.upload.id === id))
75
+ const uploadController = this.uploadControllers[index]
76
+ this.uploadControllers.splice(index, 1)
77
+ const event = uploadController && (uploadController.dispatch instanceof Function)
78
+ ? uploadController.dispatch('cancel')
79
+ : this.dispatch('cancel', { id: 'error' })
80
+ cancelUI(event)
81
+ }
82
+ }
83
+
84
+ function dispatchErrorWithoutAttachment (input, detail) {
85
+ const event = dispatchEvent(input, 'dnd-upload:error', { detail })
86
+ errorUI(event)
87
+ }
@@ -0,0 +1,102 @@
1
+ // @flow
2
+
3
+ import { dispatchEvent, fileSizeSI } from './helpers'
4
+ import { endUI, errorUI, initializeUI, progressUI } from './default_ui'
5
+ import { DirectUpload } from 'activestorage'
6
+ const eventFamily = 'dnd-upload'
7
+
8
+ export class DragAndDropUploadController {
9
+ input: HTMLInputElement;
10
+ form: HTMLFormElement;
11
+ url: string;
12
+ iconContainer: HTMLElement;
13
+ file: File;
14
+ upload: DirectUpload;
15
+
16
+ constructor (input: HTMLInputElement, file: File) {
17
+ validate(input, file)
18
+ const form = input.closest('form')
19
+ const iconContainer = document.getElementById(input.dataset.iconContainerId)
20
+ if (!(form instanceof HTMLFormElement && iconContainer)) return
21
+
22
+ this.input = input
23
+ this.form = form
24
+ this.url = this.input.dataset.directUploadUrl
25
+ this.iconContainer = iconContainer
26
+ this.file = file
27
+ this.upload = new DirectUpload(this.file, this.url, this)
28
+ const event = this.dispatch('initialize')
29
+ initializeUI(event)
30
+ }
31
+
32
+ start (callback: Function) {
33
+ this.dispatch('start')
34
+ this.upload.create((error, blob) => {
35
+ if (error) {
36
+ // Handle the error
37
+ this.dispatchError(error)
38
+ callback(error)
39
+ } else {
40
+ // Add an appropriately-named hidden input to the form with a
41
+ // value of blob.signed_id so that the blob ids will be
42
+ // transmitted in the normal upload flow
43
+ const hiddenField = document.createElement('input')
44
+ hiddenField.setAttribute('type', 'hidden')
45
+ hiddenField.setAttribute('value', blob.signed_id)
46
+ hiddenField.setAttribute('name', this.input.getAttribute('name') || '')
47
+ hiddenField.setAttribute('data-direct-upload-id', this.upload.id)
48
+ this.form.appendChild(hiddenField)
49
+ const event = this.dispatch('end')
50
+ endUI(event)
51
+ callback(error)
52
+ }
53
+ })
54
+ }
55
+
56
+ dispatch (name: string,
57
+ detail: { file?: File, id?: number, iconContainer?: Element, error?: Error } = {}) {
58
+ detail.file = this.file
59
+ detail.id = this.upload.id
60
+ detail.iconContainer = this.iconContainer
61
+ return dispatchEvent(this.input, `${eventFamily}:${name}`, { detail })
62
+ }
63
+
64
+ dispatchError (error: Error) {
65
+ const event = this.dispatch('error', { error })
66
+ errorUI(event)
67
+ }
68
+
69
+ directUploadWillCreateBlobWithXHR (xhr: XMLHttpRequest) {
70
+ this.dispatch('before-blob-request', { xhr })
71
+ }
72
+
73
+ directUploadWillStoreFileWithXHR (xhr: XMLHttpRequest) {
74
+ this.dispatch('before-storage-request', { xhr })
75
+ xhr.upload.addEventListener('progress', (event: Event) => this.uploadRequestDidProgress(event))
76
+ }
77
+
78
+ uploadRequestDidProgress (uploadEvent: Event) {
79
+ // $FlowFixMe
80
+ const progress = uploadEvent.loaded / uploadEvent.total * 100
81
+ if (!progress) return
82
+
83
+ const event = this.dispatch('progress', { progress })
84
+ progressUI(event)
85
+ }
86
+ }
87
+
88
+ class ValidationError extends Error {
89
+ constructor (...args) {
90
+ super(...args)
91
+ Error.captureStackTrace(this, ValidationError)
92
+ }
93
+ }
94
+
95
+ function validate (input, file) {
96
+ const sizeLimit = parseInt(input.getAttribute('size_limit'))
97
+ const accept = input.getAttribute('accept')
98
+ if (accept && !accept.split(', ').includes(file.type))
99
+ throw new ValidationError('Invalid filetype')
100
+ else if (sizeLimit && file.size > sizeLimit)
101
+ throw new ValidationError(`File too large. Can be no larger than ${fileSizeSI(sizeLimit)}`)
102
+ }
@@ -1,59 +1,15 @@
1
- export function dispatchEvent (element, type, eventInit = {}) {
2
- const { bubbles, cancelable, detail } = eventInit
3
- const event = document.createEvent('Event')
4
- event.initEvent(type, bubbles || true, cancelable || true)
5
- event.detail = detail || {}
1
+ // @flow
2
+
3
+ export function dispatchEvent (element: Element, type: string,
4
+ eventInit: { bubbles?: boolean, cancelable?: boolean, detail: {} } = {}) {
5
+ const { bubbles = true, cancelable = true, detail = {} } = eventInit
6
+ const event = new CustomEvent(type, { bubbles, cancelable, detail })
6
7
  element.dispatchEvent(event)
7
8
  return event
8
9
  }
9
10
 
10
- export function defaultErrorEventUI (event) {
11
- if (!event.defaultPrevented) {
12
- const { id, error } = event.detail
13
- const element = document.getElementById(`direct-upload-${id}`)
14
- element.classList.add('direct-upload--error')
15
- element.setAttribute('title', error)
16
- }
17
- }
18
-
19
- export function defaultEndEventUI (event) {
20
- if (!event.defaultPrevented) {
21
- const { id } = event.detail
22
- const element = document.getElementById(`direct-upload-${id}`)
23
- element.classList.remove('direct-upload--pending')
24
- element.classList.add('direct-upload--complete')
25
- }
26
- }
27
-
28
- export function hasClassnameInHeirarchy (element, classname) {
29
- if (element && element.classList) {
30
- if (element.classList.contains(classname)) {
31
- return true
32
- } else {
33
- return hasClassnameInHeirarchy(element.parentNode, classname)
34
- }
35
- }
36
- }
37
-
38
- export function getClassnameFromHeirarchy (element, classname) {
39
- if (element && element.classList) {
40
- if (element.classList.contains(classname)) {
41
- return element
42
- } else {
43
- return getClassnameFromHeirarchy(element.parentNode, classname)
44
- }
45
- }
46
- }
47
-
48
- export function fileUploadUIPainter (iconContainer, id, filename, complete) {
49
- // the only rule here is that all root level elements must have the data: { direct_upload_id: [id] } attribute ala: 'data-direct-upload-id="${id}"'
50
- var cname = (complete ? 'complete' : 'pending')
51
- var progress = (complete ? 100 : 0)
52
- iconContainer.insertAdjacentHTML('beforeend', `
53
- <div id="direct-upload-${id}" class="direct-upload direct-upload--${cname}" data-direct-upload-id="${id}">
54
- <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: ${progress}%"></div>
55
- <span class="direct-upload__filename">${filename}</span>
56
- </div>
57
- <a href='remove' class='direct-upload__remove' data-dnd-delete='true' data-direct-upload-id="${id}">x</a>
58
- `)
11
+ export function fileSizeSI (bytes: number) {
12
+ let e = Math.log(bytes) / Math.log(1000) | 0
13
+ const size = (bytes / Math.pow(1000, e) + 0.5) | 0
14
+ return size + (e ? 'kMGTPEZY'[--e] + 'B' : ' Bytes')
59
15
  }
@@ -1,6 +1,9 @@
1
- import { start } from './ujs'
1
+ // @flow
2
2
 
3
- export { start }
3
+ import { start, processUploadQueue } from './ujs'
4
+ import { paintUploadIcon } from './default_ui'
5
+
6
+ export { start, paintUploadIcon, processUploadQueue }
4
7
 
5
8
  function autostart () {
6
9
  start()