1
0
mirror of synced 2024-11-22 21:36:04 +03:00

Fixes stray drag/drop on missed dropzones, allows removing of non uploaded files

This commit is contained in:
Justin Schroeder 2019-11-19 07:15:13 -05:00
parent 2d1c58f725
commit dd74fcb12e
18 changed files with 895 additions and 172 deletions

186
dist/formulate.esm.js vendored
View File

@ -115,11 +115,12 @@ var library = {
* The file upload class holds and represents a files upload state durring * The file upload class holds and represents a files upload state durring
* the upload flow. * the upload flow.
*/ */
var FileUpload = function FileUpload (fileList, context, options) { var FileUpload = function FileUpload (input, context, options) {
this.fileList = fileList; this.input = input;
this.fileList = input.files;
this.files = []; this.files = [];
this.options = options; this.options = options;
this.setFileList(fileList); this.addFileList(this.fileList);
this.context = context; this.context = context;
}; };
@ -127,16 +128,28 @@ var FileUpload = function FileUpload (fileList, context, options) {
* Produce an array of files and alert the callback. * Produce an array of files and alert the callback.
* @param {FileList} * @param {FileList}
*/ */
FileUpload.prototype.setFileList = function setFileList (fileList) { FileUpload.prototype.addFileList = function addFileList (fileList) {
for (var i = 0; i < fileList.length; i++) { var this$1 = this;
var file = fileList.item(i);
this.files.push({ var loop = function ( i ) {
progress: 0, var file = fileList[i];
var uuid = nanoid();
var removeFile = function () {
this.removeFile(uuid);
};
this$1.files.push({
progress: false,
error: false,
complete: false,
justFinished: false,
name: file.name || 'file-upload', name: file.name || 'file-upload',
file: file, file: file,
uuid: nanoid() uuid: uuid,
removeFile: removeFile.bind(this$1)
}); });
} };
for (var i = 0; i < fileList.length; i++) loop( i );
}; };
/** /**
@ -196,8 +209,21 @@ FileUpload.prototype.upload = function upload () {
Promise.all(this$1.files.map(function (file) { Promise.all(this$1.files.map(function (file) {
return this$1.getUploader( return this$1.getUploader(
file.file, file.file,
function (progress) { file.progress = progress; }, function (progress) {
function (error) { return reject(new Error(error)); }, file.progress = progress;
if (progress >= 100) {
if (!file.complete) {
file.justFinished = true;
setTimeout(function () { file.justFinished = false; }, this$1.options.uploadJustCompleteDuration);
}
file.complete = true;
}
},
function (error) {
file.progress = 0;
file.error = error;
file.complete = true;
},
this$1.options this$1.options
) )
})) }))
@ -206,6 +232,20 @@ FileUpload.prototype.upload = function upload () {
}) })
}; };
/**
* Remove a file from the uploader (and the file list)
* @param {string} uuid
*/
FileUpload.prototype.removeFile = function removeFile (uuid) {
this.files = this.files.filter(function (file) { return file.uuid !== uuid; });
if (window) {
var transfer = new DataTransfer();
this.files.map(function (file) { return transfer.items.add(file.file); });
this.fileList = transfer.files;
this.input.files = this.fileList;
}
};
/** /**
* Get the files. * Get the files.
*/ */
@ -538,14 +578,10 @@ var rules = {
return Promise.resolve((function () { return Promise.resolve((function () {
if (files instanceof FileUpload) { if (files instanceof FileUpload) {
if (files.hasUploader()) { var fileList = files.getFileList();
return false for (var i = 0; i < fileList.length; i++) {
} var file = fileList[i];
files = files.getFiles(); if (!types.includes(file.type)) {
}
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (var i in files) {
if (!types.includes(files[i].type)) {
return false return false
} }
} }
@ -776,6 +812,16 @@ var en = {
return ((sentence(name)) + " must be less than " + (args[0]) + " characters long.") return ((sentence(name)) + " must be less than " + (args[0]) + " characters long.")
}, },
/**
* The (field-level) error message for mime errors.
*/
mime: function (ref) {
var name = ref.name;
var args = ref.args;
return ((sentence(name)) + " must of the the type: " + (args[0] || 'No file formats allowed.'))
},
/** /**
* The maximum value allowed. * The maximum value allowed.
*/ */
@ -842,17 +888,29 @@ var en = {
*/ */
function fauxUploader (file, progress, error, options) { function fauxUploader (file, progress, error, options) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var totalTime = options.fauxUploaderDuration || 2000; var totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random());
var start = performance.now(); var start = performance.now();
/**
* @todo - remove, intentional failure
*/
var fail = (Math.random() > 0.5);
var advance = function () { return setTimeout(function () { var advance = function () { return setTimeout(function () {
var elapsed = performance.now() - start; var elapsed = performance.now() - start;
var currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100)); var currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100));
progress(currentProgress); progress(currentProgress);
/**
* @todo - remove, intentional failure
*/
if (fail && currentProgress > 50) {
return error('There was an error uploading the file.')
}
if (currentProgress >= 100) { if (currentProgress >= 100) {
resolve({ return resolve({
url: 'http://via.placeholder.com/350x150.png', url: 'http://via.placeholder.com/350x150.png',
name: file.name name: file.name
}); })
} else { } else {
advance(); advance();
} }
@ -881,7 +939,8 @@ var context = {
showImage: this.showImage, showImage: this.showImage,
uploadUrl: this.uploadUrl, uploadUrl: this.uploadUrl,
uploader: this.uploader || this.$formulate.getUploader(), uploader: this.uploader || this.$formulate.getUploader(),
immediateUpload: this.immediateUpload}, uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops},
this.typeContext)) this.typeContext))
}, },
nameOrFallback: nameOrFallback, nameOrFallback: nameOrFallback,
@ -1182,7 +1241,11 @@ var script = {
type: [Function, Object, Boolean], type: [Function, Object, Boolean],
default: false default: false
}, },
immediateUpload: { uploadBehavior: {
type: Boolean,
default: true
},
preventWindowDrops: {
type: Boolean, type: Boolean,
default: true default: true
} }
@ -2200,18 +2263,52 @@ var __vue_render__$6 = function() {
"ul", "ul",
{ staticClass: "formulate-files" }, { staticClass: "formulate-files" },
_vm._l(_vm.fileUploads, function(file) { _vm._l(_vm.fileUploads, function(file) {
return _c("li", { key: file.uuid, staticClass: "formulate-file" }, [ return _c(
_c("span", { "li",
{ key: file.uuid, attrs: { "data-has-error": !!file.error } },
[
_c("div", { staticClass: "formulate-file" }, [
_c("div", {
staticClass: "formualte-file-name", staticClass: "formualte-file-name",
domProps: { textContent: _vm._s(file.name) } domProps: { textContent: _vm._s(file.name) }
}), }),
_vm._v(" "), _vm._v(" "),
file.progress > 0 && file.progress < 100 file.progress !== false
? _c("span", { ? _c(
domProps: { textContent: _vm._s(file.progress + "%") } "div",
{
staticClass: "formulate-file-progress",
attrs: {
"data-just-finished": file.justFinished,
"data-is-finished":
!file.justFinished && file.complete
}
},
[
_c("div", {
staticClass: "formulate-file-progress-inner",
style: { width: file.progress + "%" }
})
]
)
: _vm._e(),
_vm._v(" "),
(file.complete && !file.justFinished) || file.progress === false
? _c("div", {
staticClass: "formulate-file-remove",
on: { click: file.removeFile }
}) })
: _vm._e() : _vm._e()
]) ]),
_vm._v(" "),
file.error
? _c("div", {
staticClass: "formulate-file-upload-error",
domProps: { textContent: _vm._s(file.error) }
})
: _vm._e()
]
)
}), }),
0 0
) )
@ -2264,16 +2361,36 @@ var script$7 = {
}, },
computed: { computed: {
hasFiles: function hasFiles () { hasFiles: function hasFiles () {
return (this.context.model instanceof FileUpload && this.context.model.files.length) return !!(this.context.model instanceof FileUpload && this.context.model.files.length)
}
},
mounted: function mounted () {
// Add a listener to the window to prevent drag/drops that miss the dropzone
// from opening the file and navigating the user away from the page.
if (window && this.context.preventWindowDrops) {
window.addEventListener('dragover', this.preventDefault);
window.addEventListener('drop', this.preventDefault);
}
},
destroyed: function destroyed () {
if (window && this.context.preventWindowDrops) {
window.removeEventListener('dragover', this.preventDefault);
window.removeEventListener('drop', this.preventDefault);
} }
}, },
methods: { methods: {
preventDefault: function preventDefault (e) {
if (e.target.tagName !== 'INPUT' && e.target.getAttribute('type') !== 'file') {
e = e || event;
e.preventDefault();
}
},
handleFile: function handleFile () { handleFile: function handleFile () {
var input = this.$refs.file; var input = this.$refs.file;
if (input.files.length) { if (input.files.length) {
this.context.model = this.$formulate.createUpload(input.files, this.context); this.context.model = this.$formulate.createUpload(input, this.context);
} }
if (this.context.immediateUpload && this.context.model instanceof FileUpload) { if (this.context.uploadBehavior === 'live' && this.context.model instanceof FileUpload) {
this.context.model.upload(); this.context.model.upload();
} }
}, },
@ -2819,6 +2936,7 @@ var Formulate = function Formulate () {
rules: rules, rules: rules,
locale: 'en', locale: 'en',
uploader: fauxUploader, uploader: fauxUploader,
uploadJustCompleteDuration: 1000,
locales: { locales: {
en: en en: en
} }

186
dist/formulate.min.js vendored
View File

@ -118,11 +118,12 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
* The file upload class holds and represents a files upload state durring * The file upload class holds and represents a files upload state durring
* the upload flow. * the upload flow.
*/ */
var FileUpload = function FileUpload (fileList, context, options) { var FileUpload = function FileUpload (input, context, options) {
this.fileList = fileList; this.input = input;
this.fileList = input.files;
this.files = []; this.files = [];
this.options = options; this.options = options;
this.setFileList(fileList); this.addFileList(this.fileList);
this.context = context; this.context = context;
}; };
@ -130,16 +131,28 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
* Produce an array of files and alert the callback. * Produce an array of files and alert the callback.
* @param {FileList} * @param {FileList}
*/ */
FileUpload.prototype.setFileList = function setFileList (fileList) { FileUpload.prototype.addFileList = function addFileList (fileList) {
for (var i = 0; i < fileList.length; i++) { var this$1 = this;
var file = fileList.item(i);
this.files.push({ var loop = function ( i ) {
progress: 0, var file = fileList[i];
var uuid = nanoid();
var removeFile = function () {
this.removeFile(uuid);
};
this$1.files.push({
progress: false,
error: false,
complete: false,
justFinished: false,
name: file.name || 'file-upload', name: file.name || 'file-upload',
file: file, file: file,
uuid: nanoid() uuid: uuid,
removeFile: removeFile.bind(this$1)
}); });
} };
for (var i = 0; i < fileList.length; i++) loop( i );
}; };
/** /**
@ -199,8 +212,21 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
Promise.all(this$1.files.map(function (file) { Promise.all(this$1.files.map(function (file) {
return this$1.getUploader( return this$1.getUploader(
file.file, file.file,
function (progress) { file.progress = progress; }, function (progress) {
function (error) { return reject(new Error(error)); }, file.progress = progress;
if (progress >= 100) {
if (!file.complete) {
file.justFinished = true;
setTimeout(function () { file.justFinished = false; }, this$1.options.uploadJustCompleteDuration);
}
file.complete = true;
}
},
function (error) {
file.progress = 0;
file.error = error;
file.complete = true;
},
this$1.options this$1.options
) )
})) }))
@ -209,6 +235,20 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
}) })
}; };
/**
* Remove a file from the uploader (and the file list)
* @param {string} uuid
*/
FileUpload.prototype.removeFile = function removeFile (uuid) {
this.files = this.files.filter(function (file) { return file.uuid !== uuid; });
if (window) {
var transfer = new DataTransfer();
this.files.map(function (file) { return transfer.items.add(file.file); });
this.fileList = transfer.files;
this.input.files = this.fileList;
}
};
/** /**
* Get the files. * Get the files.
*/ */
@ -541,14 +581,10 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
return Promise.resolve((function () { return Promise.resolve((function () {
if (files instanceof FileUpload) { if (files instanceof FileUpload) {
if (files.hasUploader()) { var fileList = files.getFileList();
return false for (var i = 0; i < fileList.length; i++) {
} var file = fileList[i];
files = files.getFiles(); if (!types.includes(file.type)) {
}
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (var i in files) {
if (!types.includes(files[i].type)) {
return false return false
} }
} }
@ -779,6 +815,16 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
return ((sentence(name)) + " must be less than " + (args[0]) + " characters long.") return ((sentence(name)) + " must be less than " + (args[0]) + " characters long.")
}, },
/**
* The (field-level) error message for mime errors.
*/
mime: function (ref) {
var name = ref.name;
var args = ref.args;
return ((sentence(name)) + " must of the the type: " + (args[0] || 'No file formats allowed.'))
},
/** /**
* The maximum value allowed. * The maximum value allowed.
*/ */
@ -845,17 +891,29 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
*/ */
function fauxUploader (file, progress, error, options) { function fauxUploader (file, progress, error, options) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var totalTime = options.fauxUploaderDuration || 2000; var totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random());
var start = performance.now(); var start = performance.now();
/**
* @todo - remove, intentional failure
*/
var fail = (Math.random() > 0.5);
var advance = function () { return setTimeout(function () { var advance = function () { return setTimeout(function () {
var elapsed = performance.now() - start; var elapsed = performance.now() - start;
var currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100)); var currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100));
progress(currentProgress); progress(currentProgress);
/**
* @todo - remove, intentional failure
*/
if (fail && currentProgress > 50) {
return error('There was an error uploading the file.')
}
if (currentProgress >= 100) { if (currentProgress >= 100) {
resolve({ return resolve({
url: 'http://via.placeholder.com/350x150.png', url: 'http://via.placeholder.com/350x150.png',
name: file.name name: file.name
}); })
} else { } else {
advance(); advance();
} }
@ -884,7 +942,8 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
showImage: this.showImage, showImage: this.showImage,
uploadUrl: this.uploadUrl, uploadUrl: this.uploadUrl,
uploader: this.uploader || this.$formulate.getUploader(), uploader: this.uploader || this.$formulate.getUploader(),
immediateUpload: this.immediateUpload}, uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops},
this.typeContext)) this.typeContext))
}, },
nameOrFallback: nameOrFallback, nameOrFallback: nameOrFallback,
@ -1185,7 +1244,11 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
type: [Function, Object, Boolean], type: [Function, Object, Boolean],
default: false default: false
}, },
immediateUpload: { uploadBehavior: {
type: Boolean,
default: true
},
preventWindowDrops: {
type: Boolean, type: Boolean,
default: true default: true
} }
@ -2203,18 +2266,52 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
"ul", "ul",
{ staticClass: "formulate-files" }, { staticClass: "formulate-files" },
_vm._l(_vm.fileUploads, function(file) { _vm._l(_vm.fileUploads, function(file) {
return _c("li", { key: file.uuid, staticClass: "formulate-file" }, [ return _c(
_c("span", { "li",
{ key: file.uuid, attrs: { "data-has-error": !!file.error } },
[
_c("div", { staticClass: "formulate-file" }, [
_c("div", {
staticClass: "formualte-file-name", staticClass: "formualte-file-name",
domProps: { textContent: _vm._s(file.name) } domProps: { textContent: _vm._s(file.name) }
}), }),
_vm._v(" "), _vm._v(" "),
file.progress > 0 && file.progress < 100 file.progress !== false
? _c("span", { ? _c(
domProps: { textContent: _vm._s(file.progress + "%") } "div",
{
staticClass: "formulate-file-progress",
attrs: {
"data-just-finished": file.justFinished,
"data-is-finished":
!file.justFinished && file.complete
}
},
[
_c("div", {
staticClass: "formulate-file-progress-inner",
style: { width: file.progress + "%" }
})
]
)
: _vm._e(),
_vm._v(" "),
(file.complete && !file.justFinished) || file.progress === false
? _c("div", {
staticClass: "formulate-file-remove",
on: { click: file.removeFile }
}) })
: _vm._e() : _vm._e()
]) ]),
_vm._v(" "),
file.error
? _c("div", {
staticClass: "formulate-file-upload-error",
domProps: { textContent: _vm._s(file.error) }
})
: _vm._e()
]
)
}), }),
0 0
) )
@ -2267,16 +2364,36 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
}, },
computed: { computed: {
hasFiles: function hasFiles () { hasFiles: function hasFiles () {
return (this.context.model instanceof FileUpload && this.context.model.files.length) return !!(this.context.model instanceof FileUpload && this.context.model.files.length)
}
},
mounted: function mounted () {
// Add a listener to the window to prevent drag/drops that miss the dropzone
// from opening the file and navigating the user away from the page.
if (window && this.context.preventWindowDrops) {
window.addEventListener('dragover', this.preventDefault);
window.addEventListener('drop', this.preventDefault);
}
},
destroyed: function destroyed () {
if (window && this.context.preventWindowDrops) {
window.removeEventListener('dragover', this.preventDefault);
window.removeEventListener('drop', this.preventDefault);
} }
}, },
methods: { methods: {
preventDefault: function preventDefault (e) {
if (e.target.tagName !== 'INPUT' && e.target.getAttribute('type') !== 'file') {
e = e || event;
e.preventDefault();
}
},
handleFile: function handleFile () { handleFile: function handleFile () {
var input = this.$refs.file; var input = this.$refs.file;
if (input.files.length) { if (input.files.length) {
this.context.model = this.$formulate.createUpload(input.files, this.context); this.context.model = this.$formulate.createUpload(input, this.context);
} }
if (this.context.immediateUpload && this.context.model instanceof FileUpload) { if (this.context.uploadBehavior === 'live' && this.context.model instanceof FileUpload) {
this.context.model.upload(); this.context.model.upload();
} }
}, },
@ -2822,6 +2939,7 @@ var Formulate = (function (exports, isUrl, nanoid, isPlainObject) {
rules: rules, rules: rules,
locale: 'en', locale: 'en',
uploader: fauxUploader, uploader: fauxUploader,
uploadJustCompleteDuration: 1000,
locales: { locales: {
en: en en: en
} }

186
dist/formulate.umd.js vendored
View File

@ -121,11 +121,12 @@
* The file upload class holds and represents a files upload state durring * The file upload class holds and represents a files upload state durring
* the upload flow. * the upload flow.
*/ */
var FileUpload = function FileUpload (fileList, context, options) { var FileUpload = function FileUpload (input, context, options) {
this.fileList = fileList; this.input = input;
this.fileList = input.files;
this.files = []; this.files = [];
this.options = options; this.options = options;
this.setFileList(fileList); this.addFileList(this.fileList);
this.context = context; this.context = context;
}; };
@ -133,16 +134,28 @@
* Produce an array of files and alert the callback. * Produce an array of files and alert the callback.
* @param {FileList} * @param {FileList}
*/ */
FileUpload.prototype.setFileList = function setFileList (fileList) { FileUpload.prototype.addFileList = function addFileList (fileList) {
for (var i = 0; i < fileList.length; i++) { var this$1 = this;
var file = fileList.item(i);
this.files.push({ var loop = function ( i ) {
progress: 0, var file = fileList[i];
var uuid = nanoid();
var removeFile = function () {
this.removeFile(uuid);
};
this$1.files.push({
progress: false,
error: false,
complete: false,
justFinished: false,
name: file.name || 'file-upload', name: file.name || 'file-upload',
file: file, file: file,
uuid: nanoid() uuid: uuid,
removeFile: removeFile.bind(this$1)
}); });
} };
for (var i = 0; i < fileList.length; i++) loop( i );
}; };
/** /**
@ -202,8 +215,21 @@
Promise.all(this$1.files.map(function (file) { Promise.all(this$1.files.map(function (file) {
return this$1.getUploader( return this$1.getUploader(
file.file, file.file,
function (progress) { file.progress = progress; }, function (progress) {
function (error) { return reject(new Error(error)); }, file.progress = progress;
if (progress >= 100) {
if (!file.complete) {
file.justFinished = true;
setTimeout(function () { file.justFinished = false; }, this$1.options.uploadJustCompleteDuration);
}
file.complete = true;
}
},
function (error) {
file.progress = 0;
file.error = error;
file.complete = true;
},
this$1.options this$1.options
) )
})) }))
@ -212,6 +238,20 @@
}) })
}; };
/**
* Remove a file from the uploader (and the file list)
* @param {string} uuid
*/
FileUpload.prototype.removeFile = function removeFile (uuid) {
this.files = this.files.filter(function (file) { return file.uuid !== uuid; });
if (window) {
var transfer = new DataTransfer();
this.files.map(function (file) { return transfer.items.add(file.file); });
this.fileList = transfer.files;
this.input.files = this.fileList;
}
};
/** /**
* Get the files. * Get the files.
*/ */
@ -544,14 +584,10 @@
return Promise.resolve((function () { return Promise.resolve((function () {
if (files instanceof FileUpload) { if (files instanceof FileUpload) {
if (files.hasUploader()) { var fileList = files.getFileList();
return false for (var i = 0; i < fileList.length; i++) {
} var file = fileList[i];
files = files.getFiles(); if (!types.includes(file.type)) {
}
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (var i in files) {
if (!types.includes(files[i].type)) {
return false return false
} }
} }
@ -782,6 +818,16 @@
return ((sentence(name)) + " must be less than " + (args[0]) + " characters long.") return ((sentence(name)) + " must be less than " + (args[0]) + " characters long.")
}, },
/**
* The (field-level) error message for mime errors.
*/
mime: function (ref) {
var name = ref.name;
var args = ref.args;
return ((sentence(name)) + " must of the the type: " + (args[0] || 'No file formats allowed.'))
},
/** /**
* The maximum value allowed. * The maximum value allowed.
*/ */
@ -848,17 +894,29 @@
*/ */
function fauxUploader (file, progress, error, options) { function fauxUploader (file, progress, error, options) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var totalTime = options.fauxUploaderDuration || 2000; var totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random());
var start = performance.now(); var start = performance.now();
/**
* @todo - remove, intentional failure
*/
var fail = (Math.random() > 0.5);
var advance = function () { return setTimeout(function () { var advance = function () { return setTimeout(function () {
var elapsed = performance.now() - start; var elapsed = performance.now() - start;
var currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100)); var currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100));
progress(currentProgress); progress(currentProgress);
/**
* @todo - remove, intentional failure
*/
if (fail && currentProgress > 50) {
return error('There was an error uploading the file.')
}
if (currentProgress >= 100) { if (currentProgress >= 100) {
resolve({ return resolve({
url: 'http://via.placeholder.com/350x150.png', url: 'http://via.placeholder.com/350x150.png',
name: file.name name: file.name
}); })
} else { } else {
advance(); advance();
} }
@ -887,7 +945,8 @@
showImage: this.showImage, showImage: this.showImage,
uploadUrl: this.uploadUrl, uploadUrl: this.uploadUrl,
uploader: this.uploader || this.$formulate.getUploader(), uploader: this.uploader || this.$formulate.getUploader(),
immediateUpload: this.immediateUpload}, uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops},
this.typeContext)) this.typeContext))
}, },
nameOrFallback: nameOrFallback, nameOrFallback: nameOrFallback,
@ -1188,7 +1247,11 @@
type: [Function, Object, Boolean], type: [Function, Object, Boolean],
default: false default: false
}, },
immediateUpload: { uploadBehavior: {
type: Boolean,
default: true
},
preventWindowDrops: {
type: Boolean, type: Boolean,
default: true default: true
} }
@ -2206,18 +2269,52 @@
"ul", "ul",
{ staticClass: "formulate-files" }, { staticClass: "formulate-files" },
_vm._l(_vm.fileUploads, function(file) { _vm._l(_vm.fileUploads, function(file) {
return _c("li", { key: file.uuid, staticClass: "formulate-file" }, [ return _c(
_c("span", { "li",
{ key: file.uuid, attrs: { "data-has-error": !!file.error } },
[
_c("div", { staticClass: "formulate-file" }, [
_c("div", {
staticClass: "formualte-file-name", staticClass: "formualte-file-name",
domProps: { textContent: _vm._s(file.name) } domProps: { textContent: _vm._s(file.name) }
}), }),
_vm._v(" "), _vm._v(" "),
file.progress > 0 && file.progress < 100 file.progress !== false
? _c("span", { ? _c(
domProps: { textContent: _vm._s(file.progress + "%") } "div",
{
staticClass: "formulate-file-progress",
attrs: {
"data-just-finished": file.justFinished,
"data-is-finished":
!file.justFinished && file.complete
}
},
[
_c("div", {
staticClass: "formulate-file-progress-inner",
style: { width: file.progress + "%" }
})
]
)
: _vm._e(),
_vm._v(" "),
(file.complete && !file.justFinished) || file.progress === false
? _c("div", {
staticClass: "formulate-file-remove",
on: { click: file.removeFile }
}) })
: _vm._e() : _vm._e()
]) ]),
_vm._v(" "),
file.error
? _c("div", {
staticClass: "formulate-file-upload-error",
domProps: { textContent: _vm._s(file.error) }
})
: _vm._e()
]
)
}), }),
0 0
) )
@ -2270,16 +2367,36 @@
}, },
computed: { computed: {
hasFiles: function hasFiles () { hasFiles: function hasFiles () {
return (this.context.model instanceof FileUpload && this.context.model.files.length) return !!(this.context.model instanceof FileUpload && this.context.model.files.length)
}
},
mounted: function mounted () {
// Add a listener to the window to prevent drag/drops that miss the dropzone
// from opening the file and navigating the user away from the page.
if (window && this.context.preventWindowDrops) {
window.addEventListener('dragover', this.preventDefault);
window.addEventListener('drop', this.preventDefault);
}
},
destroyed: function destroyed () {
if (window && this.context.preventWindowDrops) {
window.removeEventListener('dragover', this.preventDefault);
window.removeEventListener('drop', this.preventDefault);
} }
}, },
methods: { methods: {
preventDefault: function preventDefault (e) {
if (e.target.tagName !== 'INPUT' && e.target.getAttribute('type') !== 'file') {
e = e || event;
e.preventDefault();
}
},
handleFile: function handleFile () { handleFile: function handleFile () {
var input = this.$refs.file; var input = this.$refs.file;
if (input.files.length) { if (input.files.length) {
this.context.model = this.$formulate.createUpload(input.files, this.context); this.context.model = this.$formulate.createUpload(input, this.context);
} }
if (this.context.immediateUpload && this.context.model instanceof FileUpload) { if (this.context.uploadBehavior === 'live' && this.context.model instanceof FileUpload) {
this.context.model.upload(); this.context.model.upload();
} }
}, },
@ -2825,6 +2942,7 @@
rules: rules, rules: rules,
locale: 'en', locale: 'en',
uploader: fauxUploader, uploader: fauxUploader,
uploadJustCompleteDuration: 1000,
locales: { locales: {
en: en en: en
} }

117
dist/snow.css vendored
View File

@ -19,7 +19,8 @@
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
margin: 0; } margin: 0; }
.formulate-input .formulate-input-error { .formulate-input .formulate-input-error,
.formulate-input .formulate-file-upload-error {
color: #960505; color: #960505;
font-size: .8em; font-size: .8em;
font-weight: 300; font-weight: 300;
@ -220,9 +221,7 @@
.formulate-input[data-classification="file"] .formulate-input-upload-area { .formulate-input[data-classification="file"] .formulate-input-upload-area {
width: 100%; width: 100%;
position: relative; position: relative;
padding: 2em; } padding: 2em 0; }
.formulate-input[data-classification="file"] .formulate-input-upload-area[data-has-files] {
padding: 0; }
.formulate-input[data-classification="file"] .formulate-input-upload-area input { .formulate-input[data-classification="file"] .formulate-input-upload-area input {
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
@ -235,6 +234,10 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 5; } z-index: 5; }
.formulate-input[data-classification="file"] .formulate-input-upload-area[data-has-files] {
padding: 0; }
.formulate-input[data-classification="file"] .formulate-input-upload-area[data-has-files] input {
display: none; }
.formulate-input[data-classification="file"] .formulate-input-upload-area-mask { .formulate-input[data-classification="file"] .formulate-input-upload-area-mask {
border-radius: .4em; border-radius: .4em;
position: absolute; position: absolute;
@ -253,6 +256,8 @@
content: ''; content: '';
background-color: #a8a8a8; background-color: #a8a8a8;
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><path d="M29,58A29,29,0,1,0,0,29,29,29,0,0,0,29,58ZM29,4A25,25,0,1,1,4,29,25,25,0,0,1,29,4Z"/><polygon points="27 22 27 44.4 31 44.4 31 22 41.7 31.1 44.3 28.1 29 15 13.7 28.1 16.3 31.1 27 22"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><path d="M29,58A29,29,0,1,0,0,29,29,29,0,0,0,29,58ZM29,4A25,25,0,1,1,4,29,25,25,0,0,1,29,4Z"/><polygon points="27 22 27 44.4 31 44.4 31 22 41.7 31.1 44.3 28.1 29 15 13.7 28.1 16.3 31.1 27 22"/></svg>');
mask-repeat: no-repeat;
mask-position: center;
width: 2em; width: 2em;
height: 2em; height: 2em;
position: absolute; position: absolute;
@ -269,7 +274,98 @@
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
padding: 0; } padding: 0; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress {
background-color: #cecece;
height: .3em;
border-radius: 1.25em;
width: 5em;
overflow: hidden;
position: absolute;
right: .75em;
transition: height .25s, width .25s;
z-index: 2; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
opacity: 0;
transform: scale(0.08);
background-color: #ffffff;
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M8.76,56.2c-6.38-6.34,3.26-16,9.64-9.69L38,65.88,80.56,23.29c6.38-6.38,16.07,3.32,9.69,9.69L42.84,80.37a6.83,6.83,0,0,1-9.65,0Z"/></svg>');
mask-size: 77%;
mask-repeat: no-repeat;
mask-position: center;
z-index: 3; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress[data-just-finished] {
width: 1.25em;
height: 1.25em; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress[data-just-finished]::before {
transition: transform .25s .2s, opacity .25s .2s;
transform: scale(1);
opacity: 1; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress[data-is-finished] {
transition: height .25s, width .25s, left .25s, top, .25s, border-radius .25s;
width: .3em;
height: 100%;
right: 0;
border-radius: 0; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress[data-is-finished]::before {
transition: opacity .1s;
opacity: 0; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-progress .formulate-file-progress-inner {
background-color: #41b883;
width: 1%;
position: absolute;
left: 0;
bottom: 0;
top: 0;
z-index: 2; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name {
padding-left: 1.5em;
padding-right: 2em; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name::before {
position: absolute;
left: .7em;
top: 50%;
margin-top: -.7em;
background-color: #a8a8a8;
content: '';
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64.06 83.59"><path d="M55.94,83.59a8.16,8.16,0,0,0,8.12-8.16V19.12a1.77,1.77,0,0,0-.52-1.25L46.21.59A1.69,1.69,0,0,0,45.14.08L44.69,0l-.18,0H8.13A8.18,8.18,0,0,0,0,8.16V75.41a8.16,8.16,0,0,0,8.13,8.16H55.94ZM46.68,6,58.11,17.38H46.68ZM3.52,75.43V8.16A4.64,4.64,0,0,1,8.13,3.52h35V19.16a1.75,1.75,0,0,0,1.76,1.74H60.55V75.43a4.65,4.65,0,0,1-4.61,4.65H8.13A4.65,4.65,0,0,1,3.52,75.43Z"/></svg>');
mask-repeat: no-repeat;
mask-size: contain;
width: 1.25em;
height: 1.25em;
display: inline-block;
margin-right: .5em; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file-remove {
width: 1.25em;
height: 1.25em;
border-radius: 1em;
border: 1px solid #a8a8a8;
background-color: #a8a8a8;
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 59.1 59.2"><path d="M1.6,57.7a5,5,0,0,0,3.5,1.5,4.85,4.85,0,0,0,3.5-1.5l21-21,21,21a5,5,0,0,0,3.5,1.5,4.85,4.85,0,0,0,3.5-1.5,5,5,0,0,0,0-7.1l-21-21,21-21a5,5,0,0,0,0-7.1,5,5,0,0,0-7.1,0l-21,21L8.6,1.7a5,5,0,0,0-7.1,0,5,5,0,0,0,0,7.1l21,21L1.6,50.7A4.83,4.83,0,0,0,1.6,57.7Z"/></svg>');
mask-size: .6em;
mask-repeat: no-repeat;
mask-position: center;
cursor: pointer;
position: absolute;
right: .75em;
z-index: 1;
transition: transform .25s; }
@media (pointer: fine) {
.formulate-input[data-classification="file"] .formulate-files .formulate-file-remove:hover {
transform: scale(1.5); } }
.formulate-input[data-classification="file"] .formulate-files li { .formulate-input[data-classification="file"] .formulate-files li {
display: block; }
.formulate-input[data-classification="file"] .formulate-files li[data-has-error] .formulate-file-progress {
background-color: #dc2c2c; }
.formulate-input[data-classification="file"] .formulate-files li + li {
margin-top: .5em; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file {
appearance: none; appearance: none;
border-radius: .3em; border-radius: .3em;
border: 1px solid #cecece; border: 1px solid #cecece;
@ -286,14 +382,19 @@
display: block; display: block;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; } justify-content: space-between;
.formulate-input[data-classification="file"] .formulate-files li::placeholder { align-items: center;
position: relative;
overflow: hidden; }
.formulate-input[data-classification="file"] .formulate-files .formulate-file::placeholder {
color: #a8a8a8; } color: #a8a8a8; }
.formulate-input[data-classification="file"] .formulate-files li:focus { .formulate-input[data-classification="file"] .formulate-files .formulate-file:focus {
outline: 0; outline: 0;
border: 1px solid #41b883; } border: 1px solid #41b883; }
.formulate-input[data-classification="file"] .formulate-files li ::-webkit-progress-bar { .formulate-input[data-classification="file"] .formulate-files .formulate-file ::-webkit-progress-bar {
appearance: none; appearance: none;
height: .5em; height: .5em;
border-radius: .5em; border-radius: .5em;
overflow: hidden; } overflow: hidden; }
.formulate-input[data-classification="file"] [data-type="image"] .formulate-input-upload-area .formulate-input-upload-area-mask::before {
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 71.05"><path d="M82.89,0H7.1A7.12,7.12,0,0,0,0,7.11V64a7.11,7.11,0,0,0,7.1,7.1H82.9A7.11,7.11,0,0,0,90,64V7.11A7.12,7.12,0,0,0,82.89,0ZM69.28,39.35a5.44,5.44,0,0,0-8,0L50.58,50.74,32.38,30.88a5.31,5.31,0,0,0-7.92,0L4.74,52.4V7.11A2.37,2.37,0,0,1,7.11,4.74H82.9a2.37,2.37,0,0,1,2.36,2.37V56.3Z"/><circle cx="67.74" cy="22.26" r="8.53"/></svg>'); }

4
dist/snow.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -10,11 +10,12 @@ class FileUpload {
* @param {FileList} fileList * @param {FileList} fileList
* @param {object} context * @param {object} context
*/ */
constructor (fileList, context, options) { constructor (input, context, options) {
this.fileList = fileList this.input = input
this.fileList = input.files
this.files = [] this.files = []
this.options = options this.options = options
this.setFileList(fileList) this.addFileList(this.fileList)
this.context = context this.context = context
} }
@ -22,14 +23,22 @@ class FileUpload {
* Produce an array of files and alert the callback. * Produce an array of files and alert the callback.
* @param {FileList} * @param {FileList}
*/ */
setFileList (fileList) { addFileList (fileList) {
for (let i = 0; i < fileList.length; i++) { for (let i = 0; i < fileList.length; i++) {
const file = fileList.item(i) const file = fileList[i]
const uuid = nanoid()
const removeFile = function () {
this.removeFile(uuid)
}
this.files.push({ this.files.push({
progress: 0, progress: false,
error: false,
complete: false,
justFinished: false,
name: file.name || 'file-upload', name: file.name || 'file-upload',
file: file, file,
uuid: nanoid() uuid,
removeFile: removeFile.bind(this)
}) })
} }
} }
@ -85,8 +94,21 @@ class FileUpload {
Promise.all(this.files.map(file => { Promise.all(this.files.map(file => {
return this.getUploader( return this.getUploader(
file.file, file.file,
(progress) => { file.progress = progress }, (progress) => {
(error) => reject(new Error(error)), file.progress = progress
if (progress >= 100) {
if (!file.complete) {
file.justFinished = true
setTimeout(() => { file.justFinished = false }, this.options.uploadJustCompleteDuration)
}
file.complete = true
}
},
(error) => {
file.progress = 0
file.error = error
file.complete = true
},
this.options this.options
) )
})) }))
@ -95,6 +117,20 @@ class FileUpload {
}) })
} }
/**
* Remove a file from the uploader (and the file list)
* @param {string} uuid
*/
removeFile (uuid) {
this.files = this.files.filter(file => file.uuid !== uuid)
if (window) {
const transfer = new DataTransfer()
this.files.map(file => transfer.items.add(file.file))
this.fileList = transfer.files
this.input.files = this.fileList
}
}
/** /**
* Get the files. * Get the files.
*/ */

View File

@ -40,6 +40,7 @@ class Formulate {
rules, rules,
locale: 'en', locale: 'en',
uploader: fauxUploader, uploader: fauxUploader,
uploadJustCompleteDuration: 1000,
locales: { locales: {
en en
} }

View File

@ -6,15 +6,34 @@
<li <li
v-for="file in fileUploads" v-for="file in fileUploads"
:key="file.uuid" :key="file.uuid"
class="formulate-file" :data-has-error="!!file.error"
> >
<span <div class="formulate-file">
<div
class="formualte-file-name" class="formualte-file-name"
v-text="file.name" v-text="file.name"
/> />
<span <div
v-if="file.progress > 0 && file.progress < 100" v-if="file.progress !== false"
v-text="`${file.progress}%`" :data-just-finished="file.justFinished"
:data-is-finished="!file.justFinished && file.complete"
class="formulate-file-progress"
>
<div
class="formulate-file-progress-inner"
:style="{width: file.progress + '%'}"
/>
</div>
<div
v-if="(file.complete && !file.justFinished) || file.progress === false"
class="formulate-file-remove"
@click="file.removeFile"
/>
</div>
<div
v-if="file.error"
class="formulate-file-upload-error"
v-text="file.error"
/> />
</li> </li>
</ul> </ul>

View File

@ -149,7 +149,11 @@ export default {
type: [Function, Object, Boolean], type: [Function, Object, Boolean],
default: false default: false
}, },
immediateUpload: { uploadBehavior: {
type: Boolean,
default: true
},
preventWindowDrops: {
type: Boolean, type: Boolean,
default: true default: true
} }

View File

@ -48,16 +48,36 @@ export default {
}, },
computed: { computed: {
hasFiles () { hasFiles () {
return (this.context.model instanceof FileUpload && this.context.model.files.length) return !!(this.context.model instanceof FileUpload && this.context.model.files.length)
}
},
mounted () {
// Add a listener to the window to prevent drag/drops that miss the dropzone
// from opening the file and navigating the user away from the page.
if (window && this.context.preventWindowDrops) {
window.addEventListener('dragover', this.preventDefault)
window.addEventListener('drop', this.preventDefault)
}
},
destroyed () {
if (window && this.context.preventWindowDrops) {
window.removeEventListener('dragover', this.preventDefault)
window.removeEventListener('drop', this.preventDefault)
} }
}, },
methods: { methods: {
preventDefault (e) {
if (e.target.tagName !== 'INPUT' && e.target.getAttribute('type') !== 'file') {
e = e || event
e.preventDefault()
}
},
handleFile () { handleFile () {
const input = this.$refs.file const input = this.$refs.file
if (input.files.length) { if (input.files.length) {
this.context.model = this.$formulate.createUpload(input.files, this.context) this.context.model = this.$formulate.createUpload(input, this.context)
} }
if (this.context.immediateUpload && this.context.model instanceof FileUpload) { if (this.context.uploadBehavior === 'live' && this.context.model instanceof FileUpload) {
this.context.model.upload() this.context.model.upload()
} }
}, },

View File

@ -22,7 +22,8 @@ export default {
showImage: this.showImage, showImage: this.showImage,
uploadUrl: this.uploadUrl, uploadUrl: this.uploadUrl,
uploader: this.uploader || this.$formulate.getUploader(), uploader: this.uploader || this.$formulate.getUploader(),
immediateUpload: this.immediateUpload, uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops,
...this.typeContext ...this.typeContext
}) })
}, },

View File

@ -8,14 +8,26 @@
*/ */
export default function (file, progress, error, options) { export default function (file, progress, error, options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const totalTime = options.fauxUploaderDuration || 2000 const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
const start = performance.now() const start = performance.now()
/**
* @todo - remove, intentional failure
*/
const fail = (Math.random() > 0.5)
const advance = () => setTimeout(() => { const advance = () => setTimeout(() => {
const elapsed = performance.now() - start const elapsed = performance.now() - start
const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100)) const currentProgress = Math.min(100, Math.round(elapsed / totalTime * 100))
progress(currentProgress) progress(currentProgress)
/**
* @todo - remove, intentional failure
*/
if (fail && currentProgress > 50) {
return error('There was an error uploading the file.')
}
if (currentProgress >= 100) { if (currentProgress >= 100) {
resolve({ return resolve({
url: 'http://via.placeholder.com/350x150.png', url: 'http://via.placeholder.com/350x150.png',
name: file.name name: file.name
}) })

View File

@ -149,14 +149,10 @@ export default {
mime: function (files, ...types) { mime: function (files, ...types) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (files instanceof FileUpload) { if (files instanceof FileUpload) {
if (files.hasUploader()) { const fileList = files.getFileList()
return false for (let i = 0; i < fileList.length; i++) {
} const file = fileList[i]
files = files.getFiles() if (!types.includes(file.type)) {
}
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (const i in files) {
if (!types.includes(files[i].type)) {
return false return false
} }
} }

View File

@ -113,6 +113,13 @@ export default {
return `${s(name)} must be less than ${args[0]} characters long.` return `${s(name)} must be less than ${args[0]} characters long.`
}, },
/**
* The (field-level) error message for mime errors.
*/
mime: function ({ name, args }) {
return `${s(name)} must of the the type: ${args[0] || 'No file formats allowed.'}`
},
/** /**
* The maximum value allowed. * The maximum value allowed.
*/ */

View File

@ -32,7 +32,6 @@ test('type "radio" with options renders a group', () => {
expect(wrapper.contains(FormulateInputGroup)).toBe(true) expect(wrapper.contains(FormulateInputGroup)).toBe(true)
}) })
test('labelPosition of type "checkbox" defaults to after', () => { test('labelPosition of type "checkbox" defaults to after', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } }) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
expect(wrapper.vm.context.labelPosition).toBe('after') expect(wrapper.vm.context.labelPosition).toBe('after')

View File

@ -1,4 +1,5 @@
import rules from '@/libs/rules' import rules from '@/libs/rules'
import FileUpload from '../src/FileUpload'
/** /**
* Required rule * Required rule
@ -295,9 +296,26 @@ describe('email', () => {
* Mime types. * Mime types.
*/ */
describe('mime', () => { describe('mime', () => {
it('passes basic image/jpeg stack', async () => expect(await rules.mime([{type: 'image/jpeg'}], 'image/png', 'image/jpeg')).toBe(true)) it('passes basic image/jpeg stack', async () => {
const fileUpload = new FileUpload({
files: [ { type: 'image/jpeg' } ]
})
expect(await rules.mime(fileUpload, 'image/png', 'image/jpeg')).toBe(true)
})
it('fails when not in stack', async () => expect(await rules.mime([{type: 'application/json'}], 'image/png', 'image/jpeg')).toBe(false)) it('passes when match is at begining of stack', async () => {
const fileUpload = new FileUpload({
files: [ { type: 'document/pdf' } ]
})
expect(await rules.mime(fileUpload, 'document/pdf')).toBe(true)
})
it('fails when not in stack', async () => {
const fileUpload = new FileUpload({
files: [ { type: 'application/json' } ]
})
expect(await rules.mime(fileUpload, 'image/png', 'image/jpeg')).toBe(false)
})
}) })
/** /**

View File

@ -31,7 +31,8 @@
margin: 0; margin: 0;
} }
.formulate-input-error { .formulate-input-error,
.formulate-file-upload-error {
color: $formulate-error; color: $formulate-error;
font-size: .8em; font-size: .8em;
font-weight: 300; font-weight: 300;
@ -286,11 +287,7 @@
.formulate-input-upload-area { .formulate-input-upload-area {
width: 100%; width: 100%;
position: relative; position: relative;
padding: 2em; padding: 2em 0;
&[data-has-files] {
padding: 0;
}
input { input {
cursor: pointer; cursor: pointer;
@ -306,6 +303,15 @@
z-index: 5; z-index: 5;
} }
&[data-has-files] {
padding: 0;
input {
display: none;
}
}
&-mask { &-mask {
border-radius: .4em; border-radius: .4em;
position: absolute; position: absolute;
@ -325,6 +331,8 @@
content: ''; content: '';
background-color: $formulate-gray-dd; background-color: $formulate-gray-dd;
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><path d="M29,58A29,29,0,1,0,0,29,29,29,0,0,0,29,58ZM29,4A25,25,0,1,1,4,29,25,25,0,0,1,29,4Z"/><polygon points="27 22 27 44.4 31 44.4 31 22 41.7 31.1 44.3 28.1 29 15 13.7 28.1 16.3 31.1 27 22"/></svg>'); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><path d="M29,58A29,29,0,1,0,0,29,29,29,0,0,0,29,58ZM29,4A25,25,0,1,1,4,29,25,25,0,0,1,29,4Z"/><polygon points="27 22 27 44.4 31 44.4 31 22 41.7 31.1 44.3 28.1 29 15 13.7 28.1 16.3 31.1 27 22"/></svg>');
mask-repeat: no-repeat;
mask-position: center;
width: 2em; width: 2em;
height: 2em; height: 2em;
position: absolute; position: absolute;
@ -345,17 +353,145 @@
} }
} }
.formulate-files { .formulate-files {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
.formulate-file-progress {
background-color: $formulate-gray-d;
height: .3em;
border-radius: 1.25em;
width: 5em;
overflow: hidden;
position: absolute;
right: .75em;
transition: height .25s, width .25s;
z-index: 2;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
opacity: 0;
transform: scale(.08);
background-color: $formulate-white;
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M8.76,56.2c-6.38-6.34,3.26-16,9.64-9.69L38,65.88,80.56,23.29c6.38-6.38,16.07,3.32,9.69,9.69L42.84,80.37a6.83,6.83,0,0,1-9.65,0Z"/></svg>');
mask-size: 77%;
mask-repeat: no-repeat;
mask-position: center;
z-index: 3;
}
&[data-just-finished] {
width: 1.25em;
height: 1.25em;
&::before {
transition: transform .25s .2s, opacity .25s .2s;
transform: scale(1);
opacity: 1;
}
}
&[data-is-finished] {
transition: height .25s, width .25s, left .25s, top, .25s, border-radius .25s;
width: .3em;
height: 100%;
right: 0;
border-radius: 0;
&::before {
transition: opacity .1s;
opacity: 0;
}
}
.formulate-file-progress-inner {
background-color: $formulate-green;
width: 1%;
position: absolute;
left: 0;
bottom: 0;
top: 0;
z-index: 2;
}
}
.formualte-file-name {
padding-left: 1.5em;
padding-right: 2em;
&::before {
position: absolute;
left: .7em;
top: 50%;
margin-top: -.7em;
background-color: $formulate-gray-dd;
content: '';
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64.06 83.59"><path d="M55.94,83.59a8.16,8.16,0,0,0,8.12-8.16V19.12a1.77,1.77,0,0,0-.52-1.25L46.21.59A1.69,1.69,0,0,0,45.14.08L44.69,0l-.18,0H8.13A8.18,8.18,0,0,0,0,8.16V75.41a8.16,8.16,0,0,0,8.13,8.16H55.94ZM46.68,6,58.11,17.38H46.68ZM3.52,75.43V8.16A4.64,4.64,0,0,1,8.13,3.52h35V19.16a1.75,1.75,0,0,0,1.76,1.74H60.55V75.43a4.65,4.65,0,0,1-4.61,4.65H8.13A4.65,4.65,0,0,1,3.52,75.43Z"/></svg>');
mask-repeat: no-repeat;
mask-size: contain;
width: 1.25em;
height: 1.25em;
display: inline-block;
margin-right: .5em;
}
}
.formulate-file-remove {
width: 1.25em;
height: 1.25em;
border-radius: 1em;
border: 1px solid $formulate-gray-dd;
background-color: $formulate-gray-dd;
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 59.1 59.2"><path d="M1.6,57.7a5,5,0,0,0,3.5,1.5,4.85,4.85,0,0,0,3.5-1.5l21-21,21,21a5,5,0,0,0,3.5,1.5,4.85,4.85,0,0,0,3.5-1.5,5,5,0,0,0,0-7.1l-21-21,21-21a5,5,0,0,0,0-7.1,5,5,0,0,0-7.1,0l-21,21L8.6,1.7a5,5,0,0,0-7.1,0,5,5,0,0,0,0,7.1l21,21L1.6,50.7A4.83,4.83,0,0,0,1.6,57.7Z"/></svg>');
mask-size: .6em;
mask-repeat: no-repeat;
mask-position: center;
cursor: pointer;
position: absolute;
right: .75em;
z-index: 1;
transition: transform .25s;
@media (pointer: fine) {
&:hover {
transform: scale(1.5);
}
}
}
li { li {
display: block;
&[data-has-error] {
.formulate-file-progress {
background-color: $formulate-error-l;
}
}
& + li {
margin-top: .5em;
}
}
.formulate-file {
@include baseinput; @include baseinput;
display: block; display: block;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
@mixin progress { @mixin progress {
appearance: none; appearance: none;
@ -369,5 +505,21 @@
} }
} }
} }
// Image uploads
// -----------------------------------------------------------------------------
[data-type="image"] {
.formulate-input-upload-area {
.formulate-input-upload-area-mask {
&::before {
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 71.05"><path d="M82.89,0H7.1A7.12,7.12,0,0,0,0,7.11V64a7.11,7.11,0,0,0,7.1,7.1H82.9A7.11,7.11,0,0,0,90,64V7.11A7.12,7.12,0,0,0,82.89,0ZM69.28,39.35a5.44,5.44,0,0,0-8,0L50.58,50.74,32.38,30.88a5.31,5.31,0,0,0-7.92,0L4.74,52.4V7.11A2.37,2.37,0,0,1,7.11,4.74H82.9a2.37,2.37,0,0,1,2.36,2.37V56.3Z"/><circle cx="67.74" cy="22.26" r="8.53"/></svg>');
}
}
}
}
} }
} }

View File

@ -16,11 +16,14 @@ $formulate-blue-l: #f3f4f4;
$formulate-green: #41b883; $formulate-green: #41b883;
$formulate-error: #960505; $formulate-error: #960505;
$formulate-error-l: #dc2c2c;
$formulate-yellow-d: #6b5900; $formulate-yellow-d: #6b5900;
$formulate-yellow: #e6c000; $formulate-yellow: #e6c000;
$formulate-yellow-l: #fff8d2; $formulate-yellow-l: #fff8d2;
$formulate-white: #ffffff;
// Mixins // Mixins