Kaynağa Gözat

Merge pull request #16007 from netchampfaris/better-autocompletion-api

fix(ux): Images in Markdown
version-14
mergify[bot] 3 yıl önce
committed by GitHub
ebeveyn
işleme
b8114eea9c
Veri tabanında bu imza için bilinen anahtar bulunamadı GPG Anahtar Kimliği: 4AEE18F83AFDEB23
11 değiştirilmiş dosya ile 236 ekleme ve 61 silme
  1. +22
    -0
      cypress/integration/control_markdown_editor.js
  2. +3
    -0
      cypress/support/commands.js
  3. +15
    -9
      frappe/public/js/frappe/file_uploader/FileUploader.vue
  4. +68
    -15
      frappe/public/js/frappe/file_uploader/ImageCropper.vue
  5. +12
    -8
      frappe/public/js/frappe/file_uploader/index.js
  6. +4
    -2
      frappe/public/js/frappe/form/controls/attach.js
  7. +0
    -1
      frappe/public/js/frappe/form/controls/attach_image.js
  8. +44
    -25
      frappe/public/js/frappe/form/controls/code.js
  9. +43
    -0
      frappe/public/js/frappe/form/controls/markdown_editor.js
  10. +24
    -0
      frappe/public/js/frappe/form/form.js
  11. +1
    -1
      frappe/public/js/frappe/form/sidebar/attachments.js

+ 22
- 0
cypress/integration/control_markdown_editor.js Dosyayı Görüntüle

@@ -0,0 +1,22 @@
context("Control Markdown Editor", () => {
before(() => {
cy.login();
cy.visit("/app");
});

it("should allow inserting images by drag and drop", () => {
cy.visit("/app/web-page/new");
cy.fill_field("content_type", "Markdown", "Select");
cy.get_field("main_section_md", "Markdown Editor").attachFile(
"sample_image.jpg",
{
subjectType: "drag-n-drop"
}
);
cy.click_modal_primary_button("Upload");
cy.get_field("main_section_md", "Markdown Editor").should(
"contain",
"![](/files/sample_image.jpg)"
);
});
});

+ 3
- 0
cypress/support/commands.js Dosyayı Görüntüle

@@ -174,6 +174,9 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
if (fieldtype === 'Code') {
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
}
if (fieldtype === 'Markdown Editor') {
selector = `[data-fieldname="${fieldname}"] .ace-editor-target`;
}

return cy.get(selector).first();
});


+ 15
- 9
frappe/public/js/frappe/file_uploader/FileUploader.vue Dosyayı Görüntüle

