feat: Better Letterhead editing

- Image resize and align
This commit is contained in:
Faris Ansari 2021-10-03 14:42:45 +05:30
parent c66236eea8
commit 4bdacf7d81
8 changed files with 291 additions and 46 deletions

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:letter_head_name",
"creation": "2012-11-22 17:45:46",
@ -13,6 +14,9 @@
"is_default",
"letter_head_image_section",
"image",
"image_height",
"image_width",
"align",
"header_section",
"content",
"footer_section",
@ -100,15 +104,34 @@
"fieldname": "footer",
"fieldtype": "HTML Editor",
"label": "Footer HTML"
},
{
"default": "Left",
"fieldname": "align",
"fieldtype": "Select",
"label": "Align",
"options": "Left\nRight\nCenter"
},
{
"fieldname": "image_height",
"fieldtype": "Float",
"label": "Image Height"
},
{
"fieldname": "image_width",
"fieldtype": "Float",
"label": "Image Width"
}
],
"icon": "fa fa-font",
"idx": 1,
"links": [],
"max_attachments": 3,
"modified": "2019-11-11 18:46:43.375120",
"modified": "2021-10-03 14:37:58.314696",
"modified_by": "Administrator",
"module": "Printing",
"name": "Letter Head",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{

View file

@ -2,7 +2,7 @@
# License: MIT. See LICENSE
import frappe
from frappe.utils import is_image
from frappe.utils import is_image, flt
from frappe.model.document import Document
from frappe import _
@ -26,7 +26,15 @@ class LetterHead(Document):
def set_image(self):
if self.source=='Image':
if self.image and is_image(self.image):
self.content = '<img src="{}">'.format(self.image)
self.image_width = flt(self.image_width)
self.image_height = flt(self.image_height)
dimension = 'width' if self.image_width > self.image_height else 'height'
dimension_value = self.get('image_' + dimension)
self.content = f'''
<div style="text-align: {self.align.lower()};">
<img src="{self.image}" alt="{self.name}" {dimension}="{dimension_value}" style="{dimension}: {dimension_value}px;">
</div>
'''
frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True)
else:
frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange')

View file

