feat: Better Letterhead editing
- Image resize and align
This commit is contained in:
parent
c66236eea8
commit
4bdacf7d81
8 changed files with 291 additions and 46 deletions
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue