Просмотр исходного кода

feat: Image cropping

version-14
MitulDavid 3 лет назад
Родитель
Сommit
47ac923b3e
10 измененных файлов: 182 добавлений и 7 удалений
  1. +6
    -0
      frappe/public/icons/timeless/symbol-defs.svg
  2. +22
    -1
      frappe/public/js/frappe/file_uploader/FilePreview.vue
  3. +34
    -3
      frappe/public/js/frappe/file_uploader/FileUploader.vue
  4. +77
    -0
      frappe/public/js/frappe/file_uploader/ImageCropper.vue
  5. +23
    -0
      frappe/public/js/frappe/file_uploader/index.js
  6. +12
    -2
      frappe/public/js/frappe/form/controls/attach.js
  7. +1
    -1
      frappe/public/js/frappe/form/sidebar/user_image.js
  8. +1
    -0
      frappe/public/scss/desk/form.scss
  9. +1
    -0
      package.json
  10. +5
    -0
      yarn.lock

+ 6
- 0
frappe/public/icons/timeless/symbol-defs.svg Просмотреть файл

@@ -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>

+ 22
- 1
frappe/public/js/frappe/file_uploader/FilePreview.vue Просмотреть файл

@@ -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>

+ 34
- 3
frappe/public/js/frappe/file_uploader/FileUploader.vue Просмотреть файл

@@ -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
- 0
frappe/public/js/frappe/file_uploader/ImageCropper.vue Просмотреть файл

@@ -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>

+ 23
- 0
frappe/public/js/frappe/file_uploader/index.js Просмотреть файл

@@ -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);
}


+ 12
- 2
frappe/public/js/frappe/form/controls/attach.js Просмотреть файл

@@ -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,


+ 1
- 1
frappe/public/js/frappe/form/sidebar/user_image.js Просмотреть файл

@@ -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
- 0
frappe/public/scss/desk/form.scss Просмотреть файл

@@ -1,4 +1,5 @@
@import "../common/form.scss";
@import '~cropperjs/dist/cropper.min';

.form-section, .form-dashboard-section {
margin: 0px;


+ 1
- 0
package.json Просмотреть файл

@@ -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",


+ 5
- 0
yarn.lock Просмотреть файл

@@ -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"


Загрузка…
Отмена
Сохранить