@ -1,43 +1,172 @@
<template>
<div class="letterhead">
<div class="mb-2 d-flex justify-content-end">
<button
class="btn btn-default btn-xs btn-edit"
@click="toggle_edit_letterhead"
>
{{ !$store.edit_letterhead ? __("Edit") : __("Done") }}
</button>
<button
v-if="type == 'Header'"
class="ml-2 btn btn-default btn-xs btn-change-letterhead"
@click="change_letterhead"
>
{{ __("Change Letter Head") }}
</button>
<div class="mb-4 d-flex justify-content-between">
<div class="d-flex align-items-center">
<div
v-if="letterhead && $store.edit_letterhead"
class="btn-group"
role="group"
aria-label="Align Letterhead"
>
<button
v-for="direction in ['Left', 'Center', 'Right']"
type="button"
class="btn btn-xs"
@click="letterhead.align = direction"
:class="
letterhead.align == direction
? 'btn-secondary'
: 'btn-default'
"
>
{{ direction }}
</button>
</div>
<input
class="ml-4 custom-range"
v-if="letterhead && $store.edit_letterhead"
type="range"
name="image-resize"
min="20"
:max="range_input_field === 'image_width' ? 700 : 500"
:value="letterhead[range_input_field]"
@input="
e =>
(letterhead[range_input_field] = parseFloat(
e.target.value
))
"
/>
</div>
<div>
<button
class="ml-2 btn btn-default btn-xs"
v-if="letterhead && $store.edit_letterhead"
@click="upload_image"
>
{{ __("Change Image") }}
</button>
<button
v-if="letterhead && $store.edit_letterhead"
class="ml-2 btn btn-default btn-xs btn-change-letterhead"
@click="change_letterhead"
>
{{ __("Change Letter Head") }}
</button>
<button
class="btn btn-default btn-xs btn-edit ml-2"
@click="toggle_edit_letterhead"
>
{{ !$store.edit_letterhead ? __("Edit") : __("Done") }}
</button>
</div>
</div>
<div
v-if="letterhead && !$store.edit_letterhead"
v-html="letterhead[field]"
v-html="letterhead.content"
></div>
<div v-show="letterhead && $store.edit_letterhead" ref="editor"></div>
<!-- <div v-show="letterhead && $store.edit_letterhead" ref="editor"></div> -->
<div
class="edit-letterhead"
v-if="letterhead && $store.edit_letterhead"
:style="{
justifyContent: {
Left: 'flex-start',
Center: 'center',
Right: 'flex-end'
}[letterhead.align]
}"
>
<div class="edit-image">
<div v-if="letterhead.image">
<img
:src="letterhead.image"
:style="{
width:
range_input_field === 'image_width'
? letterhead.image_width + 'px'
: null,
height:
range_input_field === 'image_height'
? letterhead.image_height + 'px'
: null
}"
/>
</div>
<button v-else class="btn btn-default" @click="upload_image">
{{ __("Upload Image") }}
</button>
</div>
</div>
</div>
</template>
<script>
import { storeMixin } from "./store";
import { get_image_dimensions } from "./utils";
export default {
name: "LetterHeadEditor",
props: ["type"],
mixins: [storeMixin],
data() {
return {
range_input_field: null,
aspect_ratio: null
};
},
watch: {
letterhead: {
deep: true,
immediate: true,
handler(letterhead) {
if (!letterhead) return;
if (letterhead.image_width && letterhead.image_height) {
let dimension =
letterhead.image_width > letterhead.image_height
? "width"
: "height";
let dimension_value = letterhead["image_" + dimension];
letterhead.content = `
<div style="text-align: ${letterhead.align.toLowerCase()};">
<img
src="${letterhead.image}"
alt="${letterhead.name}"
${dimension}="${dimension_value}"
style="${dimension}: ${dimension_value}px;">
</div>
`;
}
}
}
},
mounted() {
if (!this.letterhead) {
frappe
.call("frappe.client.get_default", { key: "letter_head" })
.then(r => {
if (r.message) {
this.$store.change_letterhead(r.message);
this.set_letterhead(r.message);
}
});
}
this.$watch(
function() {
return this.letterhead
? this.letterhead[this.range_input_field]
: null;
},
function() {
if (this.aspect_ratio === null) return;
let update_field =
this.range_input_field == "image_width"
? "image_height"
: "image_width";
this.letterhead[update_field] =
update_field == "image_width"
? this.aspect_ratio * this.letterhead.image_height
: this.letterhead.image_width / this.aspect_ratio;
}
);
},
methods: {
toggle_edit_letterhead() {
@ -54,9 +183,7 @@ export default {
fieldtype: "Comment",
change: () => {
this.letterhead._dirty = true;
this.letterhead[
this.field
] = this.control.get_value();
this.letterhead.content = this.control.get_value();
}
},
render_input: true,
@ -64,7 +191,7 @@ export default {
no_wrapper: true
});
}
this.control.set_value(this.letterhead[this.field]);
this.control.set_value(this.letterhead.content);
},
change_letterhead() {
let d = new frappe.ui.Dialog({
@ -79,25 +206,73 @@ export default {
],
primary_action: ({ letterhead }) => {
if (letterhead) {
this.$store.change_letterhead(letterhead);
this.set_letterhead(letterhead);
}
d.hide();
}
});
d.show();
}
},
computed: {
field() {
return {
Header: "content",
Footer: "footer"
}[this.type];
},
upload_image() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: file_doc => {
get_image_dimensions(file_doc.file_url).then(
({ width, height }) => {
this.$set(
this.letterhead,
"image",
file_doc.file_url
);
let new_width = width;
let new_height = height;
this.aspect_ratio = width / height;
this.range_input_field =
this.aspect_ratio > 1
? "image_width"
: "image_height";
if (width > 200) {
new_width = 200;
new_height = new_width / aspect_ratio;
}
if (height > 80) {
new_height = 80;
new_width = aspect_ratio * new_height;
}
this.$set(
this.letterhead,
"image_height",
new_height
);
this.$set(
this.letterhead,
"image_width",
new_width
);
}
);
}
});
},
set_letterhead(letterhead) {
this.$store.change_letterhead(letterhead).then(() => {
get_image_dimensions(this.letterhead.image).then(
({ width, height }) => {
this.aspect_ratio = width / height;
this.range_input_field =
this.aspect_ratio > 1
? "image_width"
: "image_height";
}
);
});
}
}
};
</script>
<style>
<style scoped>
.letterhead {
position: relative;
border: 1px solid var(--dark-border-color);
@ -105,4 +280,23 @@ export default {
padding: 1rem;
margin-bottom: 1rem;
}
.edit-letterhead {
display: flex;
align-items: center;
}
.edit-image {
min-width: 40px;
min-height: 40px;
border: 1px solid var(--border-color);
}
.edit-image img {
height: 100%;
}
.edit-title {
margin-left: 1rem;
border: 1px solid transparent;
border-radius: var(--border-radius);
font-size: var(--text-md);
font-weight: 600;
}
</style>

View file

@ -32,7 +32,12 @@
@change="$set(layout, 'footer', $event)"
:button-label="__('Edit Footer')"
/>
<LetterHeadEditor type="Footer" />
<HTMLEditor
v-if="letterhead"
:value="letterhead.footer"
@change="update_letterhead_footer"
:button-label="__('Edit LetterHead Footer')"
/>
</div>
</template>
@ -85,6 +90,10 @@ export default {
sections.push(_section);
}
this.$set(this.layout, "sections", sections);
},
update_letterhead_footer(val) {
this.letterhead.footer = val;
this.letterhead._dirty = true;
}
}
};

