active_storage_drag_and_drop 0.4.0 → 1.0.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.
@@ -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()