summaryrefslogtreecommitdiff
path: root/addons/website_sale/static/src/js/website_sale.editor.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website_sale/static/src/js/website_sale.editor.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_sale/static/src/js/website_sale.editor.js')
-rw-r--r--addons/website_sale/static/src/js/website_sale.editor.js698
1 files changed, 698 insertions, 0 deletions
diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js
new file mode 100644
index 00000000..b60d15f2
--- /dev/null
+++ b/addons/website_sale/static/src/js/website_sale.editor.js
@@ -0,0 +1,698 @@
+odoo.define('website_sale.add_product', function (require) {
+'use strict';
+
+var core = require('web.core');
+var wUtils = require('website.utils');
+var WebsiteNewMenu = require('website.newMenu');
+
+var _t = core._t;
+
+WebsiteNewMenu.include({
+ actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
+ new_product: '_createNewProduct',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Actions
+ //--------------------------------------------------------------------------
+
+ /**
+ * Asks the user information about a new product to create, then creates it
+ * and redirects the user to this new product.
+ *
+ * @private
+ * @returns {Promise} Unresolved if there is a redirection
+ */
+ _createNewProduct: function () {
+ var self = this;
+ return wUtils.prompt({
+ id: "editor_new_product",
+ window_title: _t("New Product"),
+ input: _t("Name"),
+ }).then(function (result) {
+ if (!result.val) {
+ return;
+ }
+ return self._rpc({
+ route: '/shop/add_product',
+ params: {
+ name: result.val,
+ },
+ }).then(function (url) {
+ window.location.href = url;
+ return new Promise(function () {});
+ });
+ });
+ },
+});
+});
+
+//==============================================================================
+
+odoo.define('website_sale.editor', function (require) {
+'use strict';
+
+var options = require('web_editor.snippets.options');
+var publicWidget = require('web.public.widget');
+const {Class: EditorMenuBar} = require('web_editor.editor');
+const {qweb} = require('web.core');
+
+EditorMenuBar.include({
+ custom_events: Object.assign(EditorMenuBar.prototype.custom_events, {
+ get_ribbons: '_onGetRibbons',
+ get_ribbon_classes: '_onGetRibbonClasses',
+ delete_ribbon: '_onDeleteRibbon',
+ set_ribbon: '_onSetRibbon',
+ set_product_ribbon: '_onSetProductRibbon',
+ }),
+
+ /**
+ * @override
+ */
+ async willStart() {
+ const _super = this._super.bind(this);
+ let ribbons = [];
+ if (this._isProductListPage()) {
+ ribbons = await this._rpc({
+ model: 'product.ribbon',
+ method: 'search_read',
+ fields: ['id', 'html', 'bg_color', 'text_color', 'html_class'],
+ });
+ }
+ this.ribbons = Object.fromEntries(ribbons.map(ribbon => [ribbon.id, ribbon]));
+ this.originalRibbons = Object.assign({}, this.ribbons);
+ this.productTemplatesRibbons = [];
+ this.deletedRibbonClasses = '';
+ return _super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async save() {
+ const _super = this._super.bind(this);
+ await this._saveRibbons();
+ return _super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Saves the ribbons in the database.
+ *
+ * @private
+ */
+ async _saveRibbons() {
+ if (!this._isProductListPage()) {
+ return;
+ }
+ const originalIds = Object.keys(this.originalRibbons).map(id => parseInt(id));
+ const currentIds = Object.keys(this.ribbons).map(id => parseInt(id));
+
+ const ribbons = Object.values(this.ribbons);
+ const created = ribbons.filter(ribbon => !originalIds.includes(ribbon.id));
+ const deletedIds = originalIds.filter(id => !currentIds.includes(id));
+ const modified = ribbons.filter(ribbon => {
+ if (created.includes(ribbon)) {
+ return false;
+ }
+ const original = this.originalRibbons[ribbon.id];
+ return Object.entries(ribbon).some(([key, value]) => value !== original[key]);
+ });
+
+ const proms = [];
+ let createdRibbonIds;
+ if (created.length > 0) {
+ proms.push(this._rpc({
+ method: 'create',
+ model: 'product.ribbon',
+ args: [created.map(ribbon => {
+ ribbon = Object.assign({}, ribbon);
+ delete ribbon.id;
+ return ribbon;
+ })],
+ }).then(ids => createdRibbonIds = ids));
+ }
+
+ modified.forEach(ribbon => proms.push(this._rpc({
+ method: 'write',
+ model: 'product.ribbon',
+ args: [[ribbon.id], ribbon],
+ })));
+
+ if (deletedIds.length > 0) {
+ proms.push(this._rpc({
+ method: 'unlink',
+ model: 'product.ribbon',
+ args: [deletedIds],
+ }));
+ }
+
+ await Promise.all(proms);
+ const localToServer = Object.assign(
+ this.ribbons,
+ Object.fromEntries(created.map((ribbon, index) => [ribbon.id, {id: createdRibbonIds[index]}])),
+ {'false': {id: false}},
+ );
+
+ // Building the final template to ribbon-id map
+ const finalTemplateRibbons = this.productTemplatesRibbons.reduce((acc, {templateId, ribbonId}) => {
+ acc[templateId] = ribbonId;
+ return acc;
+ }, {});
+ // Inverting the relationship so that we have all templates that have the same ribbon to reduce RPCs
+ const ribbonTemplates = Object.entries(finalTemplateRibbons).reduce((acc, [templateId, ribbonId]) => {
+ if (!acc[ribbonId]) {
+ acc[ribbonId] = [];
+ }
+ acc[ribbonId].push(parseInt(templateId));
+ return acc;
+ }, {});
+ const setProductTemplateRibbons = Object.entries(ribbonTemplates)
+ // If the ribbonId that the template had no longer exists, remove the ribbon (id = false)
+ .map(([ribbonId, templateIds]) => {
+ const id = currentIds.includes(parseInt(ribbonId)) ? ribbonId : false;
+ return [id, templateIds];
+ }).map(([ribbonId, templateIds]) => this._rpc({
+ method: 'write',
+ model: 'product.template',
+ args: [templateIds, {'website_ribbon_id': localToServer[ribbonId].id}],
+ }));
+ return Promise.all(setProductTemplateRibbons);
+ },
+ /**
+ * Checks whether the current page is the product list.
+ *
+ * @private
+ */
+ _isProductListPage() {
+ return $('#products_grid').length !== 0;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns a copy of this.ribbons through a callback.
+ *
+ * @private
+ */
+ _onGetRibbons(ev) {
+ ev.data.callback(Object.assign({}, this.ribbons));
+ },
+ /**
+ * Returns all ribbon classes, current and deleted, so they can be removed.
+ *
+ * @private
+ */
+ _onGetRibbonClasses(ev) {
+ const classes = Object.values(this.ribbons).reduce((classes, ribbon) => {
+ return classes + ` ${ribbon.html_class}`;
+ }, '') + this.deletedRibbonClasses;
+ ev.data.callback(classes);
+ },
+ /**
+ * Deletes a ribbon.
+ *
+ * @private
+ */
+ _onDeleteRibbon(ev) {
+ this.deletedRibbonClasses += ` ${this.ribbons[ev.data.id].html_class}`;
+ delete this.ribbons[ev.data.id];
+ },
+ /**
+ * Sets a ribbon;
+ *
+ * @private
+ */
+ _onSetRibbon(ev) {
+ const {ribbon} = ev.data;
+ const previousRibbon = this.ribbons[ribbon.id];
+ if (previousRibbon) {
+ this.deletedRibbonClasses += ` ${previousRibbon.html_class}`;
+ }
+ this.ribbons[ribbon.id] = ribbon;
+ },
+ /**
+ * Sets which ribbon is used by a product template.
+ *
+ * @private
+ */
+ _onSetProductRibbon(ev) {
+ const {templateId, ribbonId} = ev.data;
+ this.productTemplatesRibbons.push({templateId, ribbonId});
+ },
+});
+
+publicWidget.registry.websiteSaleCurrency = publicWidget.Widget.extend({
+ selector: '.oe_website_sale',
+ disabledInEditableMode: false,
+ edit_events: {
+ 'click .oe_currency_value:o_editable': '_onCurrencyValueClick',
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onCurrencyValueClick: function (ev) {
+ $(ev.currentTarget).selectContent();
+ },
+});
+
+function reload() {
+ if (window.location.href.match(/\?enable_editor/)) {
+ window.location.reload();
+ } else {
+ window.location.href = window.location.href.replace(/\?(enable_editor=1&)?|#.*|$/, '?enable_editor=1&');
+ }
+}
+
+options.registry.WebsiteSaleGridLayout = options.Class.extend({
+
+ /**
+ * @override
+ */
+ start: function () {
+ this.ppg = parseInt(this.$target.closest('[data-ppg]').data('ppg'));
+ this.ppr = parseInt(this.$target.closest('[data-ppr]').data('ppr'));
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ onFocus: function () {
+ var listLayoutEnabled = this.$target.closest('#products_grid').hasClass('o_wsale_layout_list');
+ this.$el.filter('.o_wsale_ppr_submenu').toggleClass('d-none', listLayoutEnabled);
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * @see this.selectClass for params
+ */
+ setPpg: function (previewMode, widgetValue, params) {
+ const ppg = parseInt(widgetValue);
+ if (!ppg || ppg < 1) {
+ return false;
+ }
+ this.ppg = ppg;
+ return this._rpc({
+ route: '/shop/change_ppg',
+ params: {
+ 'ppg': ppg,
+ },
+ }).then(() => reload());
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setPpr: function (previewMode, widgetValue, params) {
+ this.ppr = parseInt(widgetValue);
+ this._rpc({
+ route: '/shop/change_ppr',
+ params: {
+ 'ppr': this.ppr,
+ },
+ }).then(reload);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ switch (methodName) {
+ case 'setPpg': {
+ return this.ppg;
+ }
+ case 'setPpr': {
+ return this.ppr;
+ }
+ }
+ return this._super(...arguments);
+ },
+});
+
+options.registry.WebsiteSaleProductsItem = options.Class.extend({
+ xmlDependencies: (options.Class.prototype.xmlDependencies || []).concat(['/website_sale/static/src/xml/website_sale_utils.xml']),
+ events: _.extend({}, options.Class.prototype.events || {}, {
+ 'mouseenter .o_wsale_soptions_menu_sizes table': '_onTableMouseEnter',
+ 'mouseleave .o_wsale_soptions_menu_sizes table': '_onTableMouseLeave',
+ 'mouseover .o_wsale_soptions_menu_sizes td': '_onTableItemMouseEnter',
+ 'click .o_wsale_soptions_menu_sizes td': '_onTableItemClick',
+ }),
+
+ /**
+ * @override
+ */
+ willStart: async function () {
+ const _super = this._super.bind(this);
+ this.ppr = this.$target.closest('[data-ppr]').data('ppr');
+ this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id'));
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ return _super(...arguments);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ this._resetRibbonDummy();
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ onFocus: function () {
+ var listLayoutEnabled = this.$target.closest('#products_grid').hasClass('o_wsale_layout_list');
+ this.$el.find('.o_wsale_soptions_menu_sizes')
+ .toggleClass('d-none', listLayoutEnabled);
+ // Ribbons may have been edited or deleted in another products' option, need to make sure they're up to date
+ this.rerender = true;
+ },
+ /**
+ * @override
+ */
+ onBlur: function () {
+ // Since changes will not be saved unless they are validated, reset the
+ // previewed ribbon onBlur to communicate that to the user
+ this._resetRibbonDummy();
+ this._toggleEditingUI(false);
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ selectStyle(previewMode, widgetValue, params) {
+ const proms = [this._super(...arguments)];
+ if (params.cssProperty === 'background-color' && params.colorNames.includes(widgetValue)) {
+ // Reset text-color when choosing a background-color class, so it uses the automatic text-color of the class.
+ proms.push(this.selectStyle(previewMode, '', {applyTo: '.o_wsale_ribbon_dummy', cssProperty: 'color'}));
+ }
+ return Promise.all(proms);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ async setRibbon(previewMode, widgetValue, params) {
+ if (previewMode === 'reset') {
+ widgetValue = this.prevRibbonId;
+ } else {
+ this.prevRibbonId = this.$target[0].dataset.ribbonId;
+ }
+ this.$target[0].dataset.ribbonId = widgetValue;
+ this.trigger_up('set_product_ribbon', {
+ templateId: this.productTemplateID,
+ ribbonId: widgetValue || false,
+ });
+ const ribbon = this.ribbons[widgetValue] || {html: '', bg_color: '', text_color: '', html_class: ''};
+ const $ribbons = $(`[data-ribbon-id="${widgetValue}"] .o_ribbon:not(.o_wsale_ribbon_dummy)`);
+ $ribbons.html(ribbon.html);
+ let htmlClasses;
+ this.trigger_up('get_ribbon_classes', {callback: classes => htmlClasses = classes});
+ $ribbons.removeClass(htmlClasses);
+
+ $ribbons.addClass(ribbon.html_class || '');
+ $ribbons.css('color', ribbon.text_color);
+ $ribbons.css('background-color', ribbon.bg_color || '');
+
+ if (!this.ribbons[widgetValue]) {
+ $(`[data-ribbon-id="${widgetValue}"]`).each((index, product) => delete product.dataset.ribbonId);
+ }
+ this._resetRibbonDummy();
+ this._toggleEditingUI(false);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ editRibbon(previewMode, widgetValue, params) {
+ this.saveMethod = 'modify';
+ this._toggleEditingUI(true);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ createRibbon(previewMode, widgetValue, params) {
+ this.saveMethod = 'create';
+ this.$ribbon.html('Ribbon text');
+ this.$ribbon.addClass('bg-primary o_ribbon_left');
+ this._toggleEditingUI(true);
+ this.isCreating = true;
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ async deleteRibbon(previewMode, widgetValue, params) {
+ if (this.isCreating) {
+ // Ribbon doesn't exist yet, simply discard.
+ this.isCreating = false;
+ this._resetRibbonDummy();
+ return this._toggleEditingUI(false);
+ }
+ const {ribbonId} = this.$target[0].dataset;
+ this.trigger_up('delete_ribbon', {id: ribbonId});
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ this.rerender = true;
+ await this.setRibbon(false, ribbonId);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ async saveRibbon(previewMode, widgetValue, params) {
+ const text = this.$ribbon.html().trim();
+ if (!text) {
+ return;
+ }
+ const ribbon = {
+ 'html': text,
+ 'bg_color': this.$ribbon[0].style.backgroundColor,
+ 'text_color': this.$ribbon[0].style.color,
+ 'html_class': this.$ribbon.attr('class').split(' ')
+ .filter(c => !['d-none', 'o_wsale_ribbon_dummy', 'o_ribbon'].includes(c))
+ .join(' '),
+ };
+ ribbon.id = this.saveMethod === 'modify' ? parseInt(this.$target[0].dataset.ribbonId) : Date.now();
+ this.trigger_up('set_ribbon', {ribbon: ribbon});
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ this.rerender = true;
+ await this.setRibbon(false, ribbon.id);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setRibbonHtml(previewMode, widgetValue, params) {
+ this.$ribbon.html(widgetValue);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setRibbonMode(previewMode, widgetValue, params) {
+ this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_${widgetValue}_$2`);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setRibbonPosition(previewMode, widgetValue, params) {
+ this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_$1_${widgetValue}`);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ changeSequence: function (previewMode, widgetValue, params) {
+ this._rpc({
+ route: '/shop/change_sequence',
+ params: {
+ id: this.productTemplateID,
+ sequence: widgetValue,
+ },
+ }).then(reload);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ updateUI: async function () {
+ await this._super.apply(this, arguments);
+
+ var sizeX = parseInt(this.$target.attr('colspan') || 1);
+ var sizeY = parseInt(this.$target.attr('rowspan') || 1);
+
+ var $size = this.$el.find('.o_wsale_soptions_menu_sizes');
+ $size.find('tr:nth-child(-n + ' + sizeY + ') td:nth-child(-n + ' + sizeX + ')')
+ .addClass('selected');
+
+ // Adapt size array preview to fit ppr
+ $size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide();
+ if (this.rerender) {
+ this.rerender = false;
+ return this._rerenderXML();
+ }
+ },
+ /**
+ * @override
+ */
+ updateUIVisibility: async function () {
+ // Main updateUIVisibility will remove the d-none class because there are visible widgets
+ // inside of it. TODO: update this once updateUIVisibility can be used to compute visibility
+ // of arbitrary DOM elements and not just widgets.
+ const isEditing = this.$el.find('[data-name="ribbon_options"]').hasClass('d-none');
+ await this._super(...arguments);
+ this._toggleEditingUI(isEditing);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async _renderCustomXML(uiFragment) {
+ const $select = $(uiFragment.querySelector('.o_wsale_ribbon_select'));
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ if (!this.$ribbon) {
+ this._resetRibbonDummy();
+ }
+ const classes = this.$ribbon[0].className;
+ this.$ribbon[0].className = '';
+ const defaultTextColor = window.getComputedStyle(this.$ribbon[0]).color;
+ this.$ribbon[0].className = classes;
+ Object.values(this.ribbons).forEach(ribbon => {
+ const colorClasses = ribbon.html_class
+ .split(' ')
+ .filter(className => !/^o_(ribbon|tag)_(left|right)$/.test(className))
+ .join(' ');
+ $select.append(qweb.render('website_sale.ribbonSelectItem', {
+ ribbon,
+ colorClasses,
+ isTag: /o_tag_(left|right)/.test(ribbon.html_class),
+ isLeft: /o_(tag|ribbon)_left/.test(ribbon.html_class),
+ textColor: ribbon.text_color || colorClasses ? 'currentColor' : defaultTextColor,
+ }));
+ });
+ },
+ /**
+ * @override
+ */
+ async _computeWidgetState(methodName, params) {
+ const classList = this.$ribbon[0].classList;
+ switch (methodName) {
+ case 'setRibbon':
+ return this.$target.attr('data-ribbon-id') || '';
+ case 'setRibbonHtml':
+ return this.$ribbon.html();
+ case 'setRibbonMode': {
+ if (classList.contains('o_ribbon_left') || classList.contains('o_ribbon_right')) {
+ return 'ribbon';
+ }
+ return 'tag';
+ }
+ case 'setRibbonPosition': {
+ if (classList.contains('o_tag_left') || classList.contains('o_ribbon_left')) {
+ return 'left';
+ }
+ return 'right';
+ }
+ }
+ return this._super(methodName, params);
+ },
+ /**
+ * Toggles the UI mode between select and create/edit mode.
+ *
+ * @private
+ * @param {Boolean} state true to activate editing UI, false to deactivate.
+ */
+ _toggleEditingUI(state) {
+ this.$el.find('[data-name="ribbon_options"]').toggleClass('d-none', state);
+ this.$el.find('[data-name="ribbon_customize_opt"]').toggleClass('d-none', !state);
+ this.$('.o_ribbon:not(.o_wsale_ribbon_dummy)').toggleClass('d-none', state);
+ this.$ribbon.toggleClass('d-none', !state);
+ },
+ /**
+ * Creates a copy of current ribbon to manipulate for edition/creation.
+ *
+ * @private
+ */
+ _resetRibbonDummy() {
+ if (this.$ribbon) {
+ this.$ribbon.remove();
+ }
+ const $original = this.$('.o_ribbon');
+ this.$ribbon = $original.clone().addClass('d-none o_wsale_ribbon_dummy').appendTo($original.parent());
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onTableMouseEnter: function (ev) {
+ $(ev.currentTarget).addClass('oe_hover');
+ },
+ /**
+ * @private
+ */
+ _onTableMouseLeave: function (ev) {
+ $(ev.currentTarget).removeClass('oe_hover');
+ },
+ /**
+ * @private
+ */
+ _onTableItemMouseEnter: function (ev) {
+ var $td = $(ev.currentTarget);
+ var $table = $td.closest("table");
+ var x = $td.index() + 1;
+ var y = $td.parent().index() + 1;
+
+ var tr = [];
+ for (var yi = 0; yi < y; yi++) {
+ tr.push("tr:eq(" + yi + ")");
+ }
+ var $selectTr = $table.find(tr.join(","));
+ var td = [];
+ for (var xi = 0; xi < x; xi++) {
+ td.push("td:eq(" + xi + ")");
+ }
+ var $selectTd = $selectTr.find(td.join(","));
+
+ $table.find("td").removeClass("select");
+ $selectTd.addClass("select");
+ },
+ /**
+ * @private
+ */
+ _onTableItemClick: function (ev) {
+ var $td = $(ev.currentTarget);
+ var x = $td.index() + 1;
+ var y = $td.parent().index() + 1;
+ this._rpc({
+ route: '/shop/change_size',
+ params: {
+ id: this.productTemplateID,
+ x: x,
+ y: y,
+ },
+ }).then(reload);
+ },
+});
+});