@@ -699,4 +699,10 @@ | |||
<path d="M7.971 8.259a1.305 1.305 0 100-2.61 1.305 1.305 0 000 2.61z"></path> | |||
</g> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" id="icon-crop"> | |||
<path d="M23.5 18.07 5.86 18.07 5.86 0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
<line x1="18.14" y1="18.07" x2="18.14" y2="23.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M8.71 5.93 18.14 5.93 18.14 15.38" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
<line x1="0.5" y1="5.93" x2="5.86" y2="5.93" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
</symbol> | |||
</svg> |
@@ -40,7 +40,10 @@ | |||
/> | |||
<div v-if="uploaded" v-html="frappe.utils.icon('solid-success', 'lg')"></div> | |||
<div v-if="file.failed" v-html="frappe.utils.icon('solid-red', 'lg')"></div> | |||
<button v-if="!uploaded && !file.uploading" class="btn" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button> | |||
<div class="file-action-buttons"> | |||
<button v-if="is_cropable" class="btn muted" @click="$emit('toggle_image_cropper')" v-html="frappe.utils.icon('crop', 'md')"></button> | |||
<button v-if="!uploaded && !file.uploading" class="btn muted" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
@@ -89,6 +92,10 @@ export default { | |||
is_image() { | |||
return this.file.file_obj.type.startsWith('image'); | |||
}, | |||
is_cropable() { | |||
let croppable_types = ['image/jpeg', 'image/png']; | |||
return !this.uploaded && !this.file.uploading && croppable_types.includes(this.file.file_obj.type); | |||
}, | |||
progress() { | |||
let value = Math.round((this.file.progress * 100) / this.file.total); | |||
if (isNaN(value)) { | |||
@@ -173,4 +180,18 @@ export default { | |||
padding: var(--padding-xs); | |||
box-shadow: none; | |||
} | |||
.file-action-buttons { | |||
display: flex; | |||
justify-content: flex-end; | |||
} | |||
.muted { | |||
opacity: 0.5; | |||
transition: 0.3s; | |||
} | |||
.muted:hover { | |||
opacity: 1; | |||
} | |||
</style> |
@@ -46,7 +46,7 @@ | |||
</svg> | |||
<div class="mt-1">{{ __('Library') }}</div> | |||
</button> | |||
<button class="btn btn-file-upload" @click="show_web_link = true"> | |||
<button class="btn btn-file-upload" v-if="allow_web_link" @click="show_web_link = true"> | |||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
<circle cx="15" cy="15" r="15" fill="#ECAC4B"/> | |||
<path d="M12.0469 17.9543L17.9558 12.0454" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> | |||
@@ -79,13 +79,14 @@ | |||
</div> | |||
</div> | |||
<div class="file-preview-area" v-show="files.length && !show_file_browser && !show_web_link"> | |||
<div class="file-preview-container"> | |||
<div class="file-preview-container" v-if="!show_image_cropper"> | |||
<FilePreview | |||
v-for="(file, i) in files" | |||
:key="file.name" | |||
:file="file" | |||
@remove="remove_file(file)" | |||
@toggle_private="file.private = !file.private" | |||
@toggle_image_cropper="toggle_image_cropper(i)" | |||
/> | |||
</div> | |||
<div class="flex align-center" v-if="show_upload_button && currently_uploading === -1"> | |||
@@ -105,6 +106,13 @@ | |||
</div> | |||
</div> | |||
</div> | |||
<ImageCropper | |||
v-if="show_image_cropper" | |||
:file="files[crop_image_with_index]" | |||
:attach_doc_image="attach_doc_image" | |||
@toggle_image_cropper="toggle_image_cropper(-1)" | |||
@upload_after_crop="trigger_upload=true" | |||
/> | |||
<FileBrowser | |||
ref="file_browser" | |||
v-if="show_file_browser && !disable_file_browser" | |||
@@ -123,6 +131,7 @@ import FilePreview from './FilePreview.vue'; | |||
import FileBrowser from './FileBrowser.vue'; | |||
import WebLink from './WebLink.vue'; | |||
import GoogleDrivePicker from '../../integrations/google_drive_picker'; | |||
import ImageCropper from './ImageCropper.vue'; | |||
export default { | |||
name: 'FileUploader', | |||
@@ -164,6 +173,9 @@ export default { | |||
allowed_file_types: [] // ['image/*', 'video/*', '.jpg', '.gif', '.pdf'] | |||
}) | |||
}, | |||
attach_doc_image: { | |||
default: false | |||
}, | |||
upload_notes: { | |||
default: null // "Images or video, upto 2MB" | |||
} | |||
@@ -171,7 +183,8 @@ export default { | |||
components: { | |||
FilePreview, | |||
FileBrowser, | |||
WebLink | |||
WebLink, | |||
ImageCropper | |||
}, | |||
data() { | |||
return { | |||
@@ -180,7 +193,12 @@ export default { | |||
currently_uploading: -1, | |||
show_file_browser: false, | |||
show_web_link: false, | |||
show_image_cropper: false, | |||
crop_image_with_index: -1, | |||
trigger_upload: false, | |||
hide_dialog_footer: false, | |||
allow_take_photo: false, | |||
allow_web_link: true, | |||
google_drive_settings: { | |||
enabled: false | |||
} | |||
@@ -199,6 +217,11 @@ export default { | |||
} | |||
}); | |||
} | |||
if(this.attach_doc_image) { | |||
this.allow_web_link = false; | |||
this.allow_take_photo = false; | |||
this.google_drive_settings.enabled = false; | |||
} | |||
}, | |||
watch: { | |||
files(newvalue, oldvalue) { | |||
@@ -234,6 +257,11 @@ export default { | |||
remove_file(file) { | |||
this.files = this.files.filter(f => f !== file); | |||
}, | |||
toggle_image_cropper(index) { | |||
this.crop_image_with_index = this.show_image_cropper ? -1 : index; | |||
this.hide_dialog_footer = !this.show_image_cropper; | |||
this.show_image_cropper = !this.show_image_cropper; | |||
}, | |||
toggle_all_private() { | |||
let flag; | |||
let private_values = this.files.filter(file => file.private); | |||
@@ -267,6 +295,9 @@ export default { | |||
} | |||
}); | |||
this.files = this.files.concat(files); | |||
if(this.attach_doc_image) { | |||
this.toggle_image_cropper(0); | |||
} | |||
}, | |||
check_restrictions(file) { | |||
let { max_file_size, allowed_file_types } = this.restrictions; | |||
@@ -0,0 +1,77 @@ | |||
<template> | |||
<div> | |||
<div> | |||
<img ref="image" :src="src" :alt="file.name"/> | |||
</div> | |||
<br/> | |||
<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> | |||
</template> | |||
<script> | |||
import Cropper from "cropperjs"; | |||
export default { | |||
name: "ImageCropper", | |||
props: ["file", "attach_doc_image"], | |||
data() { | |||
return { | |||
src: null, | |||
cropper: null, | |||
image: null | |||
}; | |||
}, | |||
mounted() { | |||
if (window.FileReader) { | |||
let fr = new FileReader(); | |||
fr.onload = () => (this.src = fr.result); | |||
fr.readAsDataURL(this.file.file_obj); | |||
} | |||
aspect_ratio = this.attach_doc_image ? 1 : NaN; | |||
this.image = this.$refs.image; | |||
this.image.onload = () => { | |||
this.cropper = new Cropper(this.image, { | |||
zoomable: false, | |||
scalable: false, | |||
viewMode: 1, | |||
aspectRatio: aspect_ratio | |||
}); | |||
}; | |||
}, | |||
computed: { | |||
crop_button_text() { | |||
return this.attach_doc_image ? "Upload" : "Crop"; | |||
} | |||
}, | |||
methods: { | |||
crop_image() { | |||
const canvas = this.cropper.getCroppedCanvas(); | |||
const file_type = this.file.file_obj.type; | |||
canvas.toBlob(blob => { | |||
var cropped_file_obj = new File([blob], this.file.name, { | |||
type: blob.type | |||
}); | |||
this.file.file_obj = cropped_file_obj; | |||
this.$emit("toggle_image_cropper"); | |||
if(this.attach_doc_image) { | |||
this.$emit("upload_after_crop"); | |||
} | |||
}, file_type); | |||
} | |||
} | |||
}; | |||
</script> | |||
<style> | |||
img { | |||
display: block; | |||
max-width: 100%; | |||
max-height: 600px; | |||
} | |||
.image-cropper-actions { | |||
display: flex; | |||
justify-content: flex-end; | |||
} | |||
</style> |
@@ -15,6 +15,7 @@ export default class FileUploader { | |||
allow_multiple, | |||
as_dataurl, | |||
disable_file_browser, | |||
attach_doc_image, | |||
frm | |||
} = {}) { | |||
@@ -26,6 +27,12 @@ export default class FileUploader { | |||
this.wrapper = wrapper.get ? wrapper.get(0) : wrapper; | |||
} | |||
if (attach_doc_image) { | |||
disable_file_browser = true; | |||
restrictions.allowed_file_types = ['.jpg', '.png']; | |||
this.dialog && this.dialog.footer.addClass('hide'); | |||
} | |||
this.$fileuploader = new Vue({ | |||
el: this.wrapper, | |||
render: h => h(FileUploaderComponent, { | |||
@@ -42,6 +49,7 @@ export default class FileUploader { | |||
allow_multiple, | |||
as_dataurl, | |||
disable_file_browser, | |||
attach_doc_image, | |||
} | |||
}) | |||
}); | |||
@@ -55,6 +63,21 @@ export default class FileUploader { | |||
} | |||
}, { deep: true }); | |||
this.uploader.$watch('trigger_upload', (trigger_upload) => { | |||
if (trigger_upload) { | |||
this.upload_files(); | |||
} | |||
}); | |||
this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => { | |||
if (hide_dialog_footer) { | |||
this.dialog && this.dialog.footer.addClass('hide'); | |||
} | |||
else { | |||
this.dialog && this.dialog.footer.removeClass('hide'); | |||
} | |||
}); | |||
if (files && files.length) { | |||
this.uploader.add_files(files); | |||
} | |||
@@ -4,8 +4,13 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||
this.$input = $('<button class="btn btn-default btn-sm btn-attach">') | |||
.html(__("Attach")) | |||
.prependTo(me.input_area) | |||
.on("click", function() { | |||
me.on_attach_click(); | |||
.on({ | |||
click: function() { | |||
me.on_attach_click(); | |||
}, | |||
attach_doc_image: function() { | |||
me.on_attach_doc_image(); | |||
} | |||
}); | |||
this.$value = $( | |||
`<div class="attached-file flex justify-between align-center"> | |||
@@ -54,6 +59,11 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||
this.set_upload_options(); | |||
this.file_uploader = new frappe.ui.FileUploader(this.upload_options); | |||
} | |||
on_attach_doc_image() { | |||
this.set_upload_options(); | |||
this.upload_options["attach_doc_image"] = true; | |||
this.file_uploader = new frappe.ui.FileUploader(this.upload_options); | |||
} | |||
set_upload_options() { | |||
let options = { | |||
allow_multiple: false, | |||
@@ -83,7 +83,7 @@ frappe.ui.form.setup_user_image_event = function(frm) { | |||
if(!field.$input) { | |||
field.make_input(); | |||
} | |||
field.$input.trigger('click'); | |||
field.$input.trigger('attach_doc_image'); | |||
} else { | |||
/// on remove event for a sidebar image wrapper remove attach file. | |||
frm.attachments.remove_attachment_by_filename(frm.doc[frm.meta.image_field], function() { | |||
@@ -1,4 +1,5 @@ | |||
@import "../common/form.scss"; | |||
@import '~cropperjs/dist/cropper.min'; | |||
.form-section, .form-dashboard-section { | |||
margin: 0px; | |||
@@ -27,6 +27,7 @@ | |||
"bootstrap": "4.5.0", | |||
"cliui": "^7.0.4", | |||
"cookie": "^0.4.0", | |||
"cropperjs": "^1.5.12", | |||
"cssnano": "^5.0.0", | |||
"driver.js": "^0.9.8", | |||
"express": "^4.17.1", | |||
@@ -1500,6 +1500,11 @@ cosmiconfig@^7.0.0: | |||
path-type "^4.0.0" | |||
yaml "^1.10.0" | |||
cropperjs@^1.5.12: | |||
version "1.5.12" | |||
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.12.tgz#d9c0db2bfb8c0d769d51739e8f916bbc44e10f50" | |||
integrity sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw== | |||
cross-spawn@^3.0.0: | |||
version "3.0.1" | |||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" | |||