[IMP] web, *: Add lazy loading for images in kanban
This commit adds the loading="lazy" attribute to every img tags compiled by the kanban compiler and modifies some kanban archs in order to use img tags instead of divs with background-image style to load images (this allows again the loading="lazy" attribute to work). The purpose is to avoid loading images that are outside the user viewport at all time in order to reduce the load on the network when new images are loaded. Steps to test: find all views that are using the o_kanban_image or o_kanban_image_fill_left class and ensure all the images in these views appear in the network tab of devtools when they enter the user viewport. Example: res.partners, hr.employee, ... task-3102207 closes odoo/odoo#117049 Related: odoo/enterprise#39560 Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
@@ -273,7 +273,9 @@
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
|
||||
<div class="o_kanban_image" t-attf-style="background-image:url('#{kanban_image('fleet.vehicle', 'image_128', record.id.raw_value)}')"/>
|
||||
<div class="o_kanban_image">
|
||||
<img t-attf-src="#{kanban_image('fleet.vehicle', 'image_128', record.id.raw_value)}" t-att-alt="record.id.value"/>
|
||||
</div>
|
||||
<div class="oe_kanban_details">
|
||||
<strong class="o_kanban_record_title">
|
||||
<t t-if="record.license_plate.raw_value"><field name="license_plate"/>:</t> <field name="model_id"/>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
div.o_field_widget.o_field_background_image {
|
||||
display: inline-block;
|
||||
|
||||
> div {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="hr.BackgroundImage" owl="1">
|
||||
<div
|
||||
<img
|
||||
loading="lazy"
|
||||
t-att-data-tooltip-template="hasTooltip and tooltipAttributes.template"
|
||||
t-att-data-tooltip-info="hasTooltip and tooltipAttributes.info"
|
||||
t-att-data-tooltip-delay="hasTooltip and props.zoomDelay"
|
||||
t-attf-style="background-image: url('#{getUrl(props.previewImage or props.name)}');"/>
|
||||
t-attf-src="#{getUrl(props.previewImage or props.name)}"
|
||||
alt="Binary file"
|
||||
/>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -163,8 +163,9 @@
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block"
|
||||
t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_128', record.id.raw_value)}')"/>
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block">
|
||||
<img t-attf-src="#{kanban_image('lunch.product', 'image_128', record.id.raw_value)}" t-att-alt="record.id.value"/>
|
||||
</div>
|
||||
<div class="oe_kanban_details">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
@@ -254,8 +255,9 @@
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_kanban_record">
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block"
|
||||
t-attf-style="background-image:url('#{kanban_image('lunch.product.category', 'image_128', record.id.raw_value)}')"/>
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block">
|
||||
<img t-attf-src="#{kanban_image('lunch.product.category', 'image_128', record.id.raw_value)}" t-att-alt="record.id.value"/>
|
||||
</div>
|
||||
<div class="oe_kanban_details">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
</t>
|
||||
</div>
|
||||
<img
|
||||
loading="lazy"
|
||||
class="img img-fluid w-100"
|
||||
alt="Binary file"
|
||||
t-att-src="this.getUrl(props.previewImage or props.name)"
|
||||
|
||||
@@ -26,7 +26,8 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
this.ctx.readonly = "read_only_mode";
|
||||
this.compilers.push(
|
||||
{ selector: ".oe_kanban_colorpicker", fn: this.compileColorPicker },
|
||||
{ selector: "t[t-call]", fn: this.compileTCall }
|
||||
{ selector: "t[t-call]", fn: this.compileTCall },
|
||||
{ selector: "img", fn: this.compileImage }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +96,14 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Element}
|
||||
*/
|
||||
compileImage(el) {
|
||||
const element = el.cloneNode(true);
|
||||
element.setAttribute("loading", "lazy");
|
||||
return element;
|
||||
}
|
||||
/**
|
||||
* @returns {Element}
|
||||
*/
|
||||
|
||||
@@ -183,9 +183,21 @@
|
||||
position: center;
|
||||
repeat: no-repeat;
|
||||
}
|
||||
> img:not(.o_kanban_image_inner_pic) {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.o_kanban_image_full {
|
||||
background-size: contain;
|
||||
> img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +206,7 @@
|
||||
// Eg. In partners list we use to fill user picture only, keeping the
|
||||
// default design for company logos.
|
||||
.o_kanban_image {
|
||||
position: relative;
|
||||
margin-right: var(--KanbanRecord-padding-h);
|
||||
flex: 0 0 var(--KanbanRecord__image-width);
|
||||
min-height: var(--KanbanRecord__image-width);
|
||||
@@ -203,6 +216,15 @@
|
||||
repeat: no-repeat;
|
||||
position: center;
|
||||
}
|
||||
> img:not(.o_kanban_image_inner_pic) {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex-basis: var(--KanbanRecord__image--fill-width);
|
||||
|
||||
@@ -9538,6 +9538,11 @@ QUnit.module("Views", (hooks) => {
|
||||
'img[data-src*="/web/image"][data-src*="&id=1"]'
|
||||
);
|
||||
assert.strictEqual(imageOnRecord.length, 1, "partner with image display image by url");
|
||||
assert.strictEqual(
|
||||
imageOnRecord[0].loading,
|
||||
"lazy",
|
||||
"img in kanban view should be lazy loaded"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("test displaying image (write_date field)", async (assert) => {
|
||||
|
||||
@@ -235,14 +235,14 @@
|
||||
<t t-set="placeholder" t-value="'/website_slides/static/src/img/channel-training-default.jpg'"/>
|
||||
<t t-if="record.image_128.raw_value" t-set="image" t-value="kanban_image('slide.slide', 'image_128', record.id.raw_value, placeholder)"/>
|
||||
<t t-else="" t-set="image" t-value="'/website_slides/static/src/img/channel-training-default.jpg'"/>
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block"
|
||||
t-attf-style="background-image:url('#{image}')">
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block">
|
||||
<img t-attf-src="#{image}" t-att-alt="record.id.value"/>
|
||||
<img class="o_kanban_image_inner_pic"
|
||||
t-att-alt="record.channel_id.value"
|
||||
t-att-src="kanban_image('slide.channel', 'image_128', record.channel_id.raw_value)"/>
|
||||
</div>
|
||||
<div class="o_kanban_image rounded-circle d-md-none"
|
||||
t-attf-style="background-image:url('#{image}')">
|
||||
<div class="o_kanban_image d-md-none">
|
||||
<img class="rounded-circle" t-attf-src="#{image}" t-att-alt="record.id.value"/>
|
||||
<img class="o_kanban_image_inner_pic"
|
||||
t-att-alt="record.channel_id.value"
|
||||
t-att-src="kanban_image('slide.channel', 'image_128', record.channel_id.raw_value)"/>
|
||||
|
||||
@@ -481,16 +481,23 @@
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban">
|
||||
<t t-if="!record.is_company.raw_value">
|
||||
<t t-set="background_image" t-value="kanban_image('res.partner', 'avatar_128', record.id.raw_value)"/>
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image:url('#{background_image}')">
|
||||
<t t-set="avatar_image" t-value="kanban_image('res.partner', 'avatar_128', record.id.raw_value)"/>
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block">
|
||||
<img t-attf-src="#{avatar_image}" t-att-alt="record.id.value"/>
|
||||
<img class="o_kanban_image_inner_pic" t-if="record.parent_id.raw_value" t-att-alt="record.parent_id.value" t-att-src="kanban_image('res.partner', 'avatar_128', record.parent_id.raw_value)"/>
|
||||
</div>
|
||||
<div class="o_kanban_image d-md-none" t-attf-style="background-image: url(#{background_image})">
|
||||
<div class="o_kanban_image d-md-none d-block">
|
||||
<img t-attf-src="#{avatar_image}" t-att-alt="record.id.value"/>
|
||||
<img class="o_kanban_image_inner_pic" t-if="record.parent_id.raw_value" t-att-alt="record.parent_id.value" t-att-src="kanban_image('res.partner', 'avatar_128', record.parent_id.raw_value)"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_kanban_image_fill_left o_kanban_image_full" t-attf-style="background-image: url(#{kanban_image('res.partner', 'avatar_128', record.id.raw_value)})" role="img"/>
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block o_kanban_image_full">
|
||||
<img t-attf-src="#{kanban_image('res.partner', 'avatar_128', record.id.raw_value)}" role="img" t-att-alt="record.id.value"/>
|
||||
</div>
|
||||
<div class="o_kanban_image d-md-none d-block o_kanban_image_full">
|
||||
<img t-attf-src="#{kanban_image('res.partner', 'avatar_128', record.id.raw_value)}" role="img" t-att-alt="record.id.value"/>
|
||||
</div>
|
||||
</t>
|
||||
<div class="ribbon ribbon-top-right" attrs="{'invisible': [('active', '=', True)]}">
|
||||
<span class="bg-danger">Archived</span>
|
||||
|
||||
Reference in New Issue
Block a user