diff options
| -rw-r--r-- | addons/hr/models/hr_employee.py | 13 | ||||
| -rw-r--r-- | addons/hr/views/hr_employee_public_views.xml | 6 | ||||
| -rw-r--r-- | addons/hr/views/hr_employee_views.xml | 6 | ||||
| -rw-r--r-- | addons/hr/views/hr_templates.xml | 1 | ||||
| -rw-r--r-- | addons/hr/views/res_users.xml | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 87 | ||||
| -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.xml | 3 | ||||
| -rw-r--r-- | addons/website/static/src/js/content/website_root.js | 2 | ||||
| -rw-r--r-- | addons/website/views/website_templates.xml | 2 | ||||
| -rw-r--r-- | addons/website_sale/static/src/js/website_sale.js | 2 | ||||
| -rw-r--r-- | odoo/addons/base/models/res_partner.py | 3 | ||||
| -rw-r--r-- | odoo/addons/base/views/res_users_views.xml | 2 |
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> |