@@ -36,7 +36,7 @@
ref="file_input"
@change="on_file_input"
: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">
<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>
<ImageCropper
v-if="show_image_cropper"
v-if="show_image_cropper && wrapper_ready"
: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)"
@upload_after_crop="trigger_upload=true"
/>
@@ -171,7 +171,8 @@ export default {
default: () => ({
max_file_size: null, // 2048 -> 2KB
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: {
@@ -203,7 +204,8 @@ export default {
allow_web_link: true,
google_drive_settings: {
enabled: false
}
},
wrapper_ready: false
}
},
created() {
@@ -286,11 +288,12 @@ export default {
.filter(this.check_restrictions)
.map(file => {
let is_image = file.type.startsWith('image');
let size_kb = file.size / 1024;
return {
file_obj: file,
cropper_file: file,
crop_box_data: null,
optimize: this.attach_doc_image ? true : false,
optimize: size_kb > 200 && is_image && !file.type.includes('svg'),
name: file.name,
doc: null,
progress: 0,
@@ -303,12 +306,15 @@ export default {
}
});
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) {
let { max_file_size, allowed_file_types } = this.restrictions;
let { max_file_size, allowed_file_types = [] } = this.restrictions;

let mime_type = file.type;
let extension = '.' + file.name.split('.').pop();


+ 68
- 15
frappe/public/js/frappe/file_uploader/ImageCropper.vue Dosyayı Görüntüle

@@ -1,12 +1,39 @@
<template>
<div>
<div>
<img ref="image" :src="src" :alt="file.name"/>
<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 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>
</template>
@@ -15,22 +42,31 @@
import Cropper from "cropperjs";
export default {
name: "ImageCropper",
props: ["file", "attach_doc_image"],
props: ["file", "fixed_aspect_ratio"],
data() {
let aspect_ratio =
this.fixed_aspect_ratio != null ? this.fixed_aspect_ratio : NaN;
return {
src: null,
cropper: null,
image: null
image: null,
aspect_ratio
};
},
watch: {
aspect_ratio(value) {
if (this.cropper) {
this.cropper.setAspectRatio(value);
}
}
},
mounted() {
if (window.FileReader) {
let fr = new FileReader();
fr.onload = () => (this.src = fr.result);
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.onload = () => {
this.cropper = new Cropper(this.image, {
@@ -38,13 +74,31 @@ export default {
scalable: false,
viewMode: 1,
data: crop_box,
aspectRatio: aspect_ratio
aspectRatio: this.aspect_ratio
});
window.cropper = this.cropper;
};
},
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: {
@@ -58,9 +112,6 @@ export default {
});
this.file.file_obj = cropped_file_obj;
this.$emit("toggle_image_cropper");
if(this.attach_doc_image) {
this.$emit("upload_after_crop");
}
}, file_type);
}
}
@@ -75,6 +126,8 @@ img {

.image-cropper-actions {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
margin-top: var(--margin-md);
}
</style>

+ 12
- 8
frappe/public/js/frappe/file_uploader/index.js Dosyayı Görüntüle

@@ -10,11 +10,12 @@ export default class FileUploader {
fieldname,
files,
folder,
restrictions,
restrictions = {},
upload_notes,
allow_multiple,
as_dataurl,
disable_file_browser,
dialog_title,
attach_doc_image,
frm
} = {}) {
@@ -22,15 +23,11 @@ export default class FileUploader {
frm && frm.attachments.max_reached(true);

if (!wrapper) {
this.make_dialog();
this.make_dialog(dialog_title);
} else {
this.wrapper = wrapper.get ? wrapper.get(0) : wrapper;
}

if (attach_doc_image) {
restrictions.allowed_file_types = ['image/jpeg', 'image/png'];
}

this.$fileuploader = new Vue({
el: this.wrapper,
render: h => h(FileUploaderComponent, {
@@ -54,6 +51,10 @@ export default class FileUploader {

this.uploader = this.$fileuploader.$children[0];

if (!this.dialog) {
this.uploader.wrapper_ready = true;
}

this.uploader.$watch('files', (files) => {
let all_private = files.every(file => file.private);
if (this.dialog) {
@@ -94,14 +95,17 @@ export default class FileUploader {
return this.uploader.upload_files();
}

make_dialog() {
make_dialog(title) {
this.dialog = new frappe.ui.Dialog({
title: __('Upload'),
title: title || __('Upload'),
primary_action_label: __('Upload'),
primary_action: () => this.upload_files(),
secondary_action_label: __('Set all private'),
secondary_action: () => {
this.uploader.toggle_all_private();
},
on_page_show: () => {
this.uploader.wrapper_ready = true;
}
});



+ 4
- 2
frappe/public/js/frappe/form/controls/attach.js Dosyayı Görüntüle

@@ -61,7 +61,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
}
on_attach_doc_image() {
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);
}
set_upload_options() {
@@ -70,7 +71,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
on_success: file => {
this.on_upload_complete(file);
this.toggle_reload_button();
}
},
restrictions: {}
};

if (this.frm) {


+ 0
- 1
frappe/public/js/frappe/form/controls/attach_image.js Dosyayı Görüntüle

@@ -19,7 +19,6 @@ frappe.ui.form.ControlAttachImage = class ControlAttachImage extends frappe.ui.f
}
set_upload_options() {
super.set_upload_options();
this.upload_options.restrictions = {};
this.upload_options.restrictions.allowed_file_types = ['image/*'];
}
};

+ 44
- 25
frappe/public/js/frappe/form/controls/code.js Dosyayı Görüntüle

@@ -54,17 +54,57 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
return this._autocompletions || [];
},
set: (value) => {
let getter = value;
if (typeof getter !== 'function') {
getter = () => value;
}
if (!this._autocompletions) {
this._autocompletions = [];
}
this._autocompletions.push(getter);
this.setup_autocompletion();
this.df._autocompletions = value;
}
});
}

setup_autocompletion() {
setup_autocompletion(customGetCompletions) {
if (this._autocompletion_setup) return;

const ace = window.ace;
const get_autocompletions = () => this.df.autocompletions;

let getCompletions = (editor, session, pos, prefix, callback) => {
if (prefix.length === 0) {
callback(null, []);
return;
}
const get_autocompletions = () => {
let getters = this._autocompletions || [];
let completions = [];
for (let getter of getters) {
let values = getter({ editor, session, pos, prefix });
completions.push(...values);
}
return completions;
};
let autocompletions = get_autocompletions();
if (autocompletions.length) {
callback(
null,
autocompletions.map(a => {
if (typeof a === "string") {
a = { value: a };
}
return {
name: "frappe",
value: a.value,
score: a.score,
meta: a.meta,
caption: a.caption
};
})
);
}
};

ace.config.loadModule("ace/ext/language_tools", langTools => {
this.editor.setOptions({
@@ -73,28 +113,7 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
});

langTools.addCompleter({
getCompletions: function(editor, session, pos, prefix, callback) {
if (prefix.length === 0) {
callback(null, []);
return;
}
let autocompletions = get_autocompletions();
if (autocompletions.length) {
callback(
null,
autocompletions.map(a => {
if (typeof a === 'string') {
a = { value: a };
}
return {
name: 'frappe',
value: a.value,
score: a.score
};
})
);
}
}
getCompletions: customGetCompletions || getCompletions
});
});
this._autocompletion_setup = true;


+ 43
- 0
frappe/public/js/frappe/form/controls/markdown_editor.js Dosyayı Görüntüle

@@ -29,6 +29,8 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp

this.markdown_preview = $(`<div class="${editor_class}-preview border rounded">`).hide();
this.markdown_container.append(this.markdown_preview);

this.setup_image_drop();
}

set_language() {
@@ -53,4 +55,45 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp
set_disp_area(value) {
this.disp_area && $(this.disp_area).text(value);
}

setup_image_drop() {
this.ace_editor_target.on('drop', e => {
e.stopPropagation();
e.preventDefault();
let { dataTransfer } = e.originalEvent;
if (!dataTransfer?.files?.length) {
return;
}
let files = dataTransfer.files;
if (!files[0].type.includes('image')) {
frappe.show_alert({
message: __('You can only insert images in Markdown fields', [files[0].name]),
indicator: 'orange'
});
return;
}

new frappe.ui.FileUploader({
dialog_title: __('Insert Image in Markdown'),
doctype: this.doctype,
docname: this.docname,
frm: this.frm,
files,
folder: 'Home/Attachments',
allow_multiple: false,
restrictions: {
allowed_file_types: ['image/*']
},
on_success: (file_doc) => {
if (this.frm && !this.frm.is_new()) {
this.frm.attachments.attachment_uploaded(file_doc);
}
this.editor.session.insert(
this.editor.getCursorPosition(),
`![](${encodeURI(file_doc.file_url)})`
);
}
});
});
}
};

+ 24
- 0
frappe/public/js/frappe/form/form.js Dosyayı Görüntüle

@@ -319,6 +319,25 @@ frappe.ui.form.Form = class FrappeForm {
});
}

setup_image_autocompletions_in_markdown() {
this.fields.map(field => {
if (field.df.fieldtype === 'Markdown Editor') {
this.set_df_property(field.df.fieldname, 'autocompletions', () => {
let attachments = this.attachments.get_attachments();
return attachments
.filter(file => frappe.utils.is_image_file(file.file_url))
.map(file => {
return {
caption: 'image: ' + file.file_name,
value: `![](${file.file_url})`,
meta: 'image'
};
});
});
}
});
}

// REFRESH

refresh(docname) {
@@ -533,6 +552,7 @@ frappe.ui.form.Form = class FrappeForm {
// call onload post render for callbacks to be fired
() => {
if(this.cscript.is_onload) {
this.onload_post_render();
return this.script_manager.trigger("onload_post_render");
}
},
@@ -560,6 +580,10 @@ frappe.ui.form.Form = class FrappeForm {
});
}

onload_post_render() {
this.setup_image_autocompletions_in_markdown();
}

set_first_tab_as_active() {
this.layout.tabs[0]
&& this.layout.tabs[0].set_active();


+ 1
- 1
frappe/public/js/frappe/form/sidebar/attachments.js Dosyayı Görüntüle

@@ -62,7 +62,7 @@ frappe.ui.form.Attachments = class Attachments {

}
get_attachments() {
return this.frm.get_docinfo().attachments;
return this.frm.get_docinfo().attachments || [];
}
add_attachment(attachment) {
var file_name = attachment.file_name;


Yükleniyor…
İptal
Kaydet