[IMP] base: Add option on image widget to enable zoom
Purpose ======= As the employee picture on a form view is often too small, it should be possible to zoom on it like for products in eCommerce. Specification ============= Added new bool options on FieldBinaryImage: `zoom` and `background`. `zoom` will call zoomOdoo.js and display a popup with the zoomed out image. `background` is used to either display the image as a div element with a background-url (to be used e.g. in a kanban view) or as a standard img element. closes odoo/odoo#33839 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
committed by
Yannick Tivisse
parent
2f34aea528
commit
83aaea9e5c
@@ -92,18 +92,11 @@ class HrEmployeePrivate(models.Model):
|
||||
|
||||
# image: all image fields are base64 encoded and PIL-supported
|
||||
image = fields.Binary(
|
||||
"Photo", default=_default_image,
|
||||
help="This field holds the image used as photo for the employee, limited to 1024x1024px.")
|
||||
"Photo", default=_default_image)
|
||||
image_medium = fields.Binary(
|
||||
"Medium-sized photo",
|
||||
help="Medium-sized photo of the employee. It is automatically "
|
||||
"resized as a 128x128px image, with aspect ratio preserved. "
|
||||
"Use this field in form views or some kanban views.")
|
||||
"Medium-sized photo")
|
||||
image_small = fields.Binary(
|
||||
"Small-sized photo",
|
||||
help="Small-sized photo of the employee. It is automatically "
|
||||
"resized as a 64x64px image, with aspect ratio preserved. "
|
||||
"Use this field anywhere a small image is required.")
|
||||
"Small-sized photo")
|
||||
phone = fields.Char(related='address_home_id.phone', related_sudo=False, string="Private Phone", groups="hr.group_hr_user")
|
||||
# employee in company
|
||||
parent_id = fields.Many2one('hr.employee', 'Manager')
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<!-- Used by other modules-->
|
||||
</div>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image":"image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
@@ -98,9 +98,9 @@
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
|
||||
<!-- Medium employee's picture -->
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image: url(#{kanban_image('hr.employee.public', 'image_medium', record.id.raw_value)})" role="img"/>
|
||||
<field name="image" widget="image" class="o_kanban_image_fill_left d-none d-md-block" options="{'zoom': true, 'background': true, 'preventClicks': false}"/>
|
||||
<!-- Employee's picture optimized for mobile -->
|
||||
<div class="o_kanban_image d-md-none rounded-circle" t-attf-style="background-image: url(#{kanban_image('hr.employee.public', 'image_small', record.id.raw_value)})" role="img"/>
|
||||
<field name="image_small" widget="image" class="o_kanban_image d-md-none rounded-circle" options="{'zoom': true, 'background': true, 'preventClicks': false}"/>
|
||||
|
||||
<div class="oe_kanban_details">
|
||||
<div class="o_kanban_record_top">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<!-- Used by other modules-->
|
||||
</div>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image":"image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
@@ -204,9 +204,9 @@
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
|
||||
<!-- Medium employee's picture -->
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image: url(#{kanban_image('hr.employee', 'image_medium', record.id.raw_value)})" role="img"/>
|
||||
<field name="image" widget="image" class="o_kanban_image_fill_left d-none d-md-block" options="{'zoom': true, 'background': true, 'preventClicks': false}"/>
|
||||
<!-- Employee's picture optimized for mobile -->
|
||||
<div class="o_kanban_image d-md-none rounded-circle" t-attf-style="background-image: url(#{kanban_image('hr.employee', 'image_small', record.id.raw_value)})" role="img"/>
|
||||
<field name="image_small" widget="image" class="o_kanban_image d-md-none rounded-circle" options="{'zoom': true, 'background': true, 'preventClicks': false}"/>
|
||||
|
||||
<div class="oe_kanban_details">
|
||||
<div class="o_kanban_record_top">
|
||||
|
||||
@@ -7,5 +7,4 @@
|
||||
<script type="text/javascript" src="/hr/static/src/js/chat.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image":"image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
|
||||
@@ -22,6 +22,8 @@ var utils = require('web.utils');
|
||||
var view_dialogs = require('web.view_dialogs');
|
||||
var field_utils = require('web.field_utils');
|
||||
|
||||
require("web.zoomodoo");
|
||||
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
@@ -1571,6 +1573,25 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
|
||||
'i': 'png',
|
||||
'P': 'svg+xml',
|
||||
},
|
||||
/**
|
||||
* Returns the image URL from a model.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model model from which to retrieve the image
|
||||
* @param {string} res_id id of the record
|
||||
* @param {string} field name of the image field
|
||||
* @param {string} unique an unique integer for the record, usually __last_update
|
||||
* @returns {string} URL of the image
|
||||
*/
|
||||
_getImageUrl: function (model, res_id, field, unique) {
|
||||
return session.url('/web/image', {
|
||||
model: model,
|
||||
id: JSON.stringify(res_id),
|
||||
field: field,
|
||||
// unique forces a reload of the image when the record has been updated
|
||||
unique: field_utils.format.datetime(unique).replace(/[^0-9]/g, ''),
|
||||
});
|
||||
},
|
||||
_render: function () {
|
||||
var self = this;
|
||||
var url = this.placeholder;
|
||||
@@ -1579,13 +1600,9 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
|
||||
// Use magic-word technique for detecting image type
|
||||
url = 'data:image/' + (this.file_type_magic_word[this.value[0]] || 'png') + ';base64,' + this.value;
|
||||
} else {
|
||||
url = session.url('/web/image', {
|
||||
model: this.model,
|
||||
id: JSON.stringify(this.res_id),
|
||||
field: this.nodeOptions.preview_image || this.name,
|
||||
// unique forces a reload of the image when the record has been updated
|
||||
unique: field_utils.format.datetime(this.recordData.__last_update).replace(/[^0-9]/g, ''),
|
||||
});
|
||||
var field = this.nodeOptions.preview_image || this.name;
|
||||
var unique = this.recordData.__last_update;
|
||||
url = this._getImageUrl(this.model, this.res_id, field, unique);
|
||||
}
|
||||
}
|
||||
var $img = $(qweb.render("FieldBinaryImage-img", {widget: this, url: url}));
|
||||
@@ -1608,6 +1625,62 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
|
||||
$img.attr('src', self.placeholder);
|
||||
self.do_warn(_t("Image"), _t("Could not display the selected image."));
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Only enable the zoom on image in read-only mode, and if the option is enabled.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderReadonly: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if(this.nodeOptions.zoom) {
|
||||
var unique = this.recordData.__last_update;
|
||||
var url = this._getImageUrl(this.model, this.res_id, 'image', unique);
|
||||
var $img;
|
||||
|
||||
if(this.nodeOptions.background)
|
||||
{
|
||||
if('tag' in this.nodeOptions) {
|
||||
this.tagName = this.nodeOptions.tag;
|
||||
}
|
||||
|
||||
if('class' in this.attrs) {
|
||||
this.$el.addClass(this.attrs.class);
|
||||
}
|
||||
|
||||
var urlThumb = this._getImageUrl(this.model, this.res_id, 'image_medium', unique);
|
||||
|
||||
this.$el.empty();
|
||||
$img = this.$el;
|
||||
$img.css('backgroundImage', 'url(' + urlThumb + ')');
|
||||
} else {
|
||||
$img = this.$('img');
|
||||
}
|
||||
|
||||
if(this.recordData.image) {
|
||||
$img.attr('data-zoom', 1);
|
||||
$img.attr('data-zoom-image', url);
|
||||
|
||||
$img.zoomOdoo({
|
||||
event: 'mouseenter',
|
||||
attach: '.o_content',
|
||||
attachToTarget: true,
|
||||
onShow: function () {
|
||||
if(this.$zoom.height() < 256 && this.$zoom.width() < 256) {
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
beforeAttach: function () {
|
||||
this.$flyout.css({ width: '512px', height: '512px' });
|
||||
},
|
||||
preventClicks: this.nodeOptions.preventClicks,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+52
-3
@@ -1,4 +1,4 @@
|
||||
odoo.define('website.content.zoomodoo', function (require) {
|
||||
odoo.define('web.zoomodoo', function (require) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,10 @@ var defaults = {
|
||||
onHide: $.noop,
|
||||
|
||||
// Callback function to execute when the cursor is moved while over the image.
|
||||
onMove: $.noop
|
||||
onMove: $.noop,
|
||||
|
||||
// Callback function to execute when the flyout is attached to the target.
|
||||
beforeAttach: $.noop
|
||||
|
||||
};
|
||||
|
||||
@@ -87,6 +90,9 @@ ZoomOdoo.prototype._init = function () {
|
||||
|
||||
if (this.opts.preventClicks) {
|
||||
this.$target.on('click.zoomodoo', function (e) { e.preventDefault(); });
|
||||
} else {
|
||||
var self = this;
|
||||
this.$target.on('click.zoomodoo', function () { self.hide(); self.$target.unbind(); });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -114,8 +120,51 @@ ZoomOdoo.prototype.show = function (e, testMouseOver) {
|
||||
if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) {
|
||||
$attach = this.$target.parents(this.opts.attach);
|
||||
}
|
||||
|
||||
// Prevents having multiple zoom flyouts
|
||||
$attach.parent().find('.zoomodoo-flyout').remove();
|
||||
this.$flyout.removeAttr('style');
|
||||
$attach.parent().append(this.$flyout);
|
||||
|
||||
if (this.opts.attachToTarget) {
|
||||
this.opts.beforeAttach.call(this);
|
||||
|
||||
if(this.$zoom.height() < this.$flyout.height()) {
|
||||
this.$flyout.css('height', this.$zoom.height() + 'px');
|
||||
}
|
||||
if(this.$zoom.width() < this.$flyout.width()) {
|
||||
this.$flyout.css('width', this.$zoom.width() + 'px');
|
||||
}
|
||||
|
||||
var offset = this.$target.offset();
|
||||
var left = offset.left - this.$flyout.width();
|
||||
var top = offset.top;
|
||||
|
||||
// Position the zoom on the right side of the target
|
||||
// if there's not enough room on the left
|
||||
if(left < 0) {
|
||||
if(offset.left < ($(document).width() / 2)) {
|
||||
left = offset.left + this.$target.width();
|
||||
} else {
|
||||
left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevents the flyout to overflow
|
||||
if(left + this.$flyout.width() > $(document).width()) {
|
||||
this.$flyout.css('width', $(document).width() - left + 'px');
|
||||
} else if(left === 0) { // Limit the max width if displayed on the left
|
||||
this.$flyout.css('width', offset.left + 'px');
|
||||
}
|
||||
|
||||
// Prevents the zoom to be displayed outside the current viewport
|
||||
if((top + this.$flyout.height()) > $(document).height()) {
|
||||
top = $(document).height() - this.$flyout.height();
|
||||
}
|
||||
|
||||
this.$flyout.css('transform', 'translate3d(' + left + 'px, ' + top + 'px, 0px)');
|
||||
}
|
||||
|
||||
w1 = this.$target.width();
|
||||
h1 = this.$target.height();
|
||||
|
||||
@@ -235,7 +284,7 @@ ZoomOdoo.prototype._move = function (e) {
|
||||
var xl = Math.ceil(pl * rw);
|
||||
|
||||
// Close if outside
|
||||
if (xl < 0 || xt < 0 || xl > dw || xt > dh) {
|
||||
if (xl < 0 || xt < 0 || xl > dw || xt > dh || lx > (offset.left + this.$target.width())) {
|
||||
this.hide();
|
||||
} else {
|
||||
var top = xt * -1;
|
||||
@@ -76,6 +76,7 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/colorpicker.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/keyboard.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/name_and_signature.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/web.zoomodoo.scss"/>
|
||||
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/scss/fontawesome_overriden.scss"/>
|
||||
|
||||
@@ -159,6 +160,8 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/name_and_signature.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/notification.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/rainbow_man.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/libs/zoomodoo.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="assets_common_lazy" inherit_id="web.assets_common" primary="True">
|
||||
|
||||
@@ -4,7 +4,7 @@ odoo.define('website.root', function (require) {
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var publicRootData = require('web.public.root');
|
||||
require('website.content.zoomodoo');
|
||||
require("web.zoomodoo");
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.ui.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.zoomodoo.scss"/>
|
||||
|
||||
<!-- Custom empty file for user custom rules -->
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/user_custom_rules.scss"/>
|
||||
@@ -71,7 +70,6 @@
|
||||
<script type="text/javascript" src="/website/static/src/js/content/lazy_template_call.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/js/content/menu.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/js/content/snippets.animation.js"/>
|
||||
<script type="text/javascript" src="/website/static/src/js/content/zoomodoo.js"/>
|
||||
|
||||
<script type="text/javascript" src="/website/static/src/js/menu/navbar.js"/>
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ var config = require('web.config');
|
||||
var concurrency = require('web.concurrency');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var VariantMixin = require('sale.VariantMixin');
|
||||
require("website.content.zoomodoo");
|
||||
require("web.zoomodoo");
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
|
||||
@@ -225,8 +225,7 @@ class Partner(models.Model):
|
||||
company_name = fields.Char('Company Name')
|
||||
|
||||
# image: all image fields are base64 encoded and PIL-supported
|
||||
image = fields.Binary("Image",
|
||||
help="This field holds the image used as avatar for this contact, limited to 1024x1024px",)
|
||||
image = fields.Binary("Image")
|
||||
image_medium = fields.Binary("Medium-sized image",
|
||||
help="Medium-sized image of this contact. It is automatically "\
|
||||
"resized as a 128x128px image, with aspect ratio preserved. "\
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
<div class="alert alert-info text-center mb-3" attrs="{'invisible': [('id', '>', 0)]}" role="alert">
|
||||
You are inviting a new user.
|
||||
</div>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"preview_image": "image_medium"}'/>
|
||||
<field name="image" widget='image' class="oe_avatar" options='{"zoom": true, "preview_image": "image_medium"}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" required="1"/></h1>
|
||||
|
||||
Reference in New Issue
Block a user