feat: Image cropping
This commit is contained in:
parent
8dfbc14852
commit
47ac923b3e
10 changed files with 182 additions and 7 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
77
frappe/public/js/frappe/file_uploader/ImageCropper.vue
Normal file
77
frappe/public/js/frappe/file_uploader/ImageCropper.vue
Normal file
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue