summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Baptiste <kba@odoo.com>2019-05-29 12:57:35 +0000
committerYannick Tivisse <yti@odoo.com>2019-06-26 06:56:17 +0000
commit83aaea9e5ce92530875e4fca606a89fe423803ef (patch)
tree91dd20e5ea2087d4ddf1daa3d1d954de3490517f
parent2f34aea5289c020a240b33d2c9767baca806d59b (diff)
[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>
-rw-r--r--addons/hr/models/hr_employee.py13
-rw-r--r--addons/hr/views/hr_employee_public_views.xml6
-rw-r--r--addons/hr/views/hr_employee_views.xml6
-rw-r--r--addons/hr/views/hr_templates.xml1
-rw-r--r--addons/hr/views/res_users.xml2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js87
-rw-r--r--addons/web/static/src/js/libs/zoomodoo.js (renamed from addons/website/static/src/js/content/zoomodoo.js)55
-rw-r--r--addons/web/static/src/scss/web.zoomodoo.scss (renamed from addons/website/static/src/scss/website.zoomodoo.scss)0
-rw-r--r--addons/web/views/webclient_templates.xml3
-rw-r--r--addons/website/static/src/js/content/website_root.js2
-rw-r--r--addons/website/views/website_templates.xml2
-rw-r--r--addons/website_sale/static/src/js/website_sale.js2
-rw-r--r--odoo/addons/base/models/res_partner.py3
-rw-r--r--odoo/addons/base/views/res_users_views.xml2
14 files changed, 149 insertions, 35 deletions
diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py
index 8746e3ecc06..a197d9dc7a4 100644
--- a/addons/hr/models/hr_employee.py
+++ b/addons/hr/models/hr_employee.py
@@ -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')
diff --git a/addons/hr/views/hr_employee_public_views.xml b/addons/hr/views/hr_employee_public_views.xml
index a97c63c3feb..d6a97209f11 100644
--- a/addons/hr/views/hr_employee_public_views.xml
+++ b/addons/hr/views/hr_employee_public_views.xml
@@ -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">
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml
index bb1dcde2d77..97a05228ea2 100644
--- a/addons/hr/views/hr_employee_views.xml
+++ b/addons/hr/views/hr_employee_views.xml
@@ -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">
diff --git a/addons/hr/views/hr_templates.xml b/addons/hr/views/hr_templates.xml
index 465c98d39e0..99a33ef3185 100644
--- a/addons/hr/views/hr_templates.xml
+++ b/addons/hr/views/hr_templates.xml
@@ -7,5 +7,4 @@
<script type="text/javascript" src="/hr/static/src/js/chat.js"></script>
</xpath>
</template>
-
</odoo>
diff --git a/addons/hr/views/res_users.xml b/addons/hr/views/res_users.xml
index 7ed9b1001ad..d417b93341b 100644
--- a/addons/hr/views/res_users.xml
+++ b/addons/hr/views/res_users.xml
@@ -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>
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 14f03a83222..9b78d1e723b 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -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,
+ });
+ }
+ }
},
});
diff --git a/addons/website/static/src/js/content/zoomodoo.js b/addons/web/static/src/js/libs/zoomodoo.js
index 1e85e46062f..cbe0511102a 100644
--- a/addons/website/static/src/js/content/zoomodoo.js
+++ b/addons/web/static/src/js/libs/zoomodoo.js
@@ -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;
diff --git a/addons/website/static/src/scss/website.zoomodoo.scss b/addons/web/static/src/scss/web.zoomodoo.scss
index 3d855c96a96..3d855c96a96 100644
--- a/addons/website/static/src/scss/website.zoomodoo.scss
+++ b/addons/web/static/src/scss/web.zoomodoo.scss
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index e67005f0eaf..6494eda9622 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -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">
diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js
index 612265124cd..4b61a33d779 100644
--- a/addons/website/static/src/js/content/website_root.js
+++ b/addons/website/static/src/js/content/website_root.js
@@ -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;
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 4dd19528c0d..aa264dd76a7 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -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"/>
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index c4888233e93..85ef87d0e08 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.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;
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index e14174d65c8..87822b05279 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -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. "\
diff --git a/odoo/addons/base/views/res_users_views.xml b/odoo/addons/base/views/res_users_views.xml
index a975f5a33cd..8ea5272348d 100644
--- a/odoo/addons/base/views/res_users_views.xml
+++ b/odoo/addons/base/views/res_users_views.xml
@@ -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>