From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- .../web/static/src/js/widgets/attach_document.js | 139 +++ .../web/static/src/js/widgets/change_password.js | 75 ++ addons/web/static/src/js/widgets/colorpicker.js | 699 +++++++++++++++ addons/web/static/src/js/widgets/data_export.js | 688 ++++++++++++++ addons/web/static/src/js/widgets/date_picker.js | 358 ++++++++ .../web/static/src/js/widgets/domain_selector.js | 987 +++++++++++++++++++++ .../src/js/widgets/domain_selector_dialog.js | 54 ++ addons/web/static/src/js/widgets/iframe_widget.js | 65 ++ .../static/src/js/widgets/model_field_selector.js | 615 +++++++++++++ .../static/src/js/widgets/name_and_signature.js | 662 ++++++++++++++ addons/web/static/src/js/widgets/notification.js | 176 ++++ addons/web/static/src/js/widgets/pie_chart.js | 102 +++ addons/web/static/src/js/widgets/rainbow_man.js | 71 ++ addons/web/static/src/js/widgets/ribbon.js | 48 + addons/web/static/src/js/widgets/signature.js | 97 ++ .../static/src/js/widgets/switch_company_menu.js | 127 +++ .../static/src/js/widgets/translation_dialog.js | 183 ++++ 17 files changed, 5146 insertions(+) create mode 100644 addons/web/static/src/js/widgets/attach_document.js create mode 100644 addons/web/static/src/js/widgets/change_password.js create mode 100644 addons/web/static/src/js/widgets/colorpicker.js create mode 100644 addons/web/static/src/js/widgets/data_export.js create mode 100644 addons/web/static/src/js/widgets/date_picker.js create mode 100644 addons/web/static/src/js/widgets/domain_selector.js create mode 100644 addons/web/static/src/js/widgets/domain_selector_dialog.js create mode 100644 addons/web/static/src/js/widgets/iframe_widget.js create mode 100644 addons/web/static/src/js/widgets/model_field_selector.js create mode 100644 addons/web/static/src/js/widgets/name_and_signature.js create mode 100644 addons/web/static/src/js/widgets/notification.js create mode 100644 addons/web/static/src/js/widgets/pie_chart.js create mode 100644 addons/web/static/src/js/widgets/rainbow_man.js create mode 100644 addons/web/static/src/js/widgets/ribbon.js create mode 100644 addons/web/static/src/js/widgets/signature.js create mode 100644 addons/web/static/src/js/widgets/switch_company_menu.js create mode 100644 addons/web/static/src/js/widgets/translation_dialog.js (limited to 'addons/web/static/src/js/widgets') diff --git a/addons/web/static/src/js/widgets/attach_document.js b/addons/web/static/src/js/widgets/attach_document.js new file mode 100644 index 00000000..8ad48882 --- /dev/null +++ b/addons/web/static/src/js/widgets/attach_document.js @@ -0,0 +1,139 @@ +odoo.define('web.AttachDocument', function (require) { +"use static"; + +var core = require('web.core'); +var framework = require('web.framework'); +var widgetRegistry = require('web.widget_registry'); +var Widget = require('web.Widget'); + +var _t = core._t; + +var AttachDocument = Widget.extend({ + template: 'AttachDocument', + events: { + 'click': '_onClickAttachDocument', + 'change input.o_input_file': '_onFileChanged', + }, + /** + * @constructor + * @param {Widget} parent + * @param {Object} record + * @param {Object} nodeInfo + */ + init: function (parent, record, nodeInfo) { + this._super.apply(this, arguments); + this.res_id = record.res_id; + this.res_model = record.model; + this.state = record; + this.node = nodeInfo; + this.fileuploadID = _.uniqueId('o_fileupload'); + }, + /** + * @override + */ + start: function () { + $(window).on(this.fileuploadID, this._onFileLoaded.bind(this)); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + $(window).off(this.fileuploadID); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // private + //-------------------------------------------------------------------------- + + /** + * Helper function to display a warning that some fields have an invalid + * value. This is used when a save operation cannot be completed. + * + * @private + * @param {string[]} invalidFields - list of field names + */ + _notifyInvalidFields: function (invalidFields) { + var fields = this.state.fields; + var warnings = invalidFields.map(function (fieldName) { + var fieldStr = fields[fieldName].string; + return _.str.sprintf('
  • %s
  • ', _.escape(fieldStr)); + }); + warnings.unshift(''); + this.do_warn(_t("Invalid fields:"), warnings.join('')); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Opens File Explorer dialog if all fields are valid and record is saved + * + * @private + * @param {Event} ev + */ + _onClickAttachDocument: function (ev) { + if ($(ev.target).is('input.o_input_file')) { + return; + } + var fieldNames = this.getParent().canBeSaved(this.state.id); + if (fieldNames.length) { + return this._notifyInvalidFields(fieldNames); + } + // We want to save record on widget click and then open File Selection Explorer + // but due to this security restriction give warning to save record first. + // https://stackoverflow.com/questions/29728705/trigger-click-on-input-file-on-asynchronous-ajax-done/29873845#29873845 + if (!this.res_id) { + return this.do_warn(false, _t('Please save before attaching a file')); + } + this.$('input.o_input_file').trigger('click'); + }, + /** + * Submits file + * + * @private + * @param {Event} ev + */ + _onFileChanged: function (ev) { + ev.stopPropagation(); + this.$('form.o_form_binary_form').trigger('submit'); + framework.blockUI(); + }, + /** + * Call action given as node attribute after file submission + * + * @private + */ + _onFileLoaded: function () { + var self = this; + // the first argument isn't a file but the jQuery.Event + var files = Array.prototype.slice.call(arguments, 1); + return new Promise(function (resolve) { + if (self.node.attrs.action) { + self._rpc({ + model: self.res_model, + method: self.node.attrs.action, + args: [self.res_id], + kwargs: { + attachment_ids: _.map(files, function (file) { + return file.id; + }), + } + }).then(function () { + resolve(); + }); + } else { + resolve(); + } + }).then(function () { + self.trigger_up('reload'); + framework.unblockUI(); + }); + }, + +}); +widgetRegistry.add('attach_document', AttachDocument); +}); diff --git a/addons/web/static/src/js/widgets/change_password.js b/addons/web/static/src/js/widgets/change_password.js new file mode 100644 index 00000000..e75d8dfd --- /dev/null +++ b/addons/web/static/src/js/widgets/change_password.js @@ -0,0 +1,75 @@ +odoo.define('web.ChangePassword', function (require) { +"use strict"; + +/** + * This file defines a client action that opens in a dialog (target='new') and + * allows the user to change his password. + */ + +var AbstractAction = require('web.AbstractAction'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var web_client = require('web.web_client'); + +var _t = core._t; + +var ChangePassword = AbstractAction.extend({ + template: "ChangePassword", + + /** + * @fixme: weird interaction with the parent for the $buttons handling + * + * @override + * @returns {Promise} + */ + start: function () { + var self = this; + web_client.set_title(_t("Change Password")); + var $button = self.$('.oe_form_button'); + $button.appendTo(this.getParent().$footer); + $button.eq(1).click(function () { + self.$el.parents('.modal').modal('hide'); + }); + $button.eq(0).click(function () { + self._rpc({ + route: '/web/session/change_password', + params: { + fields: $('form[name=change_password_form]').serializeArray() + } + }) + .then(function (result) { + if (result.error) { + self._display_error(result); + } else { + self.do_action('logout'); + } + }); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Displays the error in a dialog + * + * @private + * @param {Object} error + * @param {string} error.error + * @param {string} error.title + */ + _display_error: function (error) { + return new Dialog(this, { + size: 'medium', + title: error.title, + $content: $('
    ').html(error.error) + }).open(); + }, +}); + +core.action_registry.add("change_password", ChangePassword); + +return ChangePassword; + +}); diff --git a/addons/web/static/src/js/widgets/colorpicker.js b/addons/web/static/src/js/widgets/colorpicker.js new file mode 100644 index 00000000..29e8c22a --- /dev/null +++ b/addons/web/static/src/js/widgets/colorpicker.js @@ -0,0 +1,699 @@ +odoo.define('web.Colorpicker', function (require) { +'use strict'; + +var core = require('web.core'); +var utils = require('web.utils'); +var Dialog = require('web.Dialog'); +var Widget = require('web.Widget'); + +var _t = core._t; + +var ColorpickerWidget = Widget.extend({ + xmlDependencies: ['/web/static/src/xml/colorpicker.xml'], + template: 'Colorpicker', + events: { + 'click': '_onClick', + 'keypress': '_onKeypress', + 'mousedown .o_color_pick_area': '_onMouseDownPicker', + 'mousedown .o_color_slider': '_onMouseDownSlider', + 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', + 'change .o_color_picker_inputs': '_onChangeInputs', + }, + + /** + * @constructor + * @param {Widget} parent + * @param {Object} [options] + * @param {string} [options.defaultColor='#FF0000'] + * @param {string} [options.noTransparency=false] + */ + init: function (parent, options) { + this._super(...arguments); + options = options || {}; + this.trigger_up('getRecordInfo', { + recordInfo: options, + callback: function (recordInfo) { + _.defaults(options, recordInfo); + }, + }); + + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + this.colorComponents = {}; + this.uniqueId = _.uniqueId('colorpicker'); + + // Needs to be bound on document to work in all possible cases. + const $document = $(document); + $document.on(`mousemove.${this.uniqueId}`, _.throttle((ev) => { + this._onMouseMovePicker(ev); + this._onMouseMoveSlider(ev); + this._onMouseMoveOpacitySlider(ev); + }, 50)); + $document.on(`mouseup.${this.uniqueId}`, _.throttle((ev) => { + if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) { + this._colorSelected(); + } + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + }, 10)); + + this.options = _.clone(options); + }, + /** + * @override + */ + start: function () { + this.$colorpickerArea = this.$('.o_color_pick_area'); + this.$colorpickerPointer = this.$('.o_picker_pointer'); + this.$colorSlider = this.$('.o_color_slider'); + this.$colorSliderPointer = this.$('.o_slider_pointer'); + this.$opacitySlider = this.$('.o_opacity_slider'); + this.$opacitySliderPointer = this.$('.o_opacity_pointer'); + + var defaultColor = this.options.defaultColor || '#FF0000'; + var rgba = ColorpickerWidget.convertCSSColorToRgba(defaultColor); + if (rgba) { + this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); + } + + // Pre-fill the inputs. This is because on safari, the baseline for empty + // input is not the baseline of where the text would be, but the bottom + // of the input itself. (see https://bugs.webkit.org/show_bug.cgi?id=142968) + // This will cause the first _updateUI to alter the layout of the colorpicker + // which will change its height. Changing the height of an element inside of + // the callback to a ResizeObserver observing it will cause an error + // (ResizeObserver loop completed with undelivered notifications) that cannot + // be caught, which will open the crash manager. Prefilling the inputs sets + // the baseline correctly from the start so the layout doesn't change. + Object.entries(this.colorComponents).forEach(([component, value]) => { + const input = this.el.querySelector(`.o_${component}_input`); + if (input) { + input.value = value; + } + }); + const resizeObserver = new window.ResizeObserver(() => { + this._updateUI(); + }); + resizeObserver.observe(this.el); + + this.previewActive = true; + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + $(document).off(`.${this.uniqueId}`); + }, + /** + * Sets the currently selected color + * + * @param {string} color rgb[a] + */ + setSelectedColor: function (color) { + var rgba = ColorpickerWidget.convertCSSColorToRgba(color); + if (rgba) { + this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Updates input values, color preview, picker and slider pointer positions. + * + * @private + */ + _updateUI: function () { + var self = this; + + // Update inputs + _.each(this.colorComponents, function (value, color) { + self.$(_.str.sprintf('.o_%s_input', color)).val(value); + }); + + // Update preview + this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor); + + // Update picker area and picker pointer position + this.$colorpickerArea.css('background-color', _.str.sprintf('hsl(%s, 100%%, 50%%)', this.colorComponents.hue)); + var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; + var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; + this.$colorpickerPointer.css({ + top: (top - 5) + 'px', + left: (left - 5) + 'px', + }); + + // Update color slider position + var height = this.$colorSlider.height(); + var y = this.colorComponents.hue * height / 360; + this.$colorSliderPointer.css('top', Math.round(y - 2)); + + if (! this.options.noTransparency) { + // Update opacity slider position + var heightOpacity = this.$opacitySlider.height(); + var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); + this.$opacitySliderPointer.css('top', Math.round(z - 2)); + + // Add gradient color on opacity slider + this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); + } + }, + /** + * Updates colors according to given hex value. Opacity is left unchanged. + * + * @private + * @param {string} hex - hexadecimal code + */ + _updateHex: function (hex) { + var rgb = ColorpickerWidget.convertCSSColorToRgba(hex); + if (!rgb) { + return; + } + _.extend(this.colorComponents, + {hex: hex}, + rgb, + ColorpickerWidget.convertRgbToHsl(rgb.red, rgb.green, rgb.blue) + ); + this._updateCssColor(); + }, + /** + * Updates colors according to given RGB values. + * + * @private + * @param {integer} r + * @param {integer} g + * @param {integer} b + * @param {integer} [a] + */ + _updateRgba: function (r, g, b, a) { + // We update the hexadecimal code by transforming into a css color and + // ignoring the opacity (we don't display opacity component in hexa as + // not supported on all browsers) + var hex = ColorpickerWidget.convertRgbaToCSSColor(r, g, b); + if (!hex) { + return; + } + _.extend(this.colorComponents, + {red: r, green: g, blue: b}, + a === undefined ? {} : {opacity: a}, + {hex: hex}, + ColorpickerWidget.convertRgbToHsl(r, g, b) + ); + this._updateCssColor(); + }, + /** + * Updates colors according to given HSL values. + * + * @private + * @param {integer} h + * @param {integer} s + * @param {integer} l + */ + _updateHsl: function (h, s, l) { + var rgb = ColorpickerWidget.convertHslToRgb(h, s, l); + if (!rgb) { + return; + } + // We receive an hexa as we ignore the opacity + const hex = ColorpickerWidget.convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); + _.extend(this.colorComponents, + {hue: h, saturation: s, lightness: l}, + rgb, + {hex: hex} + ); + this._updateCssColor(); + }, + /** + * Updates color opacity. + * + * @private + * @param {integer} a + */ + _updateOpacity: function (a) { + if (a < 0 || a > 100) { + return; + } + _.extend(this.colorComponents, + {opacity: a} + ); + this._updateCssColor(); + }, + /** + * Trigger an event to annonce that the widget value has changed + * + * @private + */ + _colorSelected: function () { + this.trigger_up('colorpicker_select', this.colorComponents); + }, + /** + * Updates css color representation. + * + * @private + */ + _updateCssColor: function () { + const r = this.colorComponents.red; + const g = this.colorComponents.green; + const b = this.colorComponents.blue; + const a = this.colorComponents.opacity; + _.extend(this.colorComponents, + {cssColor: ColorpickerWidget.convertRgbaToCSSColor(r, g, b, a)} + ); + if (this.previewActive) { + this.trigger_up('colorpicker_preview', this.colorComponents); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onKeypress: function (ev) { + if (ev.charCode === $.ui.keyCode.ENTER) { + if (ev.target.tagName === 'INPUT') { + this._onChangeInputs(ev); + } + ev.preventDefault(); + this.trigger_up('enter_key_color_colorpicker'); + } + }, + /** + * @param {Event} ev + */ + _onClick: function (ev) { + ev.originalEvent.__isColorpickerClick = true; + $(ev.target).find('> .o_opacity_pointer, > .o_slider_pointer, > .o_picker_pointer').addBack('.o_opacity_pointer, .o_slider_pointer, .o_picker_pointer').focus(); + }, + /** + * Updates color when the user starts clicking on the picker. + * + * @private + * @param {Event} ev + */ + _onMouseDownPicker: function (ev) { + this.pickerFlag = true; + ev.preventDefault(); + this._onMouseMovePicker(ev); + }, + /** + * Updates saturation and lightness values on mouse drag over picker. + * + * @private + * @param {Event} ev + */ + _onMouseMovePicker: function (ev) { + if (!this.pickerFlag) { + return; + } + + var offset = this.$colorpickerArea.offset(); + var top = ev.pageY - offset.top; + var left = ev.pageX - offset.left; + var saturation = Math.round(100 * left / this.$colorpickerArea.width()); + var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); + saturation = utils.confine(saturation, 0, 100); + lightness = utils.confine(lightness, 0, 100); + + this._updateHsl(this.colorComponents.hue, saturation, lightness); + this._updateUI(); + }, + /** + * Updates color when user starts clicking on slider. + * + * @private + * @param {Event} ev + */ + _onMouseDownSlider: function (ev) { + this.sliderFlag = true; + ev.preventDefault(); + this._onMouseMoveSlider(ev); + }, + /** + * Updates hue value on mouse drag over slider. + * + * @private + * @param {Event} ev + */ + _onMouseMoveSlider: function (ev) { + if (!this.sliderFlag) { + return; + } + + var y = ev.pageY - this.$colorSlider.offset().top; + var hue = Math.round(360 * y / this.$colorSlider.height()); + hue = utils.confine(hue, 0, 360); + + this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); + this._updateUI(); + }, + /** + * Updates opacity when user starts clicking on opacity slider. + * + * @private + * @param {Event} ev + */ + _onMouseDownOpacitySlider: function (ev) { + this.opacitySliderFlag = true; + ev.preventDefault(); + this._onMouseMoveOpacitySlider(ev); + }, + /** + * Updates opacity value on mouse drag over opacity slider. + * + * @private + * @param {Event} ev + */ + _onMouseMoveOpacitySlider: function (ev) { + if (!this.opacitySliderFlag || this.options.noTransparency) { + return; + } + + var y = ev.pageY - this.$opacitySlider.offset().top; + var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); + opacity = utils.confine(opacity, 0, 100); + + this._updateOpacity(opacity); + this._updateUI(); + }, + /** + * Called when input value is changed -> Updates UI: Set picker and slider + * position and set colors. + * + * @private + * @param {Event} ev + */ + _onChangeInputs: function (ev) { + switch ($(ev.target).data('colorMethod')) { + case 'hex': + this._updateHex(this.$('.o_hex_input').val()); + break; + case 'rgb': + this._updateRgba( + parseInt(this.$('.o_red_input').val()), + parseInt(this.$('.o_green_input').val()), + parseInt(this.$('.o_blue_input').val()) + ); + break; + case 'hsl': + this._updateHsl( + parseInt(this.$('.o_hue_input').val()), + parseInt(this.$('.o_saturation_input').val()), + parseInt(this.$('.o_lightness_input').val()) + ); + break; + case 'opacity': + this._updateOpacity(parseInt(this.$('.o_opacity_input').val())); + break; + } + this._updateUI(); + this._colorSelected(); + }, +}); + +//-------------------------------------------------------------------------- +// Static +//-------------------------------------------------------------------------- + +/** + * Converts RGB color components to HSL components. + * + * @static + * @param {integer} r - [0, 255] + * @param {integer} g - [0, 255] + * @param {integer} b - [0, 255] + * @returns {Object|false} + * - hue [0, 360[ + * - saturation [0, 100] + * - lightness [0, 100] + */ +ColorpickerWidget.convertRgbToHsl = function (r, g, b) { + if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 + || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 + || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { + return false; + } + + var red = r / 255; + var green = g / 255; + var blue = b / 255; + var maxColor = Math.max(red, green, blue); + var minColor = Math.min(red, green, blue); + var delta = maxColor - minColor; + var hue = 0; + var saturation = 0; + var lightness = (maxColor + minColor) / 2; + if (delta) { + if (maxColor === red) { + hue = (green - blue) / delta; + } + if (maxColor === green) { + hue = 2 + (blue - red) / delta; + } + if (maxColor === blue) { + hue = 4 + (red - green) / delta; + } + if (maxColor) { + saturation = delta / (1 - Math.abs(2 * lightness - 1)); + } + } + hue = 60 * hue | 0; + return { + hue: hue < 0 ? hue += 360 : hue, + saturation: (saturation * 100) | 0, + lightness: (lightness * 100) | 0, + }; +}; +/** + * Converts HSL color components to RGB components. + * + * @static + * @param {integer} h - [0, 360[ + * @param {integer} s - [0, 100] + * @param {integer} l - [0, 100] + * @returns {Object|false} + * - red [0, 255] + * - green [0, 255] + * - blue [0, 255] + */ +ColorpickerWidget.convertHslToRgb = function (h, s, l) { + if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 + || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 + || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) { + return false; + } + + var huePrime = h / 60; + var saturation = s / 100; + var lightness = l / 100; + var chroma = saturation * (1 - Math.abs(2 * lightness - 1)); + var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1)); + var lightnessAdjustment = lightness - chroma / 2; + var precision = 255; + chroma = (chroma + lightnessAdjustment) * precision | 0; + secondComponent = (secondComponent + lightnessAdjustment) * precision | 0; + lightnessAdjustment = lightnessAdjustment * precision | 0; + if (huePrime >= 0 && huePrime < 1) { + return { + red: chroma, + green: secondComponent, + blue: lightnessAdjustment, + }; + } + if (huePrime >= 1 && huePrime < 2) { + return { + red: secondComponent, + green: chroma, + blue: lightnessAdjustment, + }; + } + if (huePrime >= 2 && huePrime < 3) { + return { + red: lightnessAdjustment, + green: chroma, + blue: secondComponent, + }; + } + if (huePrime >= 3 && huePrime < 4) { + return { + red: lightnessAdjustment, + green: secondComponent, + blue: chroma, + }; + } + if (huePrime >= 4 && huePrime < 5) { + return { + red: secondComponent, + green: lightnessAdjustment, + blue: chroma, + }; + } + if (huePrime >= 5 && huePrime <= 6) { + return { + red: chroma, + green: lightnessAdjustment, + blue: secondComponent, + }; + } + return false; +}; +/** + * Converts RGBA color components to a normalized CSS color: if the opacity + * is invalid or equal to 100, a hex is returned; otherwise a rgba() css color + * is returned. + * + * Those choice have multiple reason: + * - A hex color is more common to c/c from other utilities on the web and is + * also shorter than rgb() css colors + * - Opacity in hexadecimal notations is not supported on all browsers and is + * also less common to use. + * + * @static + * @param {integer} r - [0, 255] + * @param {integer} g - [0, 255] + * @param {integer} b - [0, 255] + * @param {float} a - [0, 100] + * @returns {string} + */ +ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { + if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 + || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 + || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { + return false; + } + if (typeof (a) !== 'number' || isNaN(a) || a < 0 || Math.abs(a - 100) < Number.EPSILON) { + const rr = r < 16 ? '0' + r.toString(16) : r.toString(16); + const gg = g < 16 ? '0' + g.toString(16) : g.toString(16); + const bb = b < 16 ? '0' + b.toString(16) : b.toString(16); + return (`#${rr}${gg}${bb}`).toUpperCase(); + } + return `rgba(${r}, ${g}, ${b}, ${parseFloat((a / 100.0).toFixed(3))})`; +}; +/** + * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components. + * + * Note: we don't support using and displaying hexadecimal color with opacity + * but this method allows to receive one and returns the correct opacity value. + * + * @static + * @param {string} cssColor - hexadecimal code or rgb() or rgba() + * @returns {Object|false} + * - red [0, 255] + * - green [0, 255] + * - blue [0, 255] + * - opacity [0, 100.0] + */ +ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { + // Check if cssColor is a rgba() or rgb() color + const rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); + if (rgba) { + if (rgba[4] === undefined) { + rgba[4] = 1; + } + return { + red: parseInt(rgba[1]), + green: parseInt(rgba[2]), + blue: parseInt(rgba[3]), + opacity: Math.round(parseFloat(rgba[4]) * 100), + }; + } + + // Otherwise, check if cssColor is an hexadecimal code color + if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) { + return { + red: parseInt(cssColor.substr(1, 2), 16), + green: parseInt(cssColor.substr(3, 2), 16), + blue: parseInt(cssColor.substr(5, 2), 16), + opacity: (cssColor.length === 9 ? (parseInt(cssColor.substr(7, 2), 16) / 255) : 1) * 100, + }; + } + + // TODO maybe implement a support for receiving css color like 'red' or + // 'transparent' (which are now considered non-css color by isCSSColor...) + // Note: however, if ever implemented be careful of 'white'/'black' which + // actually are color names for our color system... + + return false; +}; +/** + * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version + * of the same color (@see convertRgbaToCSSColor). + * + * Normalized color can be safely compared using string comparison. + * + * @static + * @param {string} cssColor - hexadecimal code or rgb() or rgba() + * @returns {string} - the normalized css color or the given css color if it + * failed to be normalized + */ +ColorpickerWidget.normalizeCSSColor = function (cssColor) { + const rgba = ColorpickerWidget.convertCSSColorToRgba(cssColor); + if (!rgba) { + return cssColor; + } + return ColorpickerWidget.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); +}; +/** + * Checks if a given string is a css color. + * + * @static + * @param {string} cssColor + * @returns {boolean} + */ +ColorpickerWidget.isCSSColor = function (cssColor) { + return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false; +}; + +const ColorpickerDialog = Dialog.extend({ + /** + * @override + */ + init: function (parent, options) { + this.options = options || {}; + this._super(parent, _.extend({ + size: 'small', + title: _t('Pick a color'), + buttons: [ + {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)}, + {text: _t('Discard'), close: true}, + ], + }, this.options)); + }, + /** + * @override + */ + start: function () { + const proms = [this._super(...arguments)]; + this.colorPicker = new ColorpickerWidget(this, _.extend({ + colorPreview: true, + }, this.options)); + proms.push(this.colorPicker.appendTo(this.$el)); + return Promise.all(proms); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onFinalPick: function () { + this.trigger_up('colorpicker:saved', this.colorPicker.colorComponents); + }, +}); + +return { + ColorpickerDialog: ColorpickerDialog, + ColorpickerWidget: ColorpickerWidget, +}; +}); diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js new file mode 100644 index 00000000..f6354920 --- /dev/null +++ b/addons/web/static/src/js/widgets/data_export.js @@ -0,0 +1,688 @@ +odoo.define('web.DataExport', function (require) { +"use strict"; + +var config = require('web.config'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var data = require('web.data'); +var framework = require('web.framework'); +var pyUtils = require('web.py_utils'); + +var QWeb = core.qweb; +var _t = core._t; + +var DataExport = Dialog.extend({ + template: 'ExportDialog', + events: { + 'change .o_exported_lists_select': '_onChangeExportList', + 'change .o_import_compat input': '_onChangeCompatibleInput', + 'click .o_add_field': '_onClickAddField', + 'click .o_delete_exported_list': '_onClickDeleteExportListBtn', + 'click .o_expand': '_onClickExpand', + 'click .o_remove_field': '_onClickRemoveField', + 'click .o_save_list .o_save_list_btn': '_onClickSaveListBtn', + 'click .o_save_list .o_cancel_list_btn': '_resetTemplateField', + 'click .o_export_tree_item': '_onClickTreeItem', + 'dblclick .o_export_tree_item:not(.haschild)': '_onDblclickTreeItem', + 'keydown .o_export_tree_item': '_onKeydownTreeItem', + 'keydown .o_save_list_name': '_onKeydownSaveList', + 'input .o_export_search_input': '_onSearchInput', + }, + /** + * @constructor + * @param {Widget} parent + * @param {Object} record + * @param {string[]} defaultExportFields + */ + init: function (parent, record, defaultExportFields, groupedBy, activeDomain, idsToExport) { + var options = { + title: _t("Export Data"), + buttons: [ + {text: _t("Export"), click: this._onExportData, classes: 'btn-primary'}, + {text: _t("Close"), close: true}, + ], + }; + this._super(parent, options); + this.records = {}; + this.record = record; + this.defaultExportFields = defaultExportFields; + this.groupby = groupedBy; + this.exports = new data.DataSetSearch(this, 'ir.exports', this.record.getContext()); + this.rowIndex = 0; + this.rowIndexLevel = 0; + this.isCompatibleMode = false; + this.domain = activeDomain || this.record.domain; + this.idsToExport = activeDomain ? false: idsToExport; + }, + /** + * @override + */ + start: function () { + var self = this; + var proms = [this._super.apply(this, arguments)]; + + // The default for the ".modal_content" element is "max-height: 100%;" + // but we want it to always expand to "height: 100%;" for this modal. + // This can be achieved thanks to CSS modification without touching + // the ".modal-content" rules... but not with Internet explorer (11). + this.$modal.find('.modal-content').css('height', '100%'); + + this.$fieldsList = this.$('.o_fields_list'); + + proms.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats)); + proms.push(this._onChangeCompatibleInput().then(function () { + _.each(self.defaultExportFields, function (field) { + var record = self.records[field]; + self._addField(record.id, record.string); + }); + })); + + proms.push(this._showExportsList()); + + // Bind sortable events after Dialog is open + this.opened().then(function () { + self.$('.o_fields_list').sortable({ + axis: 'y', + handle: '.o_short_field', + forcePlaceholderSize: true, + placeholder: 'o-field-placeholder', + update: self.proxy('_resetTemplateField'), + }); + }); + return Promise.all(proms); + + function doSetupExportFormats(formats) { + var $fmts = self.$('.o_export_format'); + + _.each(formats, function (format) { + var $radio = $('', {type: 'radio', value: format.tag, name: 'o_export_format_name', class: 'form-check-input', id: 'o_radio' + format.label}); + var $label = $('