odoo.define('website.editor.snippets.options', function (require) { 'use strict'; const {ColorpickerWidget} = require('web.Colorpicker'); const config = require('web.config'); var core = require('web.core'); var Dialog = require('web.Dialog'); const dom = require('web.dom'); const weUtils = require('web_editor.utils'); var options = require('web_editor.snippets.options'); const wUtils = require('website.utils'); require('website.s_popup_options'); var _t = core._t; var qweb = core.qweb; const InputUserValueWidget = options.userValueWidgetsRegistry['we-input']; const SelectUserValueWidget = options.userValueWidgetsRegistry['we-select']; const UrlPickerUserValueWidget = InputUserValueWidget.extend({ custom_events: _.extend({}, InputUserValueWidget.prototype.custom_events || {}, { 'website_url_chosen': '_onWebsiteURLChosen', }), events: _.extend({}, InputUserValueWidget.prototype.events || {}, { 'click .o_we_redirect_to': '_onRedirectTo', }), /** * @override */ start: async function () { await this._super(...arguments); const linkButton = document.createElement('we-button'); const icon = document.createElement('i'); icon.classList.add('fa', 'fa-fw', 'fa-external-link') linkButton.classList.add('o_we_redirect_to'); linkButton.title = _t("Redirect to URL in a new tab"); linkButton.appendChild(icon); this.containerEl.appendChild(linkButton); this.el.classList.add('o_we_large_input'); this.inputEl.classList.add('text-left'); const options = { position: { collision: 'flip fit', }, classes: { "ui-autocomplete": 'o_website_ui_autocomplete' }, } wUtils.autocompleteWithPages(this, $(this.inputEl), options); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Called when the autocomplete change the input value. * * @private * @param {OdooEvent} ev */ _onWebsiteURLChosen: function (ev) { this._value = this.inputEl.value; this._onUserValueChange(ev); }, /** * Redirects to the URL the widget currently holds. * * @private */ _onRedirectTo: function () { if (this._value) { window.open(this._value, '_blank'); } }, }); const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({ xmlDependencies: (SelectUserValueWidget.prototype.xmlDependencies || []) .concat(['/website/static/src/xml/website.editor.xml']), events: _.extend({}, SelectUserValueWidget.prototype.events || {}, { 'click .o_we_add_google_font_btn': '_onAddGoogleFontClick', 'click .o_we_delete_google_font_btn': '_onDeleteGoogleFontClick', }), fontVariables: [], // Filled by editor menu when all options are loaded /** * @override */ start: async function () { const style = window.getComputedStyle(document.documentElement); const nbFonts = parseInt(weUtils.getCSSVariableValue('number-of-fonts', style)); const googleFontsProperty = weUtils.getCSSVariableValue('google-fonts', style); this.googleFonts = googleFontsProperty ? googleFontsProperty.split(/\s*,\s*/g) : []; this.googleFonts = this.googleFonts.map(font => font.substring(1, font.length - 1)); // Unquote await this._super(...arguments); const fontEls = []; const methodName = this.el.dataset.methodName || 'customizeWebsiteVariable'; const variable = this.el.dataset.variable; _.times(nbFonts, fontNb => { const realFontNb = fontNb + 1; const fontEl = document.createElement('we-button'); fontEl.classList.add(`o_we_option_font_${realFontNb}`); fontEl.dataset.variable = variable; fontEl.dataset[methodName] = weUtils.getCSSVariableValue(`font-number-${realFontNb}`, style); fontEl.dataset.font = realFontNb; fontEls.push(fontEl); this.menuEl.appendChild(fontEl); }); if (this.googleFonts.length) { const googleFontsEls = fontEls.slice(-this.googleFonts.length); googleFontsEls.forEach((el, index) => { $(el).append(core.qweb.render('website.delete_google_font_btn', { index: index, })); }); } $(this.menuEl).append($(core.qweb.render('website.add_google_font_btn', { variable: variable, }))); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * @override */ async setValue() { await this._super(...arguments); for (const className of this.menuTogglerEl.classList) { if (className.match(/^o_we_option_font_\d+$/)) { this.menuTogglerEl.classList.remove(className); } } const activeWidget = this._userValueWidgets.find(widget => !widget.isPreviewed() && widget.isActive()); if (activeWidget) { this.menuTogglerEl.classList.add(`o_we_option_font_${activeWidget.el.dataset.font}`); } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private */ _onAddGoogleFontClick: function (ev) { const variable = $(ev.currentTarget).data('variable'); const dialog = new Dialog(this, { title: _t("Add a Google Font"), $content: $(core.qweb.render('website.dialog.addGoogleFont')), buttons: [ { text: _t("Save & Reload"), classes: 'btn-primary', click: async () => { const inputEl = dialog.el.querySelector('.o_input_google_font'); // if font page link (what is expected) let m = inputEl.value.match(/\bspecimen\/([\w+]+)/); if (!m) { // if embed code (so that it works anyway if the user put the embed code instead of the page link) m = inputEl.value.match(/\bfamily=([\w+]+)/); if (!m) { inputEl.classList.add('is-invalid'); return; } } let isValidFamily = false; try { const result = await fetch("https://fonts.googleapis.com/css?family=" + m[1], {method: 'HEAD'}); // Google fonts server returns a 400 status code if family is not valid. if (result.ok) { isValidFamily = true; } } catch (error) { console.error(error); } if (!isValidFamily) { inputEl.classList.add('is-invalid'); return; } const font = m[1].replace(/\+/g, ' '); this.googleFonts.push(font); this.trigger_up('google_fonts_custo_request', { values: {[variable]: `'${font}'`}, googleFonts: this.googleFonts, }); }, }, { text: _t("Discard"), close: true, }, ], }); dialog.open(); }, /** * @private * @param {Event} ev */ _onDeleteGoogleFontClick: async function (ev) { ev.preventDefault(); const save = await new Promise(resolve => { Dialog.confirm(this, _t("Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?"), { confirm_callback: () => resolve(true), cancel_callback: () => resolve(false), }); }); if (!save) { return; } // Remove Google font const googleFontIndex = parseInt(ev.target.dataset.fontIndex); const googleFont = this.googleFonts[googleFontIndex]; this.googleFonts.splice(googleFontIndex, 1); // Adapt font variable indexes to the removal const values = {}; const style = window.getComputedStyle(document.documentElement); _.each(FontFamilyPickerUserValueWidget.prototype.fontVariables, variable => { const value = weUtils.getCSSVariableValue(variable, style); if (value.substring(1, value.length - 1) === googleFont) { // If an element is using the google font being removed, reset // it to the theme default. values[variable] = 'null'; } }); this.trigger_up('google_fonts_custo_request', { values: values, googleFonts: this.googleFonts, }); }, }); const GPSPicker = InputUserValueWidget.extend({ events: { // Explicitely not consider all InputUserValueWidget events 'blur input': '_onInputBlur', }, /** * @constructor */ init() { this._super(...arguments); this._gmapCacheGPSToPlace = {}; }, /** * @override */ async willStart() { await this._super(...arguments); this._gmapLoaded = await new Promise(resolve => { this.trigger_up('gmap_api_request', { editableMode: true, configureIfNecessary: true, onSuccess: key => resolve(!!key), }); }); if (!this._gmapLoaded) { this.trigger_up('user_value_widget_critical'); return; } }, /** * @override */ async start() { await this._super(...arguments); this.el.classList.add('o_we_large_input'); if (!this._gmapLoaded) { return; } this._gmapAutocomplete = new google.maps.places.Autocomplete(this.inputEl, {types: ['geocode']}); google.maps.event.addListener(this._gmapAutocomplete, 'place_changed', this._onPlaceChanged.bind(this)); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * @override */ getMethodsParams: function (methodName) { return Object.assign({gmapPlace: this._gmapPlace || {}}, this._super(...arguments)); }, /** * @override */ async setValue() { await this._super(...arguments); await new Promise(resolve => { const gps = this._value; if (this._gmapCacheGPSToPlace[gps]) { this._gmapPlace = this._gmapCacheGPSToPlace[gps]; resolve(); return; } const service = new google.maps.places.PlacesService(document.createElement('div')); const p = gps.substring(1).slice(0, -1).split(','); const location = new google.maps.LatLng(p[0] || 0, p[1] || 0); service.nearbySearch({ // Do a 'nearbySearch' followed by 'getDetails' to avoid using // GMap Geocoder which the user may not have enabled... but // ideally Geocoder should be used to get the exact location at // those coordinates and to limit billing query count. location: location, radius: 1, }, (results, status) => { const GMAP_CRITICAL_ERRORS = [google.maps.places.PlacesServiceStatus.REQUEST_DENIED, google.maps.places.PlacesServiceStatus.UNKNOWN_ERROR]; if (status === google.maps.places.PlacesServiceStatus.OK) { service.getDetails({ placeId: results[0].place_id, fields: ['geometry', 'formatted_address'], }, (place, status) => { resolve(); if (status === google.maps.places.PlacesServiceStatus.OK) { this._gmapCacheGPSToPlace[gps] = place; this._gmapPlace = place; } else if (GMAP_CRITICAL_ERRORS.includes(status)) { this.trigger_up('user_value_widget_critical'); } }); } else if (GMAP_CRITICAL_ERRORS.includes(status)) { resolve(); this.trigger_up('user_value_widget_critical'); } }); }); if (this._gmapPlace) { this.inputEl.value = this._gmapPlace.formatted_address; } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {Event} ev */ _onPlaceChanged(ev) { const gmapPlace = this._gmapAutocomplete.getPlace(); if (gmapPlace && gmapPlace.geometry) { this._gmapPlace = gmapPlace; const location = this._gmapPlace.geometry.location; this._value = `(${location.lat()},${location.lng()})`; this._gmapCacheGPSToPlace[this._value] = gmapPlace; this._onUserValueChange(ev); } }, }); options.userValueWidgetsRegistry['we-urlpicker'] = UrlPickerUserValueWidget; options.userValueWidgetsRegistry['we-fontfamilypicker'] = FontFamilyPickerUserValueWidget; options.userValueWidgetsRegistry['we-gpspicker'] = GPSPicker; //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: options.Class.include({ xmlDependencies: (options.Class.prototype.xmlDependencies || []) .concat(['/website/static/src/xml/website.editor.xml']), custom_events: _.extend({}, options.Class.prototype.custom_events || {}, { 'google_fonts_custo_request': '_onGoogleFontsCustoRequest', }), //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * @see this.selectClass for parameters */ customizeWebsiteViews: async function (previewMode, widgetValue, params) { await this._customizeWebsite(previewMode, widgetValue, params, 'views'); }, /** * @see this.selectClass for parameters */ customizeWebsiteVariable: async function (previewMode, widgetValue, params) { await this._customizeWebsite(previewMode, widgetValue, params, 'variable'); }, /** * @see this.selectClass for parameters */ customizeWebsiteColor: async function (previewMode, widgetValue, params) { await this._customizeWebsite(previewMode, widgetValue, params, 'color'); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ async _checkIfWidgetsUpdateNeedReload(widgets) { const needReload = await this._super(...arguments); if (needReload) { return needReload; } for (const widget of widgets) { const methodsNames = widget.getMethodsNames(); if (!methodsNames.includes('customizeWebsiteViews') && !methodsNames.includes('customizeWebsiteVariable') && !methodsNames.includes('customizeWebsiteColor')) { continue; } let paramsReload = false; if (widget.getMethodsParams('customizeWebsiteViews').reload || widget.getMethodsParams('customizeWebsiteVariable').reload || widget.getMethodsParams('customizeWebsiteColor').reload) { paramsReload = true; } if (paramsReload || config.isDebug('assets')) { return (config.isDebug('assets') ? _t("It appears you are in debug=assets mode, all theme customization options require a page reload in this mode.") : true); } } return false; }, /** * @override */ _computeWidgetState: async function (methodName, params) { switch (methodName) { case 'customizeWebsiteViews': { const allXmlIDs = this._getXMLIDsFromPossibleValues(params.possibleValues); const enabledXmlIDs = await this._rpc({ route: '/website/theme_customize_get', params: { 'xml_ids': allXmlIDs, }, }); let mostXmlIDsStr = ''; let mostXmlIDsNb = 0; for (const xmlIDsStr of params.possibleValues) { const enableXmlIDs = xmlIDsStr.split(/\s*,\s*/); if (enableXmlIDs.length > mostXmlIDsNb && enableXmlIDs.every(xmlID => enabledXmlIDs.includes(xmlID))) { mostXmlIDsStr = xmlIDsStr; mostXmlIDsNb = enableXmlIDs.length; } } return mostXmlIDsStr; // Need to return the exact same string as in possibleValues } case 'customizeWebsiteVariable': { return weUtils.getCSSVariableValue(params.variable); } case 'customizeWebsiteColor': { // TODO adapt in master const bugfixedValue = weUtils.getCSSVariableValue(`bugfixed-${params.color}`); if (bugfixedValue) { return bugfixedValue; } return weUtils.getCSSVariableValue(params.color); } } return this._super(...arguments); }, /** * @private */ _customizeWebsite: async function (previewMode, widgetValue, params, type) { // Never allow previews for theme customizations if (previewMode) { return; } switch (type) { case 'views': await this._customizeWebsiteViews(widgetValue, params); break; case 'variable': await this._customizeWebsiteVariable(widgetValue, params); break; case 'color': await this._customizeWebsiteColor(widgetValue, params); break; } if (params.reload || config.isDebug('assets')) { // Caller will reload the page, nothing needs to be done anymore. return; } // Finally, only update the bundles as no reload is required await this._reloadBundles(); // Some public widgets may depend on the variables that were // customized, so we have to restart them *all*. await new Promise((resolve, reject) => { this.trigger_up('widgets_start_request', { editableMode: true, onSuccess: () => resolve(), onFailure: () => reject(), }); }); }, /** * @private */ _customizeWebsiteColor: async function (color, params) { const baseURL = '/website/static/src/scss/options/colors/'; const colorType = params.colorType ? (params.colorType + '_') : ''; const url = `${baseURL}user_${colorType}color_palette.scss`; if (color) { if (weUtils.isColorCombinationName(color)) { color = parseInt(color); } else if (!ColorpickerWidget.isCSSColor(color)) { color = `'${color}'`; } } return this._makeSCSSCusto(url, {[params.color]: color}); }, /** * @private */ _customizeWebsiteVariable: async function (value, params) { return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', { [params.variable]: value, }); }, /** * @private */ _customizeWebsiteViews: async function (xmlID, params) { const allXmlIDs = this._getXMLIDsFromPossibleValues(params.possibleValues); const enableXmlIDs = xmlID.split(/\s*,\s*/); const disableXmlIDs = allXmlIDs.filter(xmlID => !enableXmlIDs.includes(xmlID)); return this._rpc({ route: '/website/theme_customize', params: { 'enable': enableXmlIDs, 'disable': disableXmlIDs, }, }); }, /** * @private */ _getXMLIDsFromPossibleValues: function (possibleValues) { const allXmlIDs = []; for (const xmlIDsStr of possibleValues) { allXmlIDs.push(...xmlIDsStr.split(/\s*,\s*/)); } return allXmlIDs.filter((v, i, arr) => arr.indexOf(v) === i); }, /** * @private */ _makeSCSSCusto: async function (url, values) { return this._rpc({ route: '/website/make_scss_custo', params: { 'url': url, 'values': _.mapObject(values, v => v || 'null'), }, }); }, /** * Refreshes all public widgets related to the given element. * * @private * @param {jQuery} [$el=this.$target] * @returns {Promise} */ _refreshPublicWidgets: async function ($el) { return new Promise((resolve, reject) => { this.trigger_up('widgets_start_request', { editableMode: true, $target: $el || this.$target, onSuccess: resolve, onFailure: reject, }); }); }, /** * @private */ _reloadBundles: async function () { const bundles = await this._rpc({ route: '/website/theme_customize_bundle_reload', }); let $allLinks = $(); const proms = _.map(bundles, (bundleURLs, bundleName) => { var $links = $('link[href*="' + bundleName + '"]'); $allLinks = $allLinks.add($links); var $newLinks = $(); _.each(bundleURLs, url => { $newLinks = $newLinks.add($('', { type: 'text/css', rel: 'stylesheet', href: url, })); }); const linksLoaded = new Promise(resolve => { let nbLoaded = 0; $newLinks.on('load error', () => { // If we have an error, just ignore it if (++nbLoaded >= $newLinks.length) { resolve(); } }); }); $links.last().after($newLinks); return linksLoaded; }); await Promise.all(proms).then(() => $allLinks.remove()); }, /** * @override */ _select: async function (previewMode, widget) { await this._super(...arguments); if (!widget.$el.closest('[data-no-widget-refresh="true"]').length) { // TODO the flag should be retrieved through widget params somehow await this._refreshPublicWidgets(); } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {OdooEvent} ev */ _onGoogleFontsCustoRequest: function (ev) { const values = ev.data.values ? _.clone(ev.data.values) : {}; const googleFonts = ev.data.googleFonts; if (googleFonts.length) { values['google-fonts'] = "('" + googleFonts.join("', '") + "')"; } else { values['google-fonts'] = 'null'; } this.trigger_up('snippet_edition_request', {exec: async () => { return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values); }}); this.trigger_up('request_save', { reloadEditor: true, }); }, }); function _getLastPreFilterLayerElement($el) { // Make sure parallax and video element are considered to be below the // color filters / shape const $bgVideo = $el.find('> .o_bg_video_container'); if ($bgVideo.length) { return $bgVideo[0]; } const $parallaxEl = $el.find('> .s_parallax_bg'); if ($parallaxEl.length) { return $parallaxEl[0]; } return null; } options.registry.BackgroundToggler.include({ /** * Toggles background video on or off. * * @see this.selectClass for parameters */ toggleBgVideo(previewMode, widgetValue, params) { if (!widgetValue) { // TODO: use setWidgetValue instead of calling background directly when possible const [bgVideoWidget] = this._requestUserValueWidgets('bg_video_opt'); const bgVideoOpt = bgVideoWidget.getParent(); return bgVideoOpt._setBgVideo(false, ''); } else { // TODO: use trigger instead of el.click when possible this._requestUserValueWidgets('bg_video_opt')[0].el.click(); } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ _computeWidgetState(methodName, params) { if (methodName === 'toggleBgVideo') { return this.$target[0].classList.contains('o_background_video'); } return this._super(...arguments); }, /** * TODO an overall better management of background layers is needed * * @override */ _getLastPreFilterLayerElement() { const el = _getLastPreFilterLayerElement(this.$target); if (el) { return el; } return this._super(...arguments); }, }); options.registry.BackgroundShape.include({ /** * TODO need a better management of background layers * * @override */ _getLastPreShapeLayerElement() { const el = this._super(...arguments); if (el) { return el; } return _getLastPreFilterLayerElement(this.$target); } }); options.registry.BackgroundVideo = options.Class.extend({ //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * Sets the target's background video. * * @see this.selectClass for parameters */ background: function (previewMode, widgetValue, params) { if (previewMode === 'reset' && this.videoSrc) { return this._setBgVideo(false, this.videoSrc); } return this._setBgVideo(previewMode, widgetValue); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ _computeWidgetState: function (methodName, params) { if (methodName === 'background') { if (this.$target[0].classList.contains('o_background_video')) { return this.$('> .o_bg_video_container iframe').attr('src'); } return ''; } return this._super(...arguments); }, /** * Updates the background video used by the snippet. * * @private * @see this.selectClass for parameters * @returns {Promise} */ _setBgVideo: async function (previewMode, value) { this.$('> .o_bg_video_container').toggleClass('d-none', previewMode === true); if (previewMode !== false) { return; } this.videoSrc = value; var target = this.$target[0]; target.classList.toggle('o_background_video', !!(value && value.length)); if (value && value.length) { target.dataset.bgVideoSrc = value; } else { delete target.dataset.bgVideoSrc; } await this._refreshPublicWidgets(); }, }); options.registry.OptionsTab = options.Class.extend({ //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * @see this.selectClass for parameters */ async configureApiKey(previewMode, widgetValue, params) { return new Promise(resolve => { this.trigger_up('gmap_api_key_request', { editableMode: true, reconfigure: true, onSuccess: () => resolve(), }); }); }, /** * @see this.selectClass for parameters */ async customizeBodyBgType(previewMode, widgetValue, params) { if (widgetValue === 'NONE') { this.bodyImageType = 'image'; return this.customizeBodyBg(previewMode, '', params); } // TODO improve: hack to click on external image picker this.bodyImageType = widgetValue; const widget = this._requestUserValueWidgets(params.imagepicker)[0]; widget.enable(); }, /** * @override */ async customizeBodyBg(previewMode, widgetValue, params) { // TODO improve: customize two variables at the same time... await this.customizeWebsiteVariable(previewMode, this.bodyImageType, {variable: 'body-image-type'}); await this.customizeWebsiteVariable(previewMode, widgetValue ? `'${widgetValue}'` : '', {variable: 'body-image'}); }, /** * @see this.selectClass for parameters */ async openCustomCodeDialog(previewMode, widgetValue, params) { const libsProm = this._loadLibs({ jsLibs: [ '/web/static/lib/ace/ace.js', '/web/static/lib/ace/mode-xml.js', ], }); let websiteId; this.trigger_up('context_get', { callback: (ctx) => { websiteId = ctx['website_id']; }, }); let website; const dataProm = this._rpc({ model: 'website', method: 'read', args: [[websiteId], ['custom_code_head', 'custom_code_footer']], }).then(websites => { website = websites[0]; }); let fieldName, title, contentText; if (widgetValue === 'head') { fieldName = 'custom_code_head'; title = _t('Custom head code'); contentText = _t('Enter code that will be added into the
of every page of your site.'); } else { fieldName = 'custom_code_footer'; title = _t('Custom end of body code'); contentText = _t('Enter code that will be added before the of every page of your site.'); } await Promise.all([libsProm, dataProm]); await new Promise(resolve => { const $content = $(core.qweb.render('website.custom_code_dialog_content', { contentText, })); const aceEditor = this._renderAceEditor($content.find('.o_ace_editor_container')[0], website[fieldName] || ''); const dialog = new Dialog(this, { title, $content, buttons: [ { text: _t("Save"), classes: 'btn-primary', click: async () => { await this._rpc({ model: 'website', method: 'write', args: [ [websiteId], {[fieldName]: aceEditor.getValue()}, ], }); }, close: true, }, { text: _t("Discard"), close: true, }, ], }); dialog.on('closed', this, resolve); dialog.open(); }); }, /** * @see this.selectClass for parameters */ async switchTheme(previewMode, widgetValue, params) { const save = await new Promise(resolve => { Dialog.confirm(this, _t("Changing theme requires to leave the editor. This will save all your changes, are you sure you want to proceed? Be careful that changing the theme will reset all your color customizations."), { confirm_callback: () => resolve(true), cancel_callback: () => resolve(false), }); }); if (!save) { return; } this.trigger_up('request_save', { reload: false, onSuccess: () => window.location.href = '/web#action=website.theme_install_kanban_action', }); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ async _checkIfWidgetsUpdateNeedWarning(widgets) { const warningMessage = await this._super(...arguments); if (warningMessage) { return warningMessage; } for (const widget of widgets) { if (widget.getMethodsNames().includes('customizeWebsiteVariable') && widget.getMethodsParams('customizeWebsiteVariable').variable === 'color-palettes-number') { const hasCustomizedColors = weUtils.getCSSVariableValue('has-customized-colors'); if (hasCustomizedColors && hasCustomizedColors !== 'false') { return _t("Changing the color palette will reset all your color customizations, are you sure you want to proceed?"); } } } return ''; }, /** * @override */ async _computeWidgetState(methodName, params) { if (methodName === 'customizeBodyBgType') { const bgImage = $('#wrapwrap').css('background-image'); if (bgImage === 'none') { return "NONE"; } return weUtils.getCSSVariableValue('body-image-type'); } return this._super(...arguments); }, /** * @override */ async _computeWidgetVisibility(widgetName, params) { if (widgetName === 'body_bg_image_opt') { return false; } return this._super(...arguments); }, /** * @private * @param {DOMElement} node * @param {String} content text of the editor * @returns {Object} */ _renderAceEditor(node, content) { const aceEditor = window.ace.edit(node); aceEditor.setTheme('ace/theme/monokai'); aceEditor.setValue(content, 1); aceEditor.setOptions({ minLines: 20, maxLines: Infinity, showPrintMargin: false, }); aceEditor.renderer.setOptions({ highlightGutterLine: true, showInvisibles: true, fontSize: 14, }); const aceSession = aceEditor.getSession(); aceSession.setOptions({ mode: "ace/mode/xml", useWorker: false, }); return aceEditor; }, /** * @override */ async _renderCustomXML(uiFragment) { uiFragment.querySelectorAll('we-colorpicker').forEach(el => { el.dataset.lazyPalette = 'true'; }); }, }); options.registry.ThemeColors = options.registry.OptionsTab.extend({ /** * @override */ async start() { // Checks for support of the old color system const style = window.getComputedStyle(document.documentElement); const supportOldColorSystem = weUtils.getCSSVariableValue('support-13-0-color-system', style) === 'true'; const hasCustomizedOldColorSystem = weUtils.getCSSVariableValue('has-customized-13-0-color-system', style) === 'true'; this._showOldColorSystemWarning = supportOldColorSystem && hasCustomizedOldColorSystem; return this._super(...arguments); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * @override */ async updateUIVisibility() { await this._super(...arguments); const oldColorSystemEl = this.el.querySelector('.o_old_color_system_warning'); oldColorSystemEl.classList.toggle('d-none', !this._showOldColorSystemWarning); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ async _renderCustomXML(uiFragment) { const paletteSelectorEl = uiFragment.querySelector('[data-variable="color-palettes-number"]'); const style = window.getComputedStyle(document.documentElement); const nbPalettes = parseInt(weUtils.getCSSVariableValue('number-of-color-palettes', style)); for (let i = 1; i <= nbPalettes; i++) { const btnEl = document.createElement('we-button'); btnEl.classList.add('o_palette_color_preview_button'); btnEl.dataset.customizeWebsiteVariable = i; for (let c = 1; c <= 5; c++) { const colorPreviewEl = document.createElement('span'); colorPreviewEl.classList.add('o_palette_color_preview'); const color = weUtils.getCSSVariableValue(`o-palette-${i}-o-color-${c}`, style); colorPreviewEl.style.backgroundColor = color; btnEl.appendChild(colorPreviewEl); } paletteSelectorEl.appendChild(btnEl); } for (let i = 1; i <= 5; i++) { const collapseEl = document.createElement('we-collapse'); const ccPreviewEl = $(qweb.render('web_editor.color.combination.preview'))[0]; ccPreviewEl.classList.add('text-center', `o_cc${i}`); collapseEl.appendChild(ccPreviewEl); const editionEls = $(qweb.render('website.color_combination_edition', {number: i})); for (const el of editionEls) { collapseEl.appendChild(el); } uiFragment.appendChild(collapseEl); } await this._super(...arguments); }, }); options.registry.menu_data = options.Class.extend({ /** * When the users selects a menu, a dialog is opened to ask him if he wants * to follow the link (and leave editor), edit the menu or do nothing. * * @override */ onFocus: function () { var self = this; (new Dialog(this, { title: _t("Confirmation"), $content: $(core.qweb.render('website.leaving_current_page_edition')), buttons: [ {text: _t("Go to Link"), classes: 'btn-primary', click: function () { self.trigger_up('request_save', { reload: false, onSuccess: function () { window.location.href = self.$target.attr('href'); }, }); }}, {text: _t("Edit the menu"), classes: 'btn-primary', click: function () { this.trigger_up('action_demand', { actionName: 'edit_menu', params: [ function () { var prom = new Promise(function (resolve, reject) { self.trigger_up('request_save', { onSuccess: resolve, onFailure: reject, }); }); return prom; }, ], }); }}, {text: _t("Stay on this page"), close: true} ] })).open(); }, }); options.registry.company_data = options.Class.extend({ /** * Fetches data to determine the URL where the user can edit its company * data. Saves the info in the prototype to do this only once. * * @override */ start: function () { var proto = options.registry.company_data.prototype; var prom; var self = this; if (proto.__link === undefined) { prom = this._rpc({route: '/web/session/get_session_info'}).then(function (session) { return self._rpc({ model: 'res.users', method: 'read', args: [session.uid, ['company_id']], }); }).then(function (res) { proto.__link = '/web#action=base.action_res_company_form&view_type=form&id=' + (res && res[0] && res[0].company_id[0] || 1); }); } return Promise.all([this._super.apply(this, arguments), prom]); }, /** * When the users selects company data, opens a dialog to ask him if he * wants to be redirected to the company form view to edit it. * * @override */ onFocus: function () { var self = this; var proto = options.registry.company_data.prototype; Dialog.confirm(this, _t("Do you want to edit the company data ?"), { confirm_callback: function () { self.trigger_up('request_save', { reload: false, onSuccess: function () { window.location.href = proto.__link; }, }); }, }); }, }); options.registry.Carousel = options.Class.extend({ /** * @override */ start: function () { this.$target.carousel('pause'); this.$indicators = this.$target.find('.carousel-indicators'); this.$controls = this.$target.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators'); // Prevent enabling the carousel overlay when clicking on the carousel // controls (indeed we want it to change the carousel slide then enable // the slide overlay) + See "CarouselItem" option. this.$controls.addClass('o_we_no_overlay'); let _slideTimestamp; this.$target.on('slide.bs.carousel.carousel_option', () => { _slideTimestamp = window.performance.now(); setTimeout(() => this.trigger_up('hide_overlay')); }); this.$target.on('slid.bs.carousel.carousel_option', () => { // slid.bs.carousel is most of the time fired too soon by bootstrap // since it emulates the transitionEnd with a setTimeout. We wait // here an extra 20% of the time before retargeting edition, which // should be enough... const _slideDuration = (window.performance.now() - _slideTimestamp); setTimeout(() => { this.trigger_up('activate_snippet', { $snippet: this.$target.find('.carousel-item.active'), ifInactiveOptions: true, }); this.$target.trigger('active_slide_targeted'); }, 0.2 * _slideDuration); }); return this._super.apply(this, arguments); }, /** * @override */ destroy: function () { this._super.apply(this, arguments); this.$target.off('.carousel_option'); }, /** * @override */ onBuilt: function () { this._assignUniqueID(); }, /** * @override */ onClone: function () { this._assignUniqueID(); }, /** * @override */ cleanForSave: function () { const $items = this.$target.find('.carousel-item'); $items.removeClass('next prev left right active').first().addClass('active'); this.$indicators.find('li').removeClass('active').empty().first().addClass('active'); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Creates a unique ID for the carousel and reassign data-attributes that * depend on it. * * @private */ _assignUniqueID: function () { const id = 'myCarousel' + Date.now(); this.$target.attr('id', id); this.$target.find('[data-target]').attr('data-target', '#' + id); _.each(this.$target.find('[data-slide], [data-slide-to]'), function (el) { var $el = $(el); if ($el.attr('data-target')) { $el.attr('data-target', '#' + id); } else if ($el.attr('href')) { $el.attr('href', '#' + id); } }); }, }); options.registry.CarouselItem = options.Class.extend({ isTopOption: true, forceNoDeleteButton: true, /** * @override */ start: function () { this.$carousel = this.$target.closest('.carousel'); this.$indicators = this.$carousel.find('.carousel-indicators'); this.$controls = this.$carousel.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators'); var leftPanelEl = this.$overlay.data('$optionsSection')[0]; var titleTextEl = leftPanelEl.querySelector('we-title > span'); this.counterEl = document.createElement('span'); titleTextEl.appendChild(this.counterEl); return this._super(...arguments); }, /** * @override */ destroy: function () { this._super(...arguments); this.$carousel.off('.carousel_item_option'); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Updates the slide counter. * * @override */ updateUI: async function () { await this._super(...arguments); const $items = this.$carousel.find('.carousel-item'); const $activeSlide = $items.filter('.active'); const updatedText = ` (${$activeSlide.index() + 1}/${$items.length})`; this.counterEl.textContent = updatedText; }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * Adds a slide. * * @see this.selectClass for parameters */ addSlide: function (previewMode) { const $items = this.$carousel.find('.carousel-item'); this.$controls.removeClass('d-none'); this.$indicators.append($('