View file

@ -115,9 +115,11 @@ export function getStore(print_format_name) {
})
.then(() => {
if (this.letterhead && this.letterhead._dirty) {
return frappe.call("frappe.client.save", {
doc: this.letterhead
});
return frappe
.call("frappe.client.save", {
doc: this.letterhead
})
.then(r => (this.letterhead = r.message));
}
})
.then(() => this.fetch())
@ -125,7 +127,6 @@ export function getStore(print_format_name) {
},
reset_changes() {
this.fetch();
},
get_layout() {
if (this.print_format) {
@ -143,7 +144,7 @@ export function getStore(print_format_name) {
return create_default_layout(this.meta);
},
change_letterhead(letterhead) {
frappe.db.get_doc("Letter Head", letterhead).then(doc => {
return frappe.db.get_doc("Letter Head", letterhead).then(doc => {
this.letterhead = doc;
});
}

View file

@ -94,8 +94,8 @@ export function get_table_columns(df) {
typeof tf.width == "number" && tf.width < 100
? tf.width
: tf.width
? 20
: 10;
? 20
: 10;
table_columns.push({
label: tf.label,
fieldname: tf.fieldname,
@ -118,3 +118,13 @@ export function pluck(object, keys) {
}
return out;
}
export function get_image_dimensions(src) {
return new Promise(resolve => {
let img = new Image();
img.onload = function() {
resolve({ width: this.width, height: this.height });
};
img.src = src;
});
}

View file

@ -13,7 +13,7 @@
</style>
</head>
<body>
{{ header }}
{{ header or '' }}
{% for section in layout.sections %}
<div class="section {{ resolve_class({'page-break': section.page_break}) }}">
{% if section.label %}
@ -31,6 +31,6 @@
</div>
</div>
{% endfor %}
{{ footer }}
{{ footer or '' }}
</body>
</html>

View file

@ -6,7 +6,7 @@
position: fixed;
top: 0;
left: 0;
width: 100%;
width: {{ body_width | int }}mm;
padding-top: {{ print_format.margin_top | int }}mm;
padding-left: {{ print_format.margin_left | int }}mm;
padding-right: {{ print_format.margin_right | int }}mm;