- New option: crop_image_aspect_ratio to force an aspect ratio during cropping - ImageCropper: Add aspect ratio buttonsversion-14
@@ -36,7 +36,7 @@ | |||||
ref="file_input" | ref="file_input" | ||||
@change="on_file_input" | @change="on_file_input" | ||||
:multiple="allow_multiple" | :multiple="allow_multiple" | ||||
:accept="restrictions.allowed_file_types.join(', ')" | |||||
:accept="(restrictions.allowed_file_types || []).join(', ')" | |||||
> | > | ||||
<button class="btn btn-file-upload" v-if="!disable_file_browser" @click="show_file_browser = true"> | <button class="btn btn-file-upload" v-if="!disable_file_browser" @click="show_file_browser = true"> | ||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> | <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
@@ -108,9 +108,9 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<ImageCropper | <ImageCropper | ||||
v-if="show_image_cropper" | |||||
v-if="show_image_cropper && wrapper_ready" | |||||
:file="files[crop_image_with_index]" | :file="files[crop_image_with_index]" | ||||
:attach_doc_image="attach_doc_image" | |||||
:fixed_aspect_ratio="restrictions.crop_image_aspect_ratio" | |||||
@toggle_image_cropper="toggle_image_cropper(-1)" | @toggle_image_cropper="toggle_image_cropper(-1)" | ||||
@upload_after_crop="trigger_upload=true" | @upload_after_crop="trigger_upload=true" | ||||
/> | /> | ||||
@@ -171,7 +171,8 @@ export default { | |||||
default: () => ({ | default: () => ({ | ||||
max_file_size: null, // 2048 -> 2KB | max_file_size: null, // 2048 -> 2KB | ||||
max_number_of_files: null, | max_number_of_files: null, | ||||
allowed_file_types: [] // ['image/*', 'video/*', '.jpg', '.gif', '.pdf'] | |||||
allowed_file_types: [], // ['image/*', 'video/*', '.jpg', '.gif', '.pdf'], | |||||
crop_image_aspect_ratio: null // 1, 16 / 9, 4 / 3, NaN (free) | |||||
}) | }) | ||||
}, | }, | ||||
attach_doc_image: { | attach_doc_image: { | ||||
@@ -203,7 +204,8 @@ export default { | |||||
allow_web_link: true, | allow_web_link: true, | ||||
google_drive_settings: { | google_drive_settings: { | ||||
enabled: false | enabled: false | ||||
} | |||||
}, | |||||
wrapper_ready: false | |||||
} | } | ||||
}, | }, | ||||
created() { | created() { | ||||
@@ -286,11 +288,12 @@ export default { | |||||
.filter(this.check_restrictions) | .filter(this.check_restrictions) | ||||
.map(file => { | .map(file => { | ||||
let is_image = file.type.startsWith('image'); | let is_image = file.type.startsWith('image'); | ||||
let size_kb = file.size / 1024; | |||||
return { | return { | ||||
file_obj: file, | file_obj: file, | ||||
cropper_file: file, | cropper_file: file, | ||||
crop_box_data: null, | crop_box_data: null, | ||||
optimize: this.attach_doc_image ? true : false, | |||||
optimize: size_kb > 200 && is_image && !file.type.includes('svg'), | |||||
name: file.name, | name: file.name, | ||||
doc: null, | doc: null, | ||||
progress: 0, | progress: 0, | ||||
@@ -303,12 +306,15 @@ export default { | |||||
} | } | ||||
}); | }); | ||||
this.files = this.files.concat(files); | this.files = this.files.concat(files); | ||||
if(this.files.length != 0 && this.attach_doc_image) { | |||||
this.toggle_image_cropper(0); | |||||
// if only one file is allowed and crop_image_aspect_ratio is set, open cropper immediately | |||||
if (this.files.length === 1 && !this.allow_multiple && this.restrictions.crop_image_aspect_ratio != null) { | |||||
if (!this.files[0].file_obj.type.includes('svg')) { | |||||
this.toggle_image_cropper(0); | |||||
} | |||||
} | } | ||||
}, | }, | ||||
check_restrictions(file) { | check_restrictions(file) { | ||||
let { max_file_size, allowed_file_types } = this.restrictions; | |||||
let { max_file_size, allowed_file_types = [] } = this.restrictions; | |||||
let mime_type = file.type; | let mime_type = file.type; | ||||
let extension = '.' + file.name.split('.').pop(); | let extension = '.' + file.name.split('.').pop(); | ||||
@@ -1,12 +1,39 @@ | |||||
<template> | <template> | ||||
<div> | <div> | ||||
<div> | <div> | ||||
<img ref="image" :src="src" :alt="file.name"/> | |||||
<img ref="image" :src="src" :alt="file.name" /> | |||||
</div> | </div> | ||||
<br/> | |||||
<div class="image-cropper-actions"> | <div class="image-cropper-actions"> | ||||
<button class="btn btn-sm margin-right" v-if="!attach_doc_image" @click="$emit('toggle_image_cropper')">Back</button> | |||||
<button class="btn btn-primary btn-sm margin-right" @click="crop_image" v-html="crop_button_text"></button> | |||||
<div> | |||||
<div class="btn-group" v-if="fixed_aspect_ratio == null"> | |||||
<button | |||||
v-for="button in aspect_ratio_buttons" | |||||
type="button" | |||||
class="btn btn-default btn-sm" | |||||
:class="{ | |||||
active: isNaN(aspect_ratio) | |||||
? isNaN(button.value) | |||||
: button.value === aspect_ratio | |||||
}" | |||||
:key="button.label" | |||||
@click="aspect_ratio = button.value" | |||||
> | |||||
{{ button.label }} | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<div> | |||||
<button | |||||
class="btn btn-sm margin-right" | |||||
@click="$emit('toggle_image_cropper')" | |||||
v-if="fixed_aspect_ratio == null" | |||||
> | |||||
{{ __("Back") }} | |||||
</button> | |||||
<button class="btn btn-primary btn-sm" @click="crop_image"> | |||||
{{ __("Crop") }} | |||||
</button> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</template> | </template> | ||||
@@ -15,22 +42,31 @@ | |||||
import Cropper from "cropperjs"; | import Cropper from "cropperjs"; | ||||
export default { | export default { | ||||
name: "ImageCropper", | name: "ImageCropper", | ||||
props: ["file", "attach_doc_image"], | |||||
props: ["file", "fixed_aspect_ratio"], | |||||
data() { | data() { | ||||
let aspect_ratio = | |||||
this.fixed_aspect_ratio != null ? this.fixed_aspect_ratio : NaN; | |||||
return { | return { | ||||
src: null, | src: null, | ||||
cropper: null, | cropper: null, | ||||
image: null | |||||
image: null, | |||||
aspect_ratio | |||||
}; | }; | ||||
}, | }, | ||||
watch: { | |||||
aspect_ratio(value) { | |||||
if (this.cropper) { | |||||
this.cropper.setAspectRatio(value); | |||||
} | |||||
} | |||||
}, | |||||
mounted() { | mounted() { | ||||
if (window.FileReader) { | if (window.FileReader) { | ||||
let fr = new FileReader(); | let fr = new FileReader(); | ||||
fr.onload = () => (this.src = fr.result); | fr.onload = () => (this.src = fr.result); | ||||
fr.readAsDataURL(this.file.cropper_file); | fr.readAsDataURL(this.file.cropper_file); | ||||
} | } | ||||
aspect_ratio = this.attach_doc_image ? 1 : NaN; | |||||
crop_box = this.file.crop_box_data; | |||||
let crop_box = this.file.crop_box_data; | |||||
this.image = this.$refs.image; | this.image = this.$refs.image; | ||||
this.image.onload = () => { | this.image.onload = () => { | ||||
this.cropper = new Cropper(this.image, { | this.cropper = new Cropper(this.image, { | ||||
@@ -38,13 +74,31 @@ export default { | |||||
scalable: false, | scalable: false, | ||||
viewMode: 1, | viewMode: 1, | ||||
data: crop_box, | data: crop_box, | ||||
aspectRatio: aspect_ratio | |||||
aspectRatio: this.aspect_ratio | |||||
}); | }); | ||||
window.cropper = this.cropper; | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | computed: { | ||||
crop_button_text() { | |||||
return this.attach_doc_image ? "Upload" : "Crop"; | |||||
aspect_ratio_buttons() { | |||||
return [ | |||||
{ | |||||
label: __("1:1"), | |||||
value: 1 | |||||
}, | |||||
{ | |||||
label: __("4:3"), | |||||
value: 4 / 3 | |||||
}, | |||||
{ | |||||
label: __("16:9"), | |||||
value: 16 / 9 | |||||
}, | |||||
{ | |||||
label: __("Free"), | |||||
value: NaN | |||||
} | |||||
]; | |||||
} | } | ||||
}, | }, | ||||
methods: { | methods: { | ||||
@@ -58,9 +112,6 @@ export default { | |||||
}); | }); | ||||
this.file.file_obj = cropped_file_obj; | this.file.file_obj = cropped_file_obj; | ||||
this.$emit("toggle_image_cropper"); | this.$emit("toggle_image_cropper"); | ||||
if(this.attach_doc_image) { | |||||
this.$emit("upload_after_crop"); | |||||
} | |||||
}, file_type); | }, file_type); | ||||
} | } | ||||
} | } | ||||
@@ -75,6 +126,8 @@ img { | |||||
.image-cropper-actions { | .image-cropper-actions { | ||||
display: flex; | display: flex; | ||||
justify-content: flex-end; | |||||
align-items: center; | |||||
justify-content: space-between; | |||||
margin-top: var(--margin-md); | |||||
} | } | ||||
</style> | </style> |
@@ -10,11 +10,12 @@ export default class FileUploader { | |||||
fieldname, | fieldname, | ||||
files, | files, | ||||
folder, | folder, | ||||
restrictions, | |||||
restrictions = {}, | |||||
upload_notes, | upload_notes, | ||||
allow_multiple, | allow_multiple, | ||||
as_dataurl, | as_dataurl, | ||||
disable_file_browser, | disable_file_browser, | ||||
dialog_title, | |||||
attach_doc_image, | attach_doc_image, | ||||
frm | frm | ||||
} = {}) { | } = {}) { | ||||
@@ -22,15 +23,11 @@ export default class FileUploader { | |||||
frm && frm.attachments.max_reached(true); | frm && frm.attachments.max_reached(true); | ||||
if (!wrapper) { | if (!wrapper) { | ||||
this.make_dialog(); | |||||
this.make_dialog(dialog_title); | |||||
} else { | } else { | ||||
this.wrapper = wrapper.get ? wrapper.get(0) : wrapper; | this.wrapper = wrapper.get ? wrapper.get(0) : wrapper; | ||||
} | } | ||||
if (attach_doc_image) { | |||||
restrictions.allowed_file_types = ['image/jpeg', 'image/png']; | |||||
} | |||||
this.$fileuploader = new Vue({ | this.$fileuploader = new Vue({ | ||||
el: this.wrapper, | el: this.wrapper, | ||||
render: h => h(FileUploaderComponent, { | render: h => h(FileUploaderComponent, { | ||||
@@ -54,6 +51,10 @@ export default class FileUploader { | |||||
this.uploader = this.$fileuploader.$children[0]; | this.uploader = this.$fileuploader.$children[0]; | ||||
if (!this.dialog) { | |||||
this.uploader.wrapper_ready = true; | |||||
} | |||||
this.uploader.$watch('files', (files) => { | this.uploader.$watch('files', (files) => { | ||||
let all_private = files.every(file => file.private); | let all_private = files.every(file => file.private); | ||||
if (this.dialog) { | if (this.dialog) { | ||||
@@ -94,14 +95,17 @@ export default class FileUploader { | |||||
return this.uploader.upload_files(); | return this.uploader.upload_files(); | ||||
} | } | ||||
make_dialog() { | |||||
make_dialog(title) { | |||||
this.dialog = new frappe.ui.Dialog({ | this.dialog = new frappe.ui.Dialog({ | ||||
title: __('Upload'), | |||||
title: title || __('Upload'), | |||||
primary_action_label: __('Upload'), | primary_action_label: __('Upload'), | ||||
primary_action: () => this.upload_files(), | primary_action: () => this.upload_files(), | ||||
secondary_action_label: __('Set all private'), | secondary_action_label: __('Set all private'), | ||||
secondary_action: () => { | secondary_action: () => { | ||||
this.uploader.toggle_all_private(); | this.uploader.toggle_all_private(); | ||||
}, | |||||
on_page_show: () => { | |||||
this.uploader.wrapper_ready = true; | |||||
} | } | ||||
}); | }); | ||||
@@ -61,7 +61,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||||
} | } | ||||
on_attach_doc_image() { | on_attach_doc_image() { | ||||
this.set_upload_options(); | this.set_upload_options(); | ||||
this.upload_options["attach_doc_image"] = true; | |||||
this.upload_options.restrictions.allowed_file_types = ['image/*']; | |||||
this.upload_options.restrictions.crop_image_aspect_ratio = 1; | |||||
this.file_uploader = new frappe.ui.FileUploader(this.upload_options); | this.file_uploader = new frappe.ui.FileUploader(this.upload_options); | ||||
} | } | ||||
set_upload_options() { | set_upload_options() { | ||||
@@ -70,7 +71,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||||
on_success: file => { | on_success: file => { | ||||
this.on_upload_complete(file); | this.on_upload_complete(file); | ||||
this.toggle_reload_button(); | this.toggle_reload_button(); | ||||
} | |||||
}, | |||||
restrictions: {} | |||||
}; | }; | ||||
if (this.frm) { | if (this.frm) { | ||||
@@ -19,7 +19,6 @@ frappe.ui.form.ControlAttachImage = class ControlAttachImage extends frappe.ui.f | |||||
} | } | ||||
set_upload_options() { | set_upload_options() { | ||||
super.set_upload_options(); | super.set_upload_options(); | ||||
this.upload_options.restrictions = {}; | |||||
this.upload_options.restrictions.allowed_file_types = ['image/*']; | this.upload_options.restrictions.allowed_file_types = ['image/*']; | ||||
} | } | ||||
}; | }; |