mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-20 00:22:03 +02:00
Initial commit: Mail packages
This commit is contained in:
commit
4e53507711
1948 changed files with 751201 additions and 0 deletions
|
|
@ -0,0 +1,185 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Many2OneField } from '@web/views/fields/many2one/many2one_field';
|
||||
import Domain from 'web.Domain';
|
||||
|
||||
const { useState, useEffect } = owl;
|
||||
|
||||
export class MailingFilterDropdown extends Dropdown {
|
||||
setup() {
|
||||
super.setup();
|
||||
useEffect((inputFilterEl) => {
|
||||
if (inputFilterEl) {
|
||||
inputFilterEl.focus();
|
||||
}
|
||||
}, () => [document.querySelector('.o_mass_mailing_filter_name')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget to create / remove favorite filters on mass mailing and/or marketing automation, extended
|
||||
* from Many2OneField. This widget is designed specifically for 'mailing_filter_id'
|
||||
* field on 'mailing.mailing' and 'marketing.campaign' form view.
|
||||
*
|
||||
* In edit mode, it will allow to save the latest configured domain
|
||||
* in form of the favorite filter, or to remove the store filters.
|
||||
*
|
||||
*/
|
||||
export class FieldMany2OneMailingFilter extends Many2OneField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.notification = useService("notification");
|
||||
this.filter = useState({
|
||||
canSaveFilter: false,
|
||||
});
|
||||
useEffect(() => this._updateFilterIcons());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the 'Add to favorite' / 'Remove' icons' visibility based on the
|
||||
* current state, and shows the custom message when no filter is available
|
||||
* for the selected model.
|
||||
*
|
||||
* The filter can be saved if one of those conditions is matched:
|
||||
* - No favorite filter is currently set
|
||||
* - User emptied the input
|
||||
* - User changed the domain when favorite filter is set
|
||||
* - The input is currently being edited, known by the "this.state.isFloating" variable
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateFilterIcons() {
|
||||
const el = document.querySelector('.o_mass_mailing_filter_container');
|
||||
if (!el || this.props.readonly) {
|
||||
return;
|
||||
}
|
||||
const filterCount = this.props.record.data.mailing_filter_count;
|
||||
const dropdown = document.querySelector('.o_field_mailing_filter > .o_field_many2one_selection > .o_input_dropdown')
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('d-none', !filterCount);
|
||||
}
|
||||
// By default, domains in recordData are in string format, but adding / removing a leaf from domain widget converts
|
||||
// value into object, so we use 'Domain' class to convert them in same (string) format, allowing proper comparison.
|
||||
let recordDomain;
|
||||
let filterDomain;
|
||||
try {
|
||||
recordDomain = new Domain(this.props.record.data[this.props.domain_field] || []).toString();
|
||||
filterDomain = new Domain(this.props.record.data.mailing_filter_domain || []).toString();
|
||||
} catch {
|
||||
// Don't raise a traceback if a domain set manually doesn't match the format expected.
|
||||
// This can happen when we unfocus the domain editor
|
||||
this.filter.canSaveFilter = false;
|
||||
this.filter.canRemoveFilter = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFieldElement = this.props.model_field && document.querySelector(
|
||||
`input#${this.props.model_field},div [name="${this.props.model_field}"]`);
|
||||
|
||||
let value = "";
|
||||
if (modelFieldElement && modelFieldElement.tagName === "span") {
|
||||
value = modelFieldElement.textContent;
|
||||
} else if (modelFieldElement && modelFieldElement.tagName === "input") {
|
||||
value = modelFieldElement.value;
|
||||
}
|
||||
|
||||
el.classList.toggle('d-none', recordDomain === '[]');
|
||||
this.filter.canSaveFilter = !this.props.record.data.mailing_filter_id
|
||||
|| value.length
|
||||
|| this.state.isFloating
|
||||
|| filterDomain !== recordDomain;
|
||||
this.filter.canRemoveFilter = !this.filter.canSaveFilter
|
||||
}
|
||||
|
||||
// HANDLERS
|
||||
|
||||
/**
|
||||
* Focus the 'Save' button on 'Tab' key, or directly save the filter on 'Enter'
|
||||
*
|
||||
* @param {event} ev
|
||||
*/
|
||||
onFilterNameInputKeydown(ev) {
|
||||
const btnSave = document.querySelector('.o_mass_mailing_btn_save_filter');
|
||||
if (ev.key === 'Tab') {
|
||||
ev.preventDefault();
|
||||
btnSave.focus();
|
||||
} else if (ev.key === 'Enter') {
|
||||
btnSave.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the saved filter, but we do not reset the applied domain
|
||||
* in this case.
|
||||
*
|
||||
* @param {event} ev
|
||||
*/
|
||||
async onRemoveFilter(ev) {
|
||||
const filterId = this.props.record.data.mailing_filter_id[0];
|
||||
const mailingDomain = this.props.record.data[this.props.domain_field];
|
||||
// Prevent multiple clicks to avoid trying to deleting same record multiple times.
|
||||
ev.target.disabled = true;
|
||||
|
||||
await this.orm.unlink('mailing.filter', [filterId]);
|
||||
this.update([{ id: false, name: false }]);
|
||||
this.props.record.update({[this.props.domain_field]: mailingDomain});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new favorite filter, with the name provided from drop-down and
|
||||
* with the 'up to date' domain. If the input is blank, displays the warning
|
||||
* and keeps the popup open by preventing event propagation.
|
||||
*
|
||||
* Note: We do not disable the save button here to avoid multiple clicks as for the delete,
|
||||
* because as soon as the 'Save' button is clicked, the popup will be closed immediately.
|
||||
*
|
||||
* @param {event} ev
|
||||
*/
|
||||
async onSaveFilter(ev) {
|
||||
const filterInput = document.querySelector('input.o_mass_mailing_filter_name');
|
||||
const filterName = filterInput.value.trim();
|
||||
if (filterName.length === 0) {
|
||||
this.notification.add(
|
||||
this.env._t("Please provide a name for the filter"),
|
||||
{type: 'danger'}
|
||||
);
|
||||
// Keep the drop-down open, and re-focus the input
|
||||
ev.stopPropagation();
|
||||
filterInput.focus();
|
||||
} else {
|
||||
const newFilterId = await this.env.model.orm.create("mailing.filter", [{
|
||||
name: filterName,
|
||||
mailing_domain: this.props.record.data[this.props.domain_field],
|
||||
mailing_model_id: this.props.record.data[this.props.model_field][0],
|
||||
}]);
|
||||
this.update([{ id: newFilterId, name: filterName }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
FieldMany2OneMailingFilter.template = 'mass_mailing.MailingFilter';
|
||||
FieldMany2OneMailingFilter.components = {
|
||||
...Many2OneField.components,
|
||||
MailingFilterDropdown,
|
||||
};
|
||||
FieldMany2OneMailingFilter.props = {
|
||||
...Many2OneField.props,
|
||||
domain_field: { type: String, optional: true },
|
||||
model_field: { type: String, optional: true },
|
||||
};
|
||||
FieldMany2OneMailingFilter.defaultProps = {
|
||||
...Many2OneField.defaultProps,
|
||||
domain_field: "mailing_domain",
|
||||
model_field: "mailing_model_id",
|
||||
};
|
||||
FieldMany2OneMailingFilter.extractProps = ({ field, attrs }) => {
|
||||
return {
|
||||
...Many2OneField.extractProps({ field, attrs }),
|
||||
domain_field: attrs.options.domain_field,
|
||||
model_field: attrs.options.model_field,
|
||||
}
|
||||
};
|
||||
|
||||
registry.category('fields').add('mailing_filter', FieldMany2OneMailingFilter);
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { throttleForAnimation } from "@web/core/utils/timing";
|
||||
|
||||
const {
|
||||
useSubEnv,
|
||||
onMounted,
|
||||
onWillUnmount,
|
||||
} = owl;
|
||||
|
||||
export class MassMailingFullWidthViewController extends formView.Controller {
|
||||
setup() {
|
||||
super.setup();
|
||||
useSubEnv({
|
||||
onIframeUpdated: () => this._updateIframe(),
|
||||
mailingFilterTemplates: true,
|
||||
});
|
||||
this._resizeObserver = new ResizeObserver(throttleForAnimation(() => {
|
||||
this._resizeMailingEditorIframe();
|
||||
this._repositionMailingEditorSidebar();
|
||||
}));
|
||||
onMounted(() => {
|
||||
$('.o_content').on('scroll.repositionMailingEditorSidebar', throttleForAnimation(this._repositionMailingEditorSidebar.bind(this)));
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
$('.o_content').off('.repositionMailingEditorSidebar');
|
||||
this._resizeObserver.disconnect();
|
||||
});
|
||||
}
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* Resize the given iframe so its height fits its contents and initialize a
|
||||
* resize observer to resize on each size change in its contents.
|
||||
* This also ensures the contents of the sidebar remain visible no matter
|
||||
* how much we resize the iframe and scroll down.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} ev.data.$iframe
|
||||
*/
|
||||
_updateIframe() {
|
||||
const $iframe = $('iframe.wysiwyg_iframe:visible, iframe.o_readonly');
|
||||
if (!$iframe.length || !$iframe.contents().length) {
|
||||
return;
|
||||
}
|
||||
const hasIframeChanged = !this.$iframe || !this.$iframe.length || $iframe[0] !== this.$iframe[0];
|
||||
this.$iframe = $iframe;
|
||||
this._resizeMailingEditorIframe();
|
||||
|
||||
const $iframeDoc = $iframe.contents();
|
||||
$iframeDoc.get(0).querySelector('html').classList.add('o_mass_mailing_iframe_full_width');
|
||||
const iframeTarget = $iframeDoc.find('#iframe_target');
|
||||
if (hasIframeChanged) {
|
||||
$iframeDoc.find('body').on('click', '.o_fullscreen_btn', this._onToggleFullscreen.bind(this));
|
||||
if (iframeTarget[0]) {
|
||||
this._resizeObserver.disconnect();
|
||||
this._resizeObserver.observe(iframeTarget[0]);
|
||||
}
|
||||
}
|
||||
if (iframeTarget[0]) {
|
||||
const isFullscreen = this._isFullScreen();
|
||||
iframeTarget.css({
|
||||
display: isFullscreen ? '' : 'flex',
|
||||
'flex-direction': isFullscreen ? '' : 'column',
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reposition the sidebar so it always occupies the full available visible
|
||||
* height, no matter the scroll position. This way, the sidebar is always
|
||||
* visible and as big as possible.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_repositionMailingEditorSidebar() {
|
||||
const windowHeight = $(window).height();
|
||||
const $iframeDocument = this.$iframe.contents();
|
||||
const $sidebar = $iframeDocument.find('#oe_snippets');
|
||||
const isFullscreen = this._isFullScreen();
|
||||
if (isFullscreen) {
|
||||
$sidebar.height(windowHeight);
|
||||
this.$iframe.height(windowHeight);
|
||||
$sidebar.css({
|
||||
top: '',
|
||||
bottom: '',
|
||||
});
|
||||
} else {
|
||||
const iframeTop = this.$iframe.offset().top;
|
||||
$sidebar.css({
|
||||
height: '',
|
||||
top: Math.max(0, $('.o_content').offset().top - iframeTop),
|
||||
bottom: this.$iframe.height() - windowHeight + iframeTop,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Switch "scrolling modes" on toggle fullscreen mode: in fullscreen mode,
|
||||
* the scroll happens within the iframe whereas in regular mode we pretend
|
||||
* there is no iframe and scroll in the top document. Also reposition the
|
||||
* sidebar since toggling the fullscreen mode visibly changes the
|
||||
* positioning of elements in the document.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onToggleFullscreen() {
|
||||
const isFullscreen = this._isFullScreen();
|
||||
const $iframeDoc = this.$iframe.contents();
|
||||
const html = $iframeDoc.find('html').get(0);
|
||||
html.scrollTop = 0;
|
||||
html.classList.toggle('o_fullscreen', isFullscreen);
|
||||
const wysiwyg = $iframeDoc.find('.note-editable').data('wysiwyg');
|
||||
if (wysiwyg && wysiwyg.snippetsMenu) {
|
||||
// Restore the appropriate scrollable depending on the mode.
|
||||
this._$scrollable = this._$scrollable || wysiwyg.snippetsMenu.$scrollable;
|
||||
wysiwyg.snippetsMenu.$scrollable = isFullscreen ? $iframeDoc.find('.note-editable') : this._$scrollable;
|
||||
}
|
||||
this._repositionMailingEditorSidebar();
|
||||
this._resizeMailingEditorIframe();
|
||||
}
|
||||
/**
|
||||
* Return true if the mailing editor is in full screen mode, false
|
||||
* otherwise.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isFullScreen() {
|
||||
return window.top.document.body.classList.contains('o_field_widgetTextHtml_fullscreen');
|
||||
}
|
||||
/**
|
||||
* Resize the mailing editor's iframe container so its height fits its
|
||||
* contents. This needs to be called whenever the iframe's contents might
|
||||
* have changed, eg. when adding/removing content to/from it or when a
|
||||
* template is picked.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_resizeMailingEditorIframe() {
|
||||
const minHeight = $(window).height() - Math.abs(this.$iframe.offset().top);
|
||||
const $iframeDoc = this.$iframe.contents();
|
||||
const $themeSelectorNew = $iframeDoc.find('.o_mail_theme_selector_new');
|
||||
if ($themeSelectorNew.length) {
|
||||
this.$iframe.height(Math.max($themeSelectorNew[0].scrollHeight, minHeight));
|
||||
} else {
|
||||
const ref = $iframeDoc.find('#iframe_target')[0];
|
||||
if (ref) {
|
||||
this.$iframe.css({
|
||||
height: this._isFullScreen()
|
||||
? $(window).height()
|
||||
: Math.max(ref.scrollHeight, minHeight),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const massMailingFormView = {
|
||||
...formView,
|
||||
Controller: MassMailingFullWidthViewController,
|
||||
};
|
||||
|
||||
registry.category("views").add("mailing_mailing_view_form_full_width", massMailingFormView);
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
odoo.define('mass_mailing.unsubscribe', function (require) {
|
||||
'use strict';
|
||||
|
||||
var session = require('web.session');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
require('web.dom_ready');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var email = $("input[name='email']").val();
|
||||
var mailing_id = parseInt($("input[name='mailing_id']").val());
|
||||
var res_id = parseInt($("input[name='res_id']").val());
|
||||
var token = (location.search.split('token' + '=')[1] || '').split('&')[0];
|
||||
|
||||
if (!$('.o_unsubscribe_form').length) {
|
||||
return;
|
||||
}
|
||||
session.load_translations().then(function () {
|
||||
if (email != '' && email != undefined){
|
||||
ajax.jsonRpc('/mailing/blacklist/check', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#button_add_blacklist').hide();
|
||||
$('#button_remove_blacklist').hide();
|
||||
}
|
||||
else if (result == true) {
|
||||
$('#button_remove_blacklist').show();
|
||||
toggle_opt_out_section(false);
|
||||
}
|
||||
else if (result == false) {
|
||||
$('#button_add_blacklist').show();
|
||||
toggle_opt_out_section(true);
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
});
|
||||
}
|
||||
else {
|
||||
$('#div_blacklist').hide();
|
||||
}
|
||||
|
||||
var unsubscribed_list = $("input[name='unsubscribed_list']").val();
|
||||
if (unsubscribed_list){
|
||||
$('#subscription_info').html(_.str.sprintf(
|
||||
_t("You have been <strong>successfully unsubscribed from %s</strong>."),
|
||||
_.escape(unsubscribed_list)
|
||||
));
|
||||
}
|
||||
else{
|
||||
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed</strong>.'));
|
||||
}
|
||||
});
|
||||
|
||||
$('#unsubscribe_form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var checked_ids = [];
|
||||
$("input[type='checkbox']:checked").each(function (i){
|
||||
checked_ids[i] = parseInt($(this).val());
|
||||
});
|
||||
|
||||
var unchecked_ids = [];
|
||||
$("input[type='checkbox']:not(:checked)").each(function (i){
|
||||
unchecked_ids[i] = parseInt($(this).val());
|
||||
});
|
||||
|
||||
ajax.jsonRpc('/mail/mailing/unsubscribe', 'call', {'opt_in_ids': checked_ids, 'opt_out_ids': unchecked_ids, 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_t('You are not authorized to do this!'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
|
||||
}
|
||||
else if (result == true) {
|
||||
$('#subscription_info').text(_t('Your changes have been saved.'));
|
||||
$('#info_state').removeClass('alert-info').addClass('alert-success');
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Your changes have not been saved, try again later.'));
|
||||
$('#info_state').removeClass('alert-info').addClass('alert-warning');
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Your changes have not been saved, try again later.'));
|
||||
$('#info_state').removeClass('alert-info').addClass('alert-warning');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================
|
||||
// Blacklist
|
||||
// ==================
|
||||
$('#button_add_blacklist').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
ajax.jsonRpc('/mailing/blacklist/add', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_t('You are not authorized to do this!'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result) {
|
||||
$('#subscription_info').html(_t('You have been successfully <strong>added to our blacklist</strong>. '
|
||||
+ 'You will not be contacted anymore by our services.'));
|
||||
$('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success');
|
||||
toggle_opt_out_section(false);
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
}
|
||||
$('#button_add_blacklist').hide();
|
||||
$('#button_remove_blacklist').show();
|
||||
$('#unsubscribed_info').hide();
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
});
|
||||
});
|
||||
|
||||
$('#button_remove_blacklist').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
ajax.jsonRpc('/mailing/blacklist/remove', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_t('You are not authorized to do this!'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result) {
|
||||
$('#subscription_info').html(_t("You have been successfully <strong>removed from our blacklist</strong>. "
|
||||
+ "You are now able to be contacted by our services."));
|
||||
$('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success');
|
||||
toggle_opt_out_section(true);
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
}
|
||||
$('#button_add_blacklist').show();
|
||||
$('#button_remove_blacklist').hide();
|
||||
$('#unsubscribed_info').hide();
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================
|
||||
// Feedback
|
||||
// ==================
|
||||
$('#button_feedback').click(function (e) {
|
||||
var feedback = $("textarea[name='opt_out_feedback']").val();
|
||||
e.preventDefault();
|
||||
ajax.jsonRpc('/mailing/feedback', 'call', {'mailing_id': mailing_id, 'res_id': res_id, 'email': email, 'feedback': feedback, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_t('You are not authorized to do this!'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
|
||||
}
|
||||
else if (result == true){
|
||||
$('#subscription_info').text(_t('Thank you! Your feedback has been sent successfully!'));
|
||||
$('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success');
|
||||
$("#div_feedback").hide();
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-info').removeClass('alert-success').removeClass('alert-error').addClass('alert-warning');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function toggle_opt_out_section(value) {
|
||||
var result = !value;
|
||||
$("#div_opt_out").find('*').attr('disabled',result);
|
||||
$("#button_add_blacklist").attr('disabled', false);
|
||||
$("#button_remove_blacklist").attr('disabled', false);
|
||||
if (value) { $('[name="button_subscription"]').addClass('clickable'); }
|
||||
else { $('[name="button_subscription"]').removeClass('clickable'); }
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
odoo.define('mass_mailing.mass_mailing', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanColumn = require('web.KanbanColumn');
|
||||
|
||||
KanbanColumn.include({
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.modelName === 'mailing.mailing') {
|
||||
this.draggable = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/** @odoo-module alias=mass_mailing.design_constants**/
|
||||
|
||||
export const CSS_PREFIX = '.o_mail_wrapper';
|
||||
|
||||
export const BTN_SIZE_STYLES = {
|
||||
'btn-sm': {
|
||||
'padding': '3px 7.5px',
|
||||
'font-size': '0.875rem',
|
||||
'line-height': '1.5rem',
|
||||
},
|
||||
'btn-lg': {
|
||||
'padding': '7px 14px',
|
||||
'font-size': '1.25rem',
|
||||
'line-height': '1.5rem',
|
||||
},
|
||||
'btn-md': {
|
||||
'padding': false, // Property must be removed.
|
||||
'font-size': '14px',
|
||||
'line-height': false, // Property must be removed.
|
||||
},
|
||||
};
|
||||
export const DEFAULT_BUTTON_SIZE = 'btn-md';
|
||||
export const PRIORITY_STYLES = {
|
||||
'h1': ['font-family'],
|
||||
'h2': ['font-family'],
|
||||
'h3': ['font-family'],
|
||||
'p': ['font-family'],
|
||||
'a:not(.btn)': [],
|
||||
'a.btn.btn-primary': [],
|
||||
'a.btn.btn-secondary': [],
|
||||
'hr': ['border-top-width','border-top-style','border-top-color'],
|
||||
};
|
||||
export const RE_CSS_TEXT_MATCH = /([^{]+)([^}]+)/;
|
||||
export const RE_SELECTOR_ENDS_WITH_GT_STAR = />\s*\*\s*$/;
|
||||
|
||||
export const transformFontFamilySelector = selector => {
|
||||
if (selector.trim().endsWith(':not(.fa)')) {
|
||||
return [selector];
|
||||
}
|
||||
if (!selector.endsWith('*')) {
|
||||
return [`${selector.trim()}:not(.fa)`, `${selector.trim()} :not(.fa)`];
|
||||
} else if (RE_SELECTOR_ENDS_WITH_GT_STAR.test(selector)) {
|
||||
return [`${selector.replace(RE_SELECTOR_ENDS_WITH_GT_STAR, '').trim()} :not(.fa)`];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Take a css text and splits each comma-separated selector into separate
|
||||
* styles, applying the css prefix to each. Return the modified css text.
|
||||
*
|
||||
* @param {string} [css]
|
||||
* @returns {string}
|
||||
*/
|
||||
export const splitCss = css => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = css;
|
||||
// Temporarily insert the style element in the dom to have a stylesheet.
|
||||
document.head.appendChild(styleElement);
|
||||
const rules = [...styleElement.sheet.cssRules];
|
||||
styleElement.remove();
|
||||
const stylesToWrite = {};
|
||||
for (const rule of rules) {
|
||||
const styles = rule.style;
|
||||
for (let selector of rule.selectorText.split(',')) {
|
||||
if (!selector.trim().startsWith(CSS_PREFIX)) {
|
||||
selector = `${CSS_PREFIX} ${selector.trim()}`;
|
||||
}
|
||||
for (const style of rule.style) {
|
||||
let selectors = [selector];
|
||||
if (style === 'font-family') {
|
||||
// Ensure font-family gets passed to all descendants and never
|
||||
// overwrite font awesome.
|
||||
selectors = transformFontFamilySelector(selector);
|
||||
}
|
||||
for (const selectorToWriteTo of selectors) {
|
||||
if (!stylesToWrite[selectorToWriteTo]) {
|
||||
stylesToWrite[selectorToWriteTo] = [];
|
||||
}
|
||||
stylesToWrite[selectorToWriteTo].push([style, styles[style] + (styles.getPropertyPriority(style) === 'important' ? ' !important' : '')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.entries(stylesToWrite).map(([selector, styles]) => (
|
||||
`${selector.trim()} {\n${styles.map(([styleName, style]) => ` ${styleName}: ${style};`).join('\n')}\n}`
|
||||
)).join('\n');
|
||||
};
|
||||
export const getFontName = fontFamily => fontFamily.split(',')[0].replace(/"/g, '').replace(/([a-z])([A-Z])/g, (v, a, b) => `${a} ${b}`).trim();
|
||||
export const normalizeFontFamily = fontFamily => fontFamily.replace(/"/g, '').replace(/, /g, ',');
|
||||
export const initializeDesignTabCss = $editable => {
|
||||
let styleElement = $editable.get(0).ownerDocument.querySelector('#design-element');
|
||||
if (styleElement) {
|
||||
styleElement.textContent = splitCss(styleElement.textContent);
|
||||
} else {
|
||||
// If a style element can't be found, create one and initialize it.
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.setAttribute('id', 'design-element');
|
||||
}
|
||||
// The style element needs to be within the layout of the email in
|
||||
// order to be saved along with it.
|
||||
$editable.find('.o_layout').prepend(styleElement);
|
||||
};
|
||||
|
||||
export const FONT_FAMILIES = [
|
||||
'Arial, "Helvetica Neue", Helvetica, sans-serif', // name: "Arial"
|
||||
'"Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace', // name: "Courier New"
|
||||
'Georgia, Times, "Times New Roman", serif', // name: "Georgia"
|
||||
'"Helvetica Neue", Helvetica, Arial, sans-serif', // name: "Helvetica Neue"
|
||||
'"Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif', // name: "Lucida Grande"
|
||||
'Tahoma, Verdana, Segoe, sans-serif', // name: "Tahoma"
|
||||
'TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif', // name: "Times New Roman"
|
||||
'"Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif', // name: "Trebuchet MS"
|
||||
'Verdana, Geneva, sans-serif', // name: "Verdana"
|
||||
].map(fontFamily => normalizeFontFamily(fontFamily));
|
||||
|
||||
export default {
|
||||
CSS_PREFIX, BTN_SIZE_STYLES, DEFAULT_BUTTON_SIZE, PRIORITY_STYLES,
|
||||
RE_CSS_TEXT_MATCH, FONT_FAMILIES, RE_SELECTOR_ENDS_WITH_GT_STAR,
|
||||
splitCss, getFontName, normalizeFontFamily, initializeDesignTabCss,
|
||||
transformFontFamilySelector,
|
||||
}
|
||||
|
|
@ -0,0 +1,671 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { initializeDesignTabCss } from "mass_mailing.design_constants";
|
||||
import { toInline } from "web_editor.convertInline";
|
||||
import { loadBundle } from "@web/core/assets";
|
||||
import { qweb } from 'web.core';
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { buildQuery } from "web.rpc";
|
||||
import { HtmlField } from "@web_editor/js/backend/html_field";
|
||||
import { getWysiwygClass } from 'web_editor.loader';
|
||||
import { device } from 'web.config';
|
||||
import { MassMailingMobilePreviewDialog } from "./mass_mailing_mobile_preview";
|
||||
import { getRangePosition } from '@web_editor/js/editor/odoo-editor/src/utils/utils';
|
||||
|
||||
const {
|
||||
useSubEnv,
|
||||
onWillUpdateProps,
|
||||
status,
|
||||
} = owl;
|
||||
|
||||
export class MassMailingHtmlField extends HtmlField {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
useSubEnv({
|
||||
onWysiwygReset: this._resetIframe.bind(this),
|
||||
});
|
||||
this.action = useService('action');
|
||||
this.rpc = useService('rpc');
|
||||
this.dialog = useService('dialog');
|
||||
|
||||
onWillUpdateProps(() => {
|
||||
if (this.props.record.data.mailing_model_id && this.wysiwyg) {
|
||||
this._hideIrrelevantTemplates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get wysiwygOptions() {
|
||||
return {
|
||||
...super.wysiwygOptions,
|
||||
onIframeUpdated: () => this.onIframeUpdated(),
|
||||
snippets: 'mass_mailing.email_designer_snippets',
|
||||
resizable: false,
|
||||
defaultDataForLinkTools: { isNewWindow: true },
|
||||
toolbarTemplate: 'mass_mailing.web_editor_toolbar',
|
||||
onWysiwygBlur: () => {
|
||||
this.commitChanges();
|
||||
this.wysiwyg.odooEditor.toolbarHide();
|
||||
},
|
||||
...this.props.wysiwygOptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} popover
|
||||
* @param {Object} position
|
||||
* @override
|
||||
*/
|
||||
positionDynamicPlaceholder(popover, position) {
|
||||
const editable = this.wysiwyg.$iframe ? this.wysiwyg.$iframe[0] : this.wysiwyg.$editable[0];
|
||||
const relativeParentPosition = editable.getBoundingClientRect();
|
||||
|
||||
let topPosition = relativeParentPosition.top;
|
||||
let leftPosition = relativeParentPosition.left;
|
||||
|
||||
const rangePosition = getRangePosition(popover, this.wysiwyg.options.document);
|
||||
topPosition += rangePosition.top;
|
||||
// Offset the popover to ensure the arrow is pointing at
|
||||
// the precise range location.
|
||||
leftPosition += rangePosition.left - 14;
|
||||
|
||||
// Apply the position back to the element.
|
||||
popover.style.top = topPosition + 'px';
|
||||
popover.style.left = leftPosition + 'px';
|
||||
}
|
||||
|
||||
async commitChanges(...args) {
|
||||
if (this.props.readonly || !this.isRendered) {
|
||||
return super.commitChanges(...args);
|
||||
}
|
||||
if (!this._isDirty() || this._pendingCommitChanges) {
|
||||
// In case there is still a pending change while committing the
|
||||
// changes from the save button, we need to wait for the previous
|
||||
// operation to finish, otherwise the "inline field" of the mass
|
||||
// mailing might not be saved.
|
||||
return this._pendingCommitChanges;
|
||||
}
|
||||
this._pendingCommitChanges = (async () => {
|
||||
const codeViewEl = this._getCodeViewEl();
|
||||
if (codeViewEl) {
|
||||
this.wysiwyg.setValue(this._getCodeViewValue(codeViewEl));
|
||||
}
|
||||
|
||||
if (this.wysiwyg.$iframeBody.find('.o_basic_theme').length) {
|
||||
this.wysiwyg.$iframeBody.find('*').css('font-family', '');
|
||||
}
|
||||
|
||||
const $editable = this.wysiwyg.getEditable();
|
||||
this.wysiwyg.odooEditor.historyPauseSteps();
|
||||
await this.wysiwyg.cleanForSave();
|
||||
if (args.length) {
|
||||
await super.commitChanges({ ...args[0], urgent: true });
|
||||
} else {
|
||||
await super.commitChanges({ urgent: true });
|
||||
}
|
||||
|
||||
const $editorEnable = $editable.closest('.editor_enable');
|
||||
$editorEnable.removeClass('editor_enable');
|
||||
// Prevent history reverts.
|
||||
this.wysiwyg.odooEditor.observerUnactive('toInline');
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.height = '0px';
|
||||
iframe.style.visibility = 'hidden';
|
||||
iframe.setAttribute('sandbox', 'allow-same-origin'); // Make sure no scripts get executed.
|
||||
const clonedHtmlNode = $editable[0].closest('html').cloneNode(true);
|
||||
// Replace the body to only contain the target as we do not care for
|
||||
// other elements (e.g. sidebar, toolbar, ...)
|
||||
const clonedBody = clonedHtmlNode.querySelector('body');
|
||||
const clonedIframeTarget = clonedHtmlNode.querySelector('#iframe_target');
|
||||
clonedBody.replaceChildren(clonedIframeTarget);
|
||||
clonedHtmlNode.querySelectorAll('script').forEach(script => script.remove()); // Remove scripts.
|
||||
iframe.srcdoc = clonedHtmlNode.outerHTML;
|
||||
const iframePromise = new Promise((resolve) => {
|
||||
iframe.addEventListener("load", resolve);
|
||||
});
|
||||
document.body.append(iframe);
|
||||
// Wait for the css and images to be loaded.
|
||||
await iframePromise;
|
||||
const editableClone = iframe.contentDocument.querySelector('.note-editable');
|
||||
// The jQuery data are lost because of the cloning operations above.
|
||||
// The hacky fix for stable is to simply add it back manually.
|
||||
// TODO in master: Update toInline to use an options parameter.
|
||||
$(editableClone).data("wysiwyg", this.wysiwyg);
|
||||
await toInline($(editableClone), undefined, $(iframe));
|
||||
iframe.remove();
|
||||
this.wysiwyg.odooEditor.observerActive('toInline');
|
||||
const inlineHtml = editableClone.innerHTML;
|
||||
$editorEnable.addClass('editor_enable');
|
||||
this.wysiwyg.odooEditor.historyUnpauseSteps();
|
||||
this.wysiwyg.odooEditor.historyRevertCurrentStep();
|
||||
|
||||
const fieldName = this.props.inlineField;
|
||||
await this.props.record.update({[fieldName]: this._unWrap(inlineHtml)});
|
||||
this._pendingCommitChanges = null;
|
||||
})();
|
||||
return this._pendingCommitChanges;
|
||||
}
|
||||
async startWysiwyg(...args) {
|
||||
await super.startWysiwyg(...args);
|
||||
|
||||
await loadBundle({
|
||||
jsLibs: [
|
||||
'/mass_mailing/static/src/js/mass_mailing_link_dialog_fix.js',
|
||||
'/mass_mailing/static/src/js/mass_mailing_snippets.js',
|
||||
'/mass_mailing/static/src/snippets/s_masonry_block/options.js',
|
||||
'/mass_mailing/static/src/snippets/s_media_list/options.js',
|
||||
'/mass_mailing/static/src/snippets/s_showcase/options.js',
|
||||
'/mass_mailing/static/src/snippets/s_rating/options.js',
|
||||
],
|
||||
});
|
||||
|
||||
if (status(this) === "destroyed") {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._resetIframe();
|
||||
}
|
||||
|
||||
async _resetIframe() {
|
||||
if (this._switchingTheme) {
|
||||
return;
|
||||
}
|
||||
this.wysiwyg.$iframeBody.find('.o_mail_theme_selector_new').remove();
|
||||
await this._onSnippetsLoaded();
|
||||
|
||||
// Data is removed on save but we need the mailing and its body to be
|
||||
// named so they are handled properly by the snippets menu.
|
||||
this.wysiwyg.$iframeBody.find('.o_layout').addBack().data('name', 'Mailing');
|
||||
// We don't want to drop snippets directly within the wysiwyg.
|
||||
this.wysiwyg.$iframeBody.find('.odoo-editor-editable').removeClass('o_editable');
|
||||
|
||||
initializeDesignTabCss(this.wysiwyg.getEditable());
|
||||
this.wysiwyg.getEditable().find('img').attr('loading', '');
|
||||
|
||||
this.wysiwyg.odooEditor.observerFlush();
|
||||
this.wysiwyg.odooEditor.historyReset();
|
||||
this.wysiwyg.$iframeBody.addClass('o_mass_mailing_iframe');
|
||||
|
||||
this.onIframeUpdated();
|
||||
}
|
||||
|
||||
async _onSnippetsLoaded() {
|
||||
if (this.wysiwyg.snippetsMenu && $(window.top.document).find('.o_mass_mailing_form_full_width')[0]) {
|
||||
// In full width form mode, ensure the snippets menu's scrollable is
|
||||
// in the form view, not in the iframe.
|
||||
this.wysiwyg.snippetsMenu.$scrollable = this.wysiwyg.$el.closestScrollable();
|
||||
// Ensure said scrollable keeps its scrollbar at all times to
|
||||
// prevent the scrollbar from appearing at awkward moments (ie: when
|
||||
// previewing an option)
|
||||
this.wysiwyg.snippetsMenu.$scrollable.css('overflow-y', 'scroll');
|
||||
}
|
||||
|
||||
// Remove the web editor menu to avoid flicker (we add it back at the
|
||||
// end of the method)
|
||||
this.wysiwyg.$iframeBody.find('.iframe-utils-zone').addClass('d-none');
|
||||
|
||||
// Filter the fetched templates based on the current model
|
||||
const args = this.props.filterTemplates
|
||||
? [[['mailing_model_id', '=', this.props.record.data.mailing_model_id[0]]]]
|
||||
: [];
|
||||
|
||||
const rpcQuery = buildQuery({
|
||||
model: 'mailing.mailing',
|
||||
method: 'action_fetch_favorites',
|
||||
args: args,
|
||||
})
|
||||
// Templates taken from old mailings
|
||||
const result = await this.rpc(rpcQuery.route, rpcQuery.params);
|
||||
const templatesParams = result.map(values => {
|
||||
return {
|
||||
id: values.id,
|
||||
modelId: values.mailing_model_id[0],
|
||||
modelName: values.mailing_model_id[1],
|
||||
name: `template_${values.id}`,
|
||||
nowrap: true,
|
||||
subject: values.subject,
|
||||
template: values.body_arch,
|
||||
userId: values.user_id[0],
|
||||
userName: values.user_id[1],
|
||||
};
|
||||
});
|
||||
|
||||
const $snippetsSideBar = this.wysiwyg.snippetsMenu.$el;
|
||||
const $themes = $snippetsSideBar.find("#email_designer_themes").children();
|
||||
const $snippets = $snippetsSideBar.find(".oe_snippet");
|
||||
const selectorToKeep = '.o_we_external_history_buttons, .email_designer_top_actions';
|
||||
// Overide `d-flex` class which style is `!important`
|
||||
$snippetsSideBar.find(`.o_we_website_top_actions > *:not(${selectorToKeep})`).attr('style', 'display: none!important');
|
||||
|
||||
if (!odoo.debug) {
|
||||
$snippetsSideBar.find('.o_codeview_btn').hide();
|
||||
}
|
||||
const $codeview = this.wysiwyg.$iframe.contents().find('textarea.o_codeview');
|
||||
// Unbind first the event handler as this method can be called multiple time during the component life.
|
||||
$snippetsSideBar.off('click', '.o_codeview_btn');
|
||||
$snippetsSideBar.on('click', '.o_codeview_btn', () => {
|
||||
this.wysiwyg.odooEditor.observerUnactive();
|
||||
$codeview.toggleClass('d-none');
|
||||
this.wysiwyg.getEditable().toggleClass('d-none');
|
||||
this.wysiwyg.odooEditor.observerActive();
|
||||
|
||||
if ($codeview.hasClass('d-none')) {
|
||||
this.wysiwyg.setValue(this._getCodeViewValue($codeview[0]));
|
||||
} else {
|
||||
$codeview.val(this.wysiwyg.getValue());
|
||||
}
|
||||
this.wysiwyg.snippetsMenu.activateSnippet(false);
|
||||
this.onIframeUpdated();
|
||||
});
|
||||
const $previewBtn = $snippetsSideBar.find('.o_mobile_preview_btn');
|
||||
$previewBtn.off('click');
|
||||
$previewBtn.on('click', () => {
|
||||
$previewBtn.prop('disabled', true); // Prevent double execution when double-clicking on the button
|
||||
let mailingHtml = new DOMParser().parseFromString(this.wysiwyg.getValue(), 'text/html');
|
||||
[...mailingHtml.querySelectorAll('a')].forEach(el => {
|
||||
el.style.setProperty('pointer-events', 'none');
|
||||
});
|
||||
this.mobilePreview = this.dialog.add(MassMailingMobilePreviewDialog, {
|
||||
title: this.env._t("Mobile Preview"),
|
||||
preview: mailingHtml.body.innerHTML,
|
||||
}, {
|
||||
onClose: () => $previewBtn.prop('disabled', false),
|
||||
});
|
||||
});
|
||||
|
||||
if (!this._themeParams) {
|
||||
// Initialize theme parameters.
|
||||
this._themeClassNames = "";
|
||||
const displayableThemes =
|
||||
device.isMobile ?
|
||||
_.filter($themes, theme => !$(theme).data("hideFromMobile")) :
|
||||
$themes;
|
||||
this._themeParams = _.map(displayableThemes, (theme) => {
|
||||
const $theme = $(theme);
|
||||
const name = $theme.data("name");
|
||||
const classname = "o_" + name + "_theme";
|
||||
this._themeClassNames += " " + classname;
|
||||
const imagesInfo = _.defaults($theme.data("imagesInfo") || {}, {
|
||||
all: {}
|
||||
});
|
||||
for (const info of Object.values(imagesInfo)) {
|
||||
_.defaults(info, imagesInfo.all, {
|
||||
module: "mass_mailing",
|
||||
format: "jpg"
|
||||
});
|
||||
}
|
||||
return {
|
||||
name: name,
|
||||
title: $theme.attr("title") || "",
|
||||
className: classname || "",
|
||||
img: $theme.data("img") || "",
|
||||
template: $theme.html().trim(),
|
||||
nowrap: !!$theme.data('nowrap'),
|
||||
get_image_info: function (filename) {
|
||||
if (imagesInfo[filename]) {
|
||||
return imagesInfo[filename];
|
||||
}
|
||||
return imagesInfo.all;
|
||||
},
|
||||
layoutStyles: $theme.data('layout-styles'),
|
||||
};
|
||||
});
|
||||
}
|
||||
$themes.parent().remove();
|
||||
|
||||
if (!this._themeParams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const themesParams = [...this._themeParams];
|
||||
|
||||
// Create theme selection screen and check if it must be forced opened.
|
||||
// Reforce it opened if the last snippet is removed.
|
||||
const $themeSelectorNew = $(qweb.render("mass_mailing.theme_selector_new", {
|
||||
themes: themesParams,
|
||||
templates: templatesParams,
|
||||
modelName: this.props.record.data.mailing_model_id[1] || '',
|
||||
}));
|
||||
|
||||
// Check if editable area is empty.
|
||||
const $layout = this.wysiwyg.$iframeBody.find(".o_layout");
|
||||
let $mailWrapper = $layout.children(".o_mail_wrapper");
|
||||
let $mailWrapperContent = $mailWrapper.find('.o_mail_wrapper_td');
|
||||
if (!$mailWrapperContent.length) {
|
||||
$mailWrapperContent = $mailWrapper;
|
||||
}
|
||||
let value;
|
||||
if ($mailWrapperContent.length > 0) {
|
||||
value = $mailWrapperContent.html();
|
||||
} else if ($layout.length) {
|
||||
value = $layout.html();
|
||||
} else {
|
||||
value = this.wysiwyg.getValue();
|
||||
}
|
||||
let blankEditable = "<p><br></p>";
|
||||
const editableAreaIsEmpty = value === "" || value === blankEditable;
|
||||
|
||||
if (editableAreaIsEmpty) {
|
||||
// unfold to prevent toolbar from going over the menu
|
||||
this.wysiwyg.setSnippetsMenuFolded(false);
|
||||
$themeSelectorNew.appendTo(this.wysiwyg.$iframeBody);
|
||||
}
|
||||
|
||||
$themeSelectorNew.on('click', '.dropdown-item', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const themeName = $(e.currentTarget).attr('id');
|
||||
|
||||
const themeParams = [...themesParams, ...templatesParams].find(theme => theme.name === themeName);
|
||||
|
||||
await this._switchThemes(themeParams);
|
||||
this.wysiwyg.$iframeBody.closest('body').removeClass("o_force_mail_theme_choice");
|
||||
|
||||
$themeSelectorNew.remove();
|
||||
|
||||
this.wysiwyg.setSnippetsMenuFolded(device.isMobile || themeName === 'basic');
|
||||
|
||||
this._switchImages(themeParams, $snippets);
|
||||
|
||||
const $editable = this.wysiwyg.$editable.find('.o_editable');
|
||||
this.$editorMessageElements = $editable
|
||||
.not('[data-editor-message]')
|
||||
.attr('data-editor-message', this.env._t('DRAG BUILDING BLOCKS HERE'));
|
||||
$editable.filter(':empty').attr('contenteditable', false);
|
||||
|
||||
// Wait the next tick because some mutation have to be processed by
|
||||
// the Odoo editor before resetting the history.
|
||||
setTimeout(() => {
|
||||
this.wysiwyg.historyReset();
|
||||
// Update undo/redo buttons
|
||||
this.wysiwyg.odooEditor.dispatchEvent(new Event('historyStep'));
|
||||
|
||||
// The selection has been lost when switching theme.
|
||||
const document = this.wysiwyg.odooEditor.document;
|
||||
const selection = document.getSelection();
|
||||
const p = this.wysiwyg.odooEditor.editable.querySelector('p');
|
||||
if (p) {
|
||||
const range = document.createRange();
|
||||
range.setStart(p, 0);
|
||||
range.setEnd(p, 0);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
// mark selection done for tour testing
|
||||
$editable.addClass('theme_selection_done');
|
||||
this.onIframeUpdated();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Remove the mailing from the favorites list
|
||||
$themeSelectorNew.on('click', '.o_mail_template_preview i.o_mail_template_remove_favorite', async (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const $target = $(ev.currentTarget);
|
||||
const mailingId = $target.data('id');
|
||||
|
||||
const rpcQuery = buildQuery({
|
||||
model: 'mailing.mailing',
|
||||
method: 'action_remove_favorite',
|
||||
args: [mailingId],
|
||||
})
|
||||
const action = await this.rpc(rpcQuery.route, rpcQuery.params);
|
||||
|
||||
this.action.doAction(action);
|
||||
|
||||
$target.parents('.o_mail_template_preview').remove();
|
||||
});
|
||||
|
||||
// Clear any previous theme class before adding new one.
|
||||
this.wysiwyg.$iframeBody.closest('body').removeClass(this._themeClassNames);
|
||||
let selectedTheme = this._getSelectedTheme(themesParams);
|
||||
if (selectedTheme) {
|
||||
this.wysiwyg.$iframeBody.closest('body').addClass(selectedTheme.className);
|
||||
this._switchImages(selectedTheme, $snippets);
|
||||
} else if (this.wysiwyg.$iframeBody.find('.o_layout').length) {
|
||||
themesParams.push({
|
||||
name: 'o_mass_mailing_no_theme',
|
||||
className: 'o_mass_mailing_no_theme',
|
||||
img: "",
|
||||
template: this.wysiwyg.$iframeBody.find('.o_layout').addClass('o_mass_mailing_no_theme').clone().find('oe_structure').empty().end().html().trim(),
|
||||
nowrap: true,
|
||||
get_image_info: function () {}
|
||||
});
|
||||
selectedTheme = this._getSelectedTheme(themesParams);
|
||||
}
|
||||
|
||||
this.wysiwyg.setSnippetsMenuFolded(device.isMobile || (selectedTheme && selectedTheme.name === 'basic'));
|
||||
|
||||
this.wysiwyg.$iframeBody.find('.iframe-utils-zone').removeClass('d-none');
|
||||
if (this.env.mailingFilterTemplates && this.wysiwyg) {
|
||||
this._hideIrrelevantTemplates();
|
||||
}
|
||||
this.wysiwyg.odooEditor.activateContenteditable();
|
||||
}
|
||||
_getCodeViewEl() {
|
||||
const codeView = this.wysiwyg &&
|
||||
this.wysiwyg.$iframe &&
|
||||
this.wysiwyg.$iframe.contents().find('textarea.o_codeview')[0];
|
||||
return codeView && !codeView.classList.contains('d-none') && codeView;
|
||||
}
|
||||
/**
|
||||
* The .o_mail_wrapper_td element is where snippets can be dropped into.
|
||||
* This getter wraps the codeview value in such element in case it got
|
||||
* removed during edition in the codeview, in order to preserve the snippets
|
||||
* dropping functionality.
|
||||
*/
|
||||
_getCodeViewValue(codeViewEl) {
|
||||
const editable = this.wysiwyg.$editable[0];
|
||||
const initialDropZone = editable.querySelector('.o_mail_wrapper_td');
|
||||
if (initialDropZone) {
|
||||
const parsedHtml = new DOMParser().parseFromString(codeViewEl.value, "text/html");
|
||||
if (!parsedHtml.querySelector('.o_mail_wrapper_td')) {
|
||||
initialDropZone.replaceChildren(...parsedHtml.body.childNodes);
|
||||
return editable.innerHTML;
|
||||
}
|
||||
}
|
||||
return codeViewEl.value;
|
||||
}
|
||||
/**
|
||||
* This method will take the model in argument and will hide all mailing template
|
||||
* in the mass mailing widget that do not belong to this model.
|
||||
*
|
||||
* This will also update the help message in the same widget to include the
|
||||
* new model name.
|
||||
*
|
||||
* @param {Number} modelId
|
||||
* @param {String} modelName
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_hideIrrelevantTemplates() {
|
||||
const iframeContent = this.wysiwyg.$iframe.contents();
|
||||
|
||||
const mailing_model_id = this.props.record.data.mailing_model_id[0];
|
||||
iframeContent
|
||||
.find(`.o_mail_template_preview[model-id!="${mailing_model_id}"]`)
|
||||
.addClass('d-none')
|
||||
.removeClass('d-inline-block');
|
||||
|
||||
const sameModelTemplates = iframeContent
|
||||
.find(`.o_mail_template_preview[model-id="${mailing_model_id}"]`);
|
||||
|
||||
sameModelTemplates
|
||||
.removeClass('d-none')
|
||||
.addClass('d-inline-block');
|
||||
|
||||
// Hide or show the help message and preview wrapper based on whether there are any relevant templates
|
||||
if (sameModelTemplates.length) {
|
||||
iframeContent.find('.o_mailing_template_message').addClass('d-none');
|
||||
iframeContent.find('.o_mailing_template_preview_wrapper').removeClass('d-none');
|
||||
} else {
|
||||
iframeContent.find('.o_mailing_template_message').removeClass('d-none');
|
||||
iframeContent.find('.o_mailing_template_message span').text(this.props.record.data.mailing_model_id[1]);
|
||||
iframeContent.find('.o_mailing_template_preview_wrapper').addClass('d-none');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the selected theme, if any.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} themesParams
|
||||
* @returns {false|Object}
|
||||
*/
|
||||
_getSelectedTheme(themesParams) {
|
||||
const $layout = this.wysiwyg.$iframeBody.find(".o_layout");
|
||||
let selectedTheme = false;
|
||||
if ($layout.length !== 0) {
|
||||
_.each(themesParams, function (themeParams) {
|
||||
if ($layout.hasClass(themeParams.className)) {
|
||||
selectedTheme = themeParams;
|
||||
}
|
||||
});
|
||||
}
|
||||
return selectedTheme;
|
||||
}
|
||||
/**
|
||||
* Swap the previous theme's default images with the new ones.
|
||||
* (Redefine the `src` attribute of all images in a $container, depending on the theme parameters.)
|
||||
*
|
||||
* @private
|
||||
* @param {Object} themeParams
|
||||
* @param {JQuery} $container
|
||||
*/
|
||||
_switchImages(themeParams, $container) {
|
||||
if (!themeParams) {
|
||||
return;
|
||||
}
|
||||
for (const img of $container.find("img")) {
|
||||
const $img = $(img);
|
||||
const src = $img.attr("src");
|
||||
|
||||
let m = src.match(/^\/web\/image\/\w+\.s_default_image_(?:theme_[a-z]+_)?(.+)$/);
|
||||
if (!m) {
|
||||
m = src.match(/^\/\w+\/static\/src\/img\/(?:theme_[a-z]+\/)?s_default_image_(.+)\.[a-z]+$/);
|
||||
}
|
||||
if (!m) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeParams.get_image_info) {
|
||||
const file = m[1];
|
||||
const imgInfo = themeParams.get_image_info(file);
|
||||
|
||||
const src = imgInfo.format
|
||||
? `/${imgInfo.module}/static/src/img/theme_${themeParams.name}/s_default_image_${file}.${imgInfo.format}`
|
||||
: `/web/image/${imgInfo.module}.s_default_image_theme_${themeParams.name}_${file}`;
|
||||
|
||||
$img.attr('src', src);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Switch themes or import first theme.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} themeParams
|
||||
*/
|
||||
async _switchThemes(themeParams) {
|
||||
if (!themeParams || this.switchThemeLast === themeParams) {
|
||||
return;
|
||||
}
|
||||
this.switchThemeLast = themeParams;
|
||||
|
||||
this.wysiwyg.$iframeBody.closest('body').removeClass(this._themeClassNames).addClass(themeParams.className);
|
||||
|
||||
const old_layout = this.wysiwyg.$editable.find('.o_layout')[0];
|
||||
|
||||
let $newWrapper;
|
||||
let $newWrapperContent;
|
||||
if (themeParams.nowrap) {
|
||||
$newWrapper = $('<div/>', {
|
||||
class: 'oe_structure'
|
||||
});
|
||||
$newWrapperContent = $newWrapper;
|
||||
} else {
|
||||
// This wrapper structure is the only way to have a responsive
|
||||
// and centered fixed-width content column on all mail clients
|
||||
$newWrapper = $('<div/>', {
|
||||
class: 'container o_mail_wrapper o_mail_regular oe_unremovable',
|
||||
});
|
||||
$newWrapperContent = $('<div/>', {
|
||||
class: 'col o_mail_no_options o_mail_wrapper_td bg-white oe_structure o_editable'
|
||||
});
|
||||
$newWrapper.append($('<div class="row"/>').append($newWrapperContent));
|
||||
}
|
||||
const $newLayout = $('<div/>', {
|
||||
class: 'o_layout oe_unremovable oe_unmovable bg-200 ' + themeParams.className,
|
||||
style: themeParams.layoutStyles,
|
||||
'data-name': 'Mailing',
|
||||
}).append($newWrapper);
|
||||
|
||||
const $contents = themeParams.template;
|
||||
$newWrapperContent.append($contents);
|
||||
this._switchImages(themeParams, $newWrapperContent);
|
||||
old_layout && old_layout.remove();
|
||||
this.wysiwyg.odooEditor.resetContent($newLayout[0].outerHTML);
|
||||
|
||||
$newWrapperContent.find('*').addBack()
|
||||
.contents()
|
||||
.filter(function () {
|
||||
return this.nodeType === 3 && this.textContent.match(/\S/);
|
||||
}).parent().addClass('o_default_snippet_text');
|
||||
|
||||
if (themeParams.name === 'basic') {
|
||||
this.wysiwyg.$editable[0].focus();
|
||||
}
|
||||
initializeDesignTabCss(this.wysiwyg.$editable);
|
||||
this.wysiwyg.trigger('reload_snippet_dropzones');
|
||||
this.onIframeUpdated();
|
||||
this.wysiwyg.odooEditor.historyStep(true);
|
||||
// The value of the field gets updated upon editor blur. If for any
|
||||
// reason, the selection was not in the editable before modifying
|
||||
// another field, ensure that the value is properly set.
|
||||
this._switchingTheme = true;
|
||||
await this.commitChanges();
|
||||
this._switchingTheme = false;
|
||||
}
|
||||
async _getWysiwygClass() {
|
||||
return getWysiwygClass({moduleName: 'mass_mailing.wysiwyg'});
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _setupReadonlyIframe() {
|
||||
if (!this.props.value.length) {
|
||||
this.props.value = this.props.record.data.body_html;
|
||||
}
|
||||
await super._setupReadonlyIframe();
|
||||
}
|
||||
}
|
||||
|
||||
MassMailingHtmlField.props = {
|
||||
...standardFieldProps,
|
||||
...HtmlField.props,
|
||||
filterTemplates: { type: Boolean, optional: true },
|
||||
inlineField: { type: String, optional: true },
|
||||
iframeHtmlClass: { type: String, optional: true },
|
||||
};
|
||||
|
||||
MassMailingHtmlField.displayName = _lt("Email");
|
||||
MassMailingHtmlField.extractProps = (...args) => {
|
||||
const [{ attrs }] = args;
|
||||
const htmlProps = HtmlField.extractProps(...args);
|
||||
return {
|
||||
...htmlProps,
|
||||
filterTemplates: attrs.options.filterTemplates,
|
||||
inlineField: attrs.options['inline-field'],
|
||||
iframeHtmlClass: attrs['iframeHtmlClass'],
|
||||
};
|
||||
};
|
||||
MassMailingHtmlField.fieldDependencies = {
|
||||
body_html: { type: 'html' },
|
||||
};
|
||||
|
||||
registry.category("fields").add("mass_mailing_html", MassMailingHtmlField);
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
odoo.define('mass_mailing.fix.LinkDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
const LinkDialog = require('wysiwyg.widgets.LinkDialog');
|
||||
|
||||
/**
|
||||
* Primary and link buttons are "hacked" by mailing themes scss. We thus
|
||||
* have to fix their preview if possible.
|
||||
*/
|
||||
LinkDialog.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start() {
|
||||
const ret = this._super(...arguments);
|
||||
if (!$(this.editable).find('.o_mail_wrapper').length) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
this.opened().then(() => {
|
||||
// Ugly hack to show the real color for link and primary which
|
||||
// depend on the mailing themes. Note: the hack is not enough as
|
||||
// the mailing theme changes those colors in some environment,
|
||||
// sometimes (for example 'btn-primary in this snippet looks like
|
||||
// that')... we'll consider this a limitation until a master
|
||||
// refactoring of those mailing themes.
|
||||
this.__realMMColors = {};
|
||||
const $previewArea = $('<div/>').addClass('o_mail_snippet_general');
|
||||
$(this.editable).find('.o_layout').append($previewArea);
|
||||
_.each(['link', 'primary', 'secondary'], type => {
|
||||
const $el = $('<a href="#" class="btn btn-' + type + '"/>');
|
||||
$el.appendTo($previewArea);
|
||||
this.__realMMColors[type] = {
|
||||
'border-color': $el.css('border-top-color'),
|
||||
'background-color': $el.css('background-color'),
|
||||
'color': $el.css('color'),
|
||||
};
|
||||
$el.remove();
|
||||
|
||||
this.$('label > .o_btn_preview.btn-' + type)
|
||||
.css(_.pick(this.__realMMColors[type], 'background-color', 'color'));
|
||||
});
|
||||
$previewArea.remove();
|
||||
|
||||
this._adaptPreview();
|
||||
});
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_adaptPreview() {
|
||||
this._super(...arguments);
|
||||
if (this.__realMMColors) {
|
||||
var $preview = this.$("#link-preview");
|
||||
$preview.css('border-color', '');
|
||||
$preview.css('background-color', '');
|
||||
$preview.css('color', '');
|
||||
_.each(['link', 'primary', 'secondary'], type => {
|
||||
if ($preview.hasClass('btn-' + type) || type === 'link' && !$preview.hasClass('btn')) {
|
||||
$preview.css(this.__realMMColors[type]);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
const { useEffect, onWillStart } = owl;
|
||||
|
||||
export class MassMailingMobilePreviewDialog extends Dialog {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.rpc = useService("rpc");
|
||||
onWillStart(async () => {
|
||||
this.styleSheets = await this.rpc("/mailing/get_preview_assets");
|
||||
});
|
||||
useEffect((modalEl) => {
|
||||
if (modalEl) {
|
||||
const modalBody = modalEl.querySelector('.modal-body');
|
||||
const invertIcon = document.createElement("span");
|
||||
invertIcon.className = "fa fa-refresh";
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.srcdoc = this._getSourceDocument();
|
||||
|
||||
modalEl.classList.add('o_mailing_mobile_preview');
|
||||
modalEl.querySelector('.modal-title').append(invertIcon);
|
||||
modalEl.querySelector('.modal-header').addEventListener('click', () => modalBody.classList.toggle('o_invert_orientation'));
|
||||
modalBody.append(iframe);
|
||||
}
|
||||
}, () => [document.querySelector(':not(.o_inactive_modal).o_dialog')]);
|
||||
}
|
||||
|
||||
_getSourceDocument() {
|
||||
return '<!DOCTYPE html><html>' +
|
||||
'<head>' + this.styleSheets + '</head>' +
|
||||
'<body>' + this.props.preview + '</body>' +
|
||||
'</html>';
|
||||
}
|
||||
}
|
||||
|
||||
MassMailingMobilePreviewDialog.props = {
|
||||
...Dialog.props,
|
||||
preview: { type: String },
|
||||
close: Function,
|
||||
};
|
||||
delete MassMailingMobilePreviewDialog.props.slots;
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
odoo.define('mass_mailing.snippets.options', function (require) {
|
||||
"use strict";
|
||||
|
||||
const options = require('web_editor.snippets.options');
|
||||
const {loadImage} = require('web_editor.image_processing');
|
||||
const {ColorpickerWidget} = require('web.Colorpicker');
|
||||
const SelectUserValueWidget = options.userValueWidgetsRegistry['we-select'];
|
||||
const weUtils = require('web_editor.utils');
|
||||
const {
|
||||
CSS_PREFIX, BTN_SIZE_STYLES,
|
||||
DEFAULT_BUTTON_SIZE, PRIORITY_STYLES, FONT_FAMILIES,
|
||||
getFontName, normalizeFontFamily, initializeDesignTabCss,
|
||||
transformFontFamilySelector,
|
||||
} = require('mass_mailing.design_constants');
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Snippet option for resizing image and column width inline like excel
|
||||
options.registry.mass_mailing_sizing_x = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
|
||||
this.containerWidth = this.$target.parent().closest("td, table, div").width();
|
||||
|
||||
var self = this;
|
||||
var offset, sib_offset, target_width, sib_width;
|
||||
|
||||
this.$overlay.find(".o_handle.e, .o_handle.w").removeClass("readonly");
|
||||
this.isIMG = this.$target.is("img");
|
||||
if (this.isIMG) {
|
||||
this.$overlay.find(".o_handle.w").addClass("readonly");
|
||||
}
|
||||
|
||||
var $body = $(this.ownerDocument.body);
|
||||
this.$overlay.find(".o_handle").on('mousedown', function (event) {
|
||||
event.preventDefault();
|
||||
var $handle = $(this);
|
||||
var compass = false;
|
||||
|
||||
_.each(['n', 's', 'e', 'w'], function (handler) {
|
||||
if ($handle.hasClass(handler)) { compass = handler; }
|
||||
});
|
||||
if (self.isIMG) { compass = "image"; }
|
||||
|
||||
$body.on("mousemove.mass_mailing_width_x", function (event) {
|
||||
event.preventDefault();
|
||||
offset = self.$target.offset().left;
|
||||
target_width = self.get_max_width(self.$target);
|
||||
if (compass === 'e' && self.$target.next().offset()) {
|
||||
sib_width = self.get_max_width(self.$target.next());
|
||||
sib_offset = self.$target.next().offset().left;
|
||||
self.change_width(event, self.$target, target_width, offset, true);
|
||||
self.change_width(event, self.$target.next(), sib_width, sib_offset, false);
|
||||
}
|
||||
if (compass === 'w' && self.$target.prev().offset()) {
|
||||
sib_width = self.get_max_width(self.$target.prev());
|
||||
sib_offset = self.$target.prev().offset().left;
|
||||
self.change_width(event, self.$target, target_width, offset, false);
|
||||
self.change_width(event, self.$target.prev(), sib_width, sib_offset, true);
|
||||
}
|
||||
if (compass === 'image') {
|
||||
const maxWidth = self.$target.closest("div").width();
|
||||
// Equivalent to `self.change_width` but ensuring `maxWidth` is the maximum:
|
||||
self.$target.css("width", Math.min(maxWidth, Math.round(event.pageX - offset)));
|
||||
self.trigger_up('cover_update');
|
||||
}
|
||||
});
|
||||
$body.one("mouseup", function () {
|
||||
$body.off('.mass_mailing_width_x');
|
||||
});
|
||||
});
|
||||
|
||||
return def;
|
||||
},
|
||||
change_width: function (event, target, target_width, offset, grow) {
|
||||
target.css("width", Math.round(grow ? (event.pageX - offset) : (offset + target_width - event.pageX)));
|
||||
this.trigger_up('cover_update');
|
||||
},
|
||||
get_int_width: function (el) {
|
||||
return parseInt($(el).css("width"), 10);
|
||||
},
|
||||
get_max_width: function ($el) {
|
||||
return this.containerWidth - _.reduce(_.map($el.siblings(), this.get_int_width), function (memo, w) { return memo + w; });
|
||||
},
|
||||
onFocus: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (this.$target.is("td, th")) {
|
||||
this.$overlay.find(".o_handle.e, .o_handle.w").toggleClass("readonly", this.$target.siblings().length === 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Adding compatibility for the outlook compliance of mailings.
|
||||
// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
|
||||
options.registry.MassMailingBackgroundImage = options.registry.BackgroundImage.extend({
|
||||
start: function () {
|
||||
this._super();
|
||||
const $table_target = this.$target.find('table:first');
|
||||
if ($table_target.length) {
|
||||
this.$target = $table_target;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
options.registry.MassMailingImageTools = options.registry.ImageTools.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getCSSColorValue(color) {
|
||||
if (!color || ColorpickerWidget.isCSSColor(color)) {
|
||||
return color;
|
||||
}
|
||||
const doc = this.options.document;
|
||||
const tempEl = doc.body.appendChild(doc.createElement('div'));
|
||||
tempEl.className = `bg-${color}`;
|
||||
const colorValue = window.getComputedStyle(tempEl).getPropertyValue("background-color").trim();
|
||||
tempEl.parentNode.removeChild(tempEl);
|
||||
return ColorpickerWidget.normalizeCSSColor(colorValue).replace(/"/g, "'");
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async computeShape(svgText, img) {
|
||||
const dataURL = await this._super(...arguments);
|
||||
const image = await loadImage(dataURL);
|
||||
const canvas = document.createElement("canvas");
|
||||
const imgFilename = (img.dataset.originalSrc.split("/").pop()).split(".")[0];
|
||||
img.dataset.fileName = `${imgFilename}.png`;
|
||||
img.dataset.mimetype = "image/png";
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
canvas.getContext("2d").drawImage(image, 0, 0, image.width, image.height);
|
||||
return canvas.toDataURL(`image/png`, 1.0);
|
||||
}
|
||||
});
|
||||
|
||||
options.userValueWidgetsRegistry['we-fontfamilypicker'] = SelectUserValueWidget.extend({
|
||||
/**
|
||||
* @override
|
||||
* @see FONT_FAMILIES
|
||||
*/
|
||||
start: async function () {
|
||||
const res = await this._super(...arguments);
|
||||
// Populate the `we-select` with the font family buttons
|
||||
for (const fontFamily of FONT_FAMILIES) {
|
||||
const button = document.createElement('we-button');
|
||||
button.style.setProperty('font-family', fontFamily);
|
||||
button.dataset.customizeCssProperty = fontFamily;
|
||||
button.dataset.cssProperty = 'font-family';
|
||||
button.dataset.selectorText = this.el.dataset.selectorText;
|
||||
button.textContent = getFontName(fontFamily);
|
||||
this.menuEl.appendChild(button);
|
||||
};
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.DesignTab = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
// Set the target on the whole editable so apply-to looks within it.
|
||||
this.setTarget(this.options.wysiwyg.getEditable());
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async start() {
|
||||
const res = await this._super(...arguments);
|
||||
const $editable = this.options.wysiwyg.getEditable();
|
||||
this.document = $editable[0].ownerDocument;
|
||||
this.$layout = $editable.find('.o_layout');
|
||||
initializeDesignTabCss($editable);
|
||||
this.styleElement = this.document.querySelector('#design-element');
|
||||
// When editing a stylesheet, its content is not updated so it won't be
|
||||
// saved along with the mailing. Therefore we need to write its cssText
|
||||
// into it. However, when doing that we lose its reference. So we need
|
||||
// two separate style elements: one that will be saved and one to hold
|
||||
// the stylesheet. Both need to be synchronized, which will be done via
|
||||
// `_commitCss`.
|
||||
let sheetOwner = this.document.querySelector('#sheet-owner');
|
||||
if (!sheetOwner) {
|
||||
sheetOwner = document.createElement('style');
|
||||
sheetOwner.setAttribute('id', 'sheet-owner');
|
||||
this.document.head.appendChild(sheetOwner);
|
||||
}
|
||||
sheetOwner.disabled = true;
|
||||
sheetOwner.textContent = this.styleElement.textContent;
|
||||
this.styleSheet = sheetOwner.sheet;
|
||||
return res;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Option method to set a css property in the mailing's custom stylesheet.
|
||||
* Note: marks all the styles as important to make sure they take precedence
|
||||
* on other stylesheets.
|
||||
*
|
||||
* @param {boolean|string} previewMode
|
||||
* @param {string} widgetValue
|
||||
* @param {Object} params
|
||||
* @param {string} params.selectorText the css selector for which to apply
|
||||
* the css
|
||||
* @param {string} params.cssProperty the name of the property to edit
|
||||
* (camel cased)
|
||||
* @param {string} [params.toggle] if 'true', will remove the property if
|
||||
* its value is already the one it's being
|
||||
* set to
|
||||
* @param {string} [params.activeValue] the value to set, if `widgetValue`
|
||||
* is not defined.
|
||||
* @returns {Promise|undefined}
|
||||
*/
|
||||
customizeCssProperty(previewMode, widgetValue, params) {
|
||||
if (!params.selectorText || !params.cssProperty) {
|
||||
return;
|
||||
}
|
||||
let value = widgetValue || params.activeValue;
|
||||
if (params.cssProperty.includes('color')) {
|
||||
value = weUtils.normalizeColor(value);
|
||||
}
|
||||
let selectors = this._getSelectors(params.selectorText);
|
||||
const firstSelector = selectors[0].replace(CSS_PREFIX, '').trim();
|
||||
if (params.cssProperty === 'font-family') {
|
||||
// Ensure font-family gets passed to all descendants and never
|
||||
// overwrite font awesome.
|
||||
const newSelectors = [];
|
||||
for (const selector of selectors) {
|
||||
newSelectors.push(...transformFontFamilySelector(selector));
|
||||
}
|
||||
selectors = [...new Set(newSelectors)];
|
||||
}
|
||||
for (const selector of selectors) {
|
||||
const priority = PRIORITY_STYLES[firstSelector].includes(params.cssProperty) ? ' !important' : '';
|
||||
const rule = this._getRule(selector);
|
||||
if (rule) {
|
||||
// The rule exists: update it.
|
||||
if (params.toggle === 'true' && rule.style.getPropertyValue(params.cssProperty) === value) {
|
||||
rule.style.removeProperty(params.cssProperty);
|
||||
} else {
|
||||
// Convert the style to css text and add the new style (the
|
||||
// `style` property is readonly, we can only edit
|
||||
// `cssText`).
|
||||
const cssTexts = [];
|
||||
for (const style of rule.style) {
|
||||
const ownPriority = rule.style.getPropertyPriority(style) ? ' !important' : '';
|
||||
if (style !== params.cssProperty) {
|
||||
cssTexts.push(`${style}: ${rule.style[style]}${ownPriority};`);
|
||||
}
|
||||
}
|
||||
cssTexts.push(`${params.cssProperty}: ${value}${priority};`);
|
||||
rule.style.cssText = cssTexts.join('\n'); // Apply the new css text.
|
||||
}
|
||||
} else {
|
||||
// The rule doesn't exist: create it.
|
||||
this.styleSheet.insertRule(`${selector} {
|
||||
${params.cssProperty}: ${value}${priority};
|
||||
}`);
|
||||
}
|
||||
}
|
||||
this._commitCss();
|
||||
},
|
||||
/**
|
||||
* Option method to change the size of buttons.
|
||||
*
|
||||
* @see BTN_SIZE_STYLES
|
||||
* @param {boolean|string} previewMode
|
||||
* @param {string} widgetValue ('btn-sm'|'btn-md'|'btn-lg'|''|undefined)
|
||||
* @param {Object} params
|
||||
* @returns {Promise|undefined}
|
||||
*/
|
||||
applyButtonSize(previewMode, widgetValue, params) {
|
||||
for (const [styleName, styleValue] of Object.entries(BTN_SIZE_STYLES[widgetValue || params.activeValue || DEFAULT_BUTTON_SIZE])) {
|
||||
if (styleValue) {
|
||||
this.customizeCssProperty(previewMode, styleValue, Object.assign({}, params, { cssProperty: styleName }));
|
||||
} else {
|
||||
// If the value is falsy, remove the property.
|
||||
for (const selector of this._getSelectors(params.selectorText)) {
|
||||
const rule = this._getRule(selector);
|
||||
if (rule) {
|
||||
rule.style.removeProperty(styleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._commitCss();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Apply the stylesheet's css text to the style element that will be saved.
|
||||
*/
|
||||
_commitCss() {
|
||||
const cssTexts = [];
|
||||
for (const rule of this.styleSheet.cssRules || this.styleSheet.rules) {
|
||||
cssTexts.push(rule.cssText);
|
||||
}
|
||||
this.styleElement.textContent = cssTexts.join('\n');
|
||||
// Flush the rules cache for convert_inline, to make sure they are
|
||||
// recomputed to account for the change.
|
||||
this.options.wysiwyg._rulesCache = undefined;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _computeWidgetState(methodName, params) {
|
||||
const res = await this._super(...arguments);
|
||||
if (res === undefined) {
|
||||
switch (methodName) {
|
||||
case 'applyButtonSize':
|
||||
case 'customizeCssProperty': {
|
||||
if (!params.selectorText) {
|
||||
return;
|
||||
}
|
||||
// Here we parse the selector in order to create a matching
|
||||
// element that we inject into the DOM so we can retrieve
|
||||
// its computed style. We then remove the element from the
|
||||
// DOM, no harm, no foul.
|
||||
const firstSelector = params.selectorText.split(',')[0].replace(CSS_PREFIX, '').trim();
|
||||
const classes = firstSelector.replace(/:not\([^\)]*\)/g, '').match(/\.([\w\d-_]+)/g) || [];
|
||||
const fakeElement = document.createElement(firstSelector.split(/[\.:, ]/)[0]);
|
||||
for (const className of classes) {
|
||||
fakeElement.classList.toggle(className.replace('.', ''), true);
|
||||
}
|
||||
this.$layout.find(CSS_PREFIX).prepend(fakeElement);
|
||||
let res;
|
||||
if (methodName === 'applyButtonSize') {
|
||||
// Match a button size by its padding value.
|
||||
const padding = getComputedStyle(fakeElement).padding;
|
||||
const classIndex = Object.values(BTN_SIZE_STYLES).findIndex(style => style.padding === padding);
|
||||
res = classIndex >= 0 ? Object.keys(BTN_SIZE_STYLES)[classIndex] : DEFAULT_BUTTON_SIZE;
|
||||
} else {
|
||||
fakeElement.style.display = 'none'; // Needed to get width in %.
|
||||
res = getComputedStyle(fakeElement)[params.cssProperty || 'font-family'];
|
||||
if (params.possibleValues && params.possibleValues[1] === FONT_FAMILIES[0]) {
|
||||
// For font-family, we need to normalize it so it
|
||||
// matches an option value.
|
||||
res = normalizeFontFamily(res);
|
||||
}
|
||||
if (params.cssProperty === 'font-weight') {
|
||||
res = parseInt(res) >= 600 ? 'bolder' : '';
|
||||
} else if (res === 'auto') {
|
||||
res = '100%';
|
||||
}
|
||||
}
|
||||
fakeElement.remove();
|
||||
return res;
|
||||
}
|
||||
case 'applyButtonSize':
|
||||
// Match a button size by its padding value.
|
||||
const rule = this._getRule(this._getSelectors(params.selectorText)[0]);
|
||||
if (rule) {
|
||||
const classIndex = Object.values(BTN_SIZE_STYLES).findIndex(style => style.padding === rule.style.padding);
|
||||
return classIndex >= 0 ? Object.keys(BTN_SIZE_STYLES)[classIndex] : DEFAULT_BUTTON_SIZE;
|
||||
} else {
|
||||
return DEFAULT_BUTTON_SIZE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Take a CSS selector and split it into separate selectors, all prefixed
|
||||
* with the `CSS_PREFIX`. Return them as an array.
|
||||
*
|
||||
* @see CSS_PREFIX
|
||||
* @param {string} selectorText
|
||||
* @returns {string[]}
|
||||
*/
|
||||
_getSelectors(selectorText) {
|
||||
return selectorText.split(',').map(t => `${t.startsWith(CSS_PREFIX) ? '' : CSS_PREFIX + ' '}${t.trim()}`.trim());;
|
||||
},
|
||||
/**
|
||||
* Take a CSS selector and find its matching rule in the mailing's custom
|
||||
* stylesheet, if it exists.
|
||||
*
|
||||
* @param {string} selectorText
|
||||
* @returns {CSSStyleRule|undefined}
|
||||
*/
|
||||
_getRule(selectorText) {
|
||||
return [...(this.styleSheet.cssRules || this.styleSheet.rules)].find(rule => rule.selectorText === selectorText);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
odoo.define('mass_mailing.snippets.editor', function (require) {
|
||||
'use strict';
|
||||
|
||||
const {_lt} = require('web.core');
|
||||
const snippetsEditor = require('web_editor.snippet.editor');
|
||||
|
||||
const MassMailingSnippetsMenu = snippetsEditor.SnippetsMenu.extend({
|
||||
events: _.extend({}, snippetsEditor.SnippetsMenu.prototype.events, {
|
||||
'click .o_we_customize_design_btn': '_onDesignTabClick',
|
||||
}),
|
||||
custom_events: _.extend({}, snippetsEditor.SnippetsMenu.prototype.custom_events, {
|
||||
drop_zone_over: '_onDropZoneOver',
|
||||
drop_zone_out: '_onDropZoneOut',
|
||||
drop_zone_start: '_onDropZoneStart',
|
||||
drop_zone_stop: '_onDropZoneStop',
|
||||
}),
|
||||
tabs: _.extend({}, snippetsEditor.SnippetsMenu.prototype.tabs, {
|
||||
DESIGN: 'design',
|
||||
}),
|
||||
optionsTabStructure: [
|
||||
['design-options', _lt("Design Options")],
|
||||
],
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
return this._super(...arguments).then(() => {
|
||||
this.$editable = this.options.wysiwyg.getEditable();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onClick: function (ev) {
|
||||
this._super(...arguments);
|
||||
var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement;
|
||||
// When we select something and move our cursor too far from the editable area, we get the
|
||||
// entire editable area as the target, which causes the tab to shift from OPTIONS to BLOCK.
|
||||
// To prevent unnecessary tab shifting, we provide a selection for this specific case.
|
||||
if (srcElement.classList.contains('o_mail_wrapper') || srcElement.querySelector('.o_mail_wrapper')) {
|
||||
const selection = this.options.wysiwyg.odooEditor.document.getSelection();
|
||||
if (selection.anchorNode) {
|
||||
const parent = selection.anchorNode.parentElement;
|
||||
if (parent) {
|
||||
srcElement = parent;
|
||||
}
|
||||
this._activateSnippet($(srcElement));
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_insertDropzone: function ($hook) {
|
||||
const $hookParent = $hook.parent();
|
||||
const $dropzone = this._super(...arguments);
|
||||
$dropzone.attr('data-editor-message', $hookParent.attr('data-editor-message'));
|
||||
$dropzone.attr('data-editor-sub-message', $hookParent.attr('data-editor-sub-message'));
|
||||
return $dropzone;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_updateRightPanelContent: function ({content, tab}) {
|
||||
this._super(...arguments);
|
||||
this.$('.o_we_customize_design_btn').toggleClass('active', tab === this.tabs.DESIGN);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_computeSnippetTemplates: function (html) {
|
||||
const $html = $(html);
|
||||
const btnSelector = '.note-editable .oe_structure > div.o_mail_snippet_general .btn:not(.btn-link)';
|
||||
const $colorpickers = $html.find('[data-selector] > we-colorpicker[data-css-property="background-color"]');
|
||||
for (const colorpicker of $colorpickers) {
|
||||
const $option = $(colorpicker).parent();
|
||||
const selectors = $option.data('selector').split(',');
|
||||
const filteredSelectors = selectors.filter(selector => !selector.includes(btnSelector)).join(',');
|
||||
$option.attr('data-selector', filteredSelectors);
|
||||
}
|
||||
html = $html.toArray().map(node => node.outerHTML).join('');
|
||||
return this._super(html);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onDropZoneOver: function () {
|
||||
this.$editable.find('.o_editable').css('background-color', '');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onDropZoneOut: function () {
|
||||
const $oEditable = this.$editable.find('.o_editable');
|
||||
if ($oEditable.find('.oe_drop_zone.oe_insert:not(.oe_vertical):only-child').length) {
|
||||
$oEditable[0].style.setProperty('background-color', 'transparent', 'important');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onDropZoneStart: function () {
|
||||
const $oEditable = this.$editable.find('.o_editable');
|
||||
if ($oEditable.find('.oe_drop_zone.oe_insert:not(.oe_vertical):only-child').length) {
|
||||
$oEditable[0].style.setProperty('background-color', 'transparent', 'important');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onDropZoneStop: function () {
|
||||
const $oEditable = this.$editable.find('.o_editable');
|
||||
$oEditable.css('background-color', '');
|
||||
if (!$oEditable.find('.oe_drop_zone.oe_insert:not(.oe_vertical):only-child').length) {
|
||||
$oEditable.attr('contenteditable', true);
|
||||
}
|
||||
// Refocus again to save updates when calling `_onWysiwygBlur`
|
||||
this.$editable.focus();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onSnippetRemoved: function () {
|
||||
this._super(...arguments);
|
||||
const $oEditable = this.$editable.find('.o_editable');
|
||||
if (!$oEditable.children().length) {
|
||||
$oEditable.empty(); // remove any superfluous whitespace
|
||||
$oEditable.attr('contenteditable', false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _onDesignTabClick() {
|
||||
// Note: nothing async here but start the loading effect asap
|
||||
let releaseLoader;
|
||||
try {
|
||||
const promise = new Promise(resolve => releaseLoader = resolve);
|
||||
this._execWithLoadingEffect(() => promise, false, 0);
|
||||
// loader is added to the DOM synchronously
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
// ensure loader is rendered: first call asks for the (already done) DOM update,
|
||||
// second call happens only after rendering the first "updates"
|
||||
|
||||
if (!this.topFakeOptionEl) {
|
||||
let el;
|
||||
for (const [elementName, title] of this.optionsTabStructure) {
|
||||
const newEl = document.createElement(elementName);
|
||||
newEl.dataset.name = title;
|
||||
if (el) {
|
||||
el.appendChild(newEl);
|
||||
} else {
|
||||
this.topFakeOptionEl = newEl;
|
||||
}
|
||||
el = newEl;
|
||||
}
|
||||
this.bottomFakeOptionEl = el;
|
||||
this.el.appendChild(this.topFakeOptionEl);
|
||||
}
|
||||
|
||||
// Need all of this in that order so that:
|
||||
// - the element is visible and can be enabled and the onFocus method is
|
||||
// called each time.
|
||||
// - the element is hidden afterwards so it does not take space in the
|
||||
// DOM, same as the overlay which may make a scrollbar appear.
|
||||
this.topFakeOptionEl.classList.remove('d-none');
|
||||
const editorPromise = this._activateSnippet($(this.bottomFakeOptionEl));
|
||||
releaseLoader(); // because _activateSnippet uses the same mutex as the loader
|
||||
releaseLoader = undefined;
|
||||
const editor = await editorPromise;
|
||||
this.topFakeOptionEl.classList.add('d-none');
|
||||
editor.toggleOverlay(false);
|
||||
|
||||
this._updateRightPanelContent({
|
||||
tab: this.tabs.DESIGN,
|
||||
});
|
||||
} catch (e) {
|
||||
// Normally the loading effect is removed in case of error during the action but here
|
||||
// the actual activity is happening outside of the action, the effect must therefore
|
||||
// be cleared in case of error as well
|
||||
if (releaseLoader) {
|
||||
releaseLoader();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return MassMailingSnippetsMenu;
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
odoo.define('mass_mailing.mass_mailing_code_view_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('mass_mailing_code_view_tour', {
|
||||
url: '/web',
|
||||
test: true,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
}, {
|
||||
trigger: 'button.o_list_button_add',
|
||||
}, {
|
||||
trigger: 'input#subject',
|
||||
content: ('Pick the <b>email subject</b>.'),
|
||||
position: 'bottom',
|
||||
run: 'text Test'
|
||||
}, {
|
||||
trigger: 'div[name="contact_list_ids"] .o_input_dropdown input[type="text"]',
|
||||
content: 'Click on the dropdown to open it and then start typing to search.',
|
||||
}, {
|
||||
trigger: 'div[name="contact_list_ids"] .ui-state-active',
|
||||
content: 'Select item from dropdown',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe #default',
|
||||
content: 'Choose this <b>theme</b>.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'iframe .o_codeview_btn',
|
||||
content: ('Click here to switch to <b>code view</b>'),
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'iframe .o_codeview',
|
||||
content: ('Remove all content from codeview'),
|
||||
run: function () {
|
||||
const iframe = document.querySelector('.wysiwyg_iframe');
|
||||
const iframeDocument = iframe.contentWindow.document;
|
||||
let element = iframeDocument.querySelector(".o_codeview");
|
||||
element.value = '';
|
||||
}
|
||||
}, {
|
||||
trigger: 'iframe .o_codeview_btn',
|
||||
content: ('Click here to switch back from <b>code view</b>'),
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '[name="body_arch"] iframe .o_mail_wrapper_td',
|
||||
content: 'Verify that the dropable zone was not removed',
|
||||
run: () => {},
|
||||
}, {
|
||||
trigger: '[name="body_arch"] iframe #email_designer_default_body [name="Title"] .ui-draggable-handle',
|
||||
content: 'Drag the "Title" snippet from the design panel and drop it in the editor',
|
||||
extra_trigger: '[name="body_arch"] iframe body.editor_enable',
|
||||
run: function (actions) {
|
||||
actions.drag_and_drop('[name="body_arch"] iframe .o_editable', this.$anchor);
|
||||
}
|
||||
}, {
|
||||
trigger: '[name="body_arch"] iframe .o_editable h1',
|
||||
content: 'Verify that the title was inserted properly in the editor',
|
||||
run: () => {},
|
||||
}, {
|
||||
trigger: 'button.o_form_button_save',
|
||||
content: 'Click on the "Save" button to save the changes.',
|
||||
run: 'click',
|
||||
},
|
||||
...tour.stepUtils.saveForm(),]);
|
||||
});
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
odoo.define('mass_mailing.mass_mailing_editor_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
const { boundariesIn, setSelection } = require('@web_editor/js/editor/odoo-editor/src/utils/utils');
|
||||
|
||||
tour.register('mass_mailing_editor_tour', {
|
||||
url: '/web',
|
||||
test: true,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
}, {
|
||||
trigger: 'button.o_list_button_add',
|
||||
}, {
|
||||
trigger: 'div[name="contact_list_ids"] .o_input_dropdown input[type="text"]',
|
||||
}, {
|
||||
trigger: 'div[name="contact_list_ids"] .ui-state-active'
|
||||
}, {
|
||||
content: 'choose the theme "empty" to edit the mailing with snippets',
|
||||
trigger: '[name="body_arch"] iframe #empty',
|
||||
}, {
|
||||
content: 'wait for the editor to be rendered',
|
||||
trigger: '[name="body_arch"] iframe .o_editable[data-editor-message="DRAG BUILDING BLOCKS HERE"]',
|
||||
run: () => {},
|
||||
}, {
|
||||
content: 'drag the "Title" snippet from the design panel and drop it in the editor',
|
||||
trigger: '[name="body_arch"] iframe #email_designer_default_body [name="Title"] .ui-draggable-handle',
|
||||
run: function (actions) {
|
||||
actions.drag_and_drop('[name="body_arch"] iframe .o_editable', this.$anchor);
|
||||
}
|
||||
}, {
|
||||
content: 'wait for the snippet menu to finish the drop process',
|
||||
trigger: '[name="body_arch"] iframe #email_designer_header_elements:not(:has(.o_we_already_dragging))',
|
||||
run: () => {}
|
||||
}, {
|
||||
content: 'verify that the title was inserted properly in the editor',
|
||||
trigger: '[name="body_arch"] iframe .o_editable h1',
|
||||
run: () => {},
|
||||
}, {
|
||||
trigger: 'button.o_form_button_save',
|
||||
}, {
|
||||
content: 'verify that the save failed (since the field "subject" was not set and it is required)',
|
||||
trigger: 'label.o_field_invalid',
|
||||
run: () => {},
|
||||
}, {
|
||||
content: 'verify that the edited mailing body was not lost during the failed save',
|
||||
trigger: '[name="body_arch"] iframe .o_editable h1',
|
||||
run: () => {},
|
||||
}, {
|
||||
trigger: 'input#subject',
|
||||
run: 'text Test',
|
||||
}, {
|
||||
trigger: '.o_form_view', // blur previous input
|
||||
},
|
||||
...tour.stepUtils.saveForm(),
|
||||
{
|
||||
trigger: 'iframe .o_editable',
|
||||
run: () => {},
|
||||
}]);
|
||||
|
||||
tour.register('mass_mailing_basic_theme_toolbar', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: "Select the 'Email Marketing' app.",
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
},
|
||||
{
|
||||
content: "Click on the create button to create a new mailing.",
|
||||
trigger: 'button.o_list_button_add',
|
||||
},
|
||||
{
|
||||
content: "Fill in Subject",
|
||||
trigger: '#subject',
|
||||
run: 'text Test Basic Theme',
|
||||
},
|
||||
{
|
||||
content: "Fill in Mailing list",
|
||||
trigger: '#contact_list_ids',
|
||||
run: 'text Newsletter',
|
||||
},
|
||||
{
|
||||
content: "Pick 'Newsletter' option",
|
||||
trigger: '.o_input_dropdown a:contains(Newsletter)',
|
||||
},
|
||||
{
|
||||
content: "Pick the basic theme",
|
||||
trigger: 'iframe #basic',
|
||||
extra_trigger: 'iframe .o_mail_theme_selector_new',
|
||||
},
|
||||
{
|
||||
content: "Make sure the snippets menu is hidden",
|
||||
trigger: 'iframe html:has(#oe_snippets.d-none)',
|
||||
run: () => null, // no click, just check
|
||||
},
|
||||
{
|
||||
content: "Click on the New button to create another mailing",
|
||||
trigger: 'button.o_form_button_create',
|
||||
},
|
||||
{
|
||||
content: "Fill in Subject",
|
||||
trigger: '#subject',
|
||||
extra_trigger: 'iframe .o_mail_theme_selector_new',
|
||||
run: 'text Test Newsletter Theme',
|
||||
},
|
||||
{
|
||||
content: "Fill in Mailing list",
|
||||
trigger: '#contact_list_ids',
|
||||
run: 'text Newsletter',
|
||||
},
|
||||
{
|
||||
content: "Pick 'Newsletter' option",
|
||||
trigger: '.o_input_dropdown a:contains(Newsletter)',
|
||||
},
|
||||
{
|
||||
content: "Pick the newsletter theme",
|
||||
trigger: 'iframe #newsletter',
|
||||
// extra_trigger: 'iframe .o_mail_theme_selector_new',
|
||||
},
|
||||
{
|
||||
content: "Make sure the snippets menu is displayed",
|
||||
trigger: 'iframe #oe_snippets',
|
||||
run: () => null, // no click, just check
|
||||
},
|
||||
{
|
||||
content: 'Save form',
|
||||
trigger: '.o_form_button_save',
|
||||
},
|
||||
{
|
||||
content: 'Go back to previous mailing',
|
||||
trigger: 'button.o_pager_previous',
|
||||
},
|
||||
{
|
||||
content: "Make sure the snippets menu is hidden",
|
||||
trigger: 'iframe html:has(#oe_snippets.d-none)',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: "Add some content to be selected afterwards",
|
||||
trigger: 'iframe p',
|
||||
run: 'text content',
|
||||
},
|
||||
{
|
||||
content: "Select text",
|
||||
trigger: 'iframe p:contains(content)',
|
||||
run() {
|
||||
setSelection(...boundariesIn(this.$anchor[0]), false);
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "Make sure the floating toolbar is visible",
|
||||
trigger: '#toolbar.oe-floating[style*="visible"]',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: "Open the color picker",
|
||||
trigger: '#toolbar #oe-text-color',
|
||||
},
|
||||
{
|
||||
content: "Pick a color",
|
||||
trigger: '#toolbar button[data-color="o-color-1"]',
|
||||
},
|
||||
{
|
||||
content: "Check that color was applied",
|
||||
trigger: 'iframe p font.text-o-color-1',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: 'Save changes',
|
||||
trigger: '.o_form_button_save',
|
||||
},
|
||||
{
|
||||
content: "Go to 'Mailings' list view",
|
||||
trigger: '.breadcrumb a:contains(Mailings)'
|
||||
},
|
||||
{
|
||||
content: "Open newly created mailing",
|
||||
trigger: 'td:contains("Test Basic Theme")',
|
||||
},
|
||||
{
|
||||
content: "Make sure the snippets menu is hidden",
|
||||
trigger: 'iframe html:has(#oe_snippets.d-none)',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: "Select content",
|
||||
trigger: 'iframe p:contains(content)',
|
||||
run() {
|
||||
setSelection(...boundariesIn(this.$anchor[0]), false);
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "Make sure the floating toolbar is visible",
|
||||
trigger: '#toolbar.oe-floating[style*="visible"]',
|
||||
run: () => null,
|
||||
},
|
||||
...tour.stepUtils.discardForm(),
|
||||
]);
|
||||
|
||||
tour.register('mass_mailing_campaing_new_mailing', {
|
||||
url: '/web',
|
||||
test: true,
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: 'Select the "Email Marketing" app',
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
},
|
||||
{
|
||||
content: 'Select "Campaings" Navbar item',
|
||||
trigger: '.o_nav_entry[data-menu-xmlid="mass_mailing.menu_email_campaigns"]',
|
||||
},
|
||||
{
|
||||
content: 'Select "Newsletter" campaign',
|
||||
trigger: '.oe_kanban_card:contains("Test Newsletter")',
|
||||
},
|
||||
{
|
||||
content: 'Add a line (create new mailing)',
|
||||
trigger: '.o_field_x2many_list_row_add a',
|
||||
},
|
||||
{
|
||||
content: 'Pick the basic theme',
|
||||
trigger: 'iframe',
|
||||
run(actions) {
|
||||
// For some reason the selectors inside the iframe cannot be triggered.
|
||||
const link = this.$anchor[0].contentDocument.querySelector('#basic');
|
||||
actions.click(link);
|
||||
}
|
||||
},
|
||||
{
|
||||
content: 'Fill in Subject',
|
||||
trigger: '#subject',
|
||||
run: 'text Test',
|
||||
},
|
||||
{
|
||||
content: 'Fill in Mailing list',
|
||||
trigger: '#contact_list_ids',
|
||||
run: 'text Test Newsletter',
|
||||
},
|
||||
{
|
||||
content: 'Pick "Newsletter" option',
|
||||
trigger: '.o_input_dropdown a:contains(Test Newsletter)',
|
||||
},
|
||||
{
|
||||
content: 'Save form',
|
||||
trigger: '.o_form_button_save',
|
||||
},
|
||||
{
|
||||
content: 'Check that newly created record is on the list',
|
||||
trigger: '[name="mailing_mail_ids"] td[name="subject"]:contains("Test")',
|
||||
run: () => null,
|
||||
},
|
||||
...tour.stepUtils.saveForm(),
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('mass_mailing_snippets_menu_tabs', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(), {
|
||||
content: "Select the 'Email Marketing' app.",
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
},
|
||||
{
|
||||
content: "Click on the create button to create a new mailing.",
|
||||
trigger: 'button.o_list_button_add',
|
||||
},
|
||||
{
|
||||
content: "Click on the 'Start From Scratch' template.",
|
||||
trigger: 'iframe #empty',
|
||||
},
|
||||
{
|
||||
content: "Click on the 'Design' tab.",
|
||||
trigger: 'iframe .o_we_customize_design_btn',
|
||||
},
|
||||
{
|
||||
content: "Click on the empty 'DRAG BUILDING BLOCKS HERE' area.",
|
||||
trigger: 'iframe .oe_structure.o_mail_no_options',
|
||||
},
|
||||
{
|
||||
content: "Click on the 'Design' tab.",
|
||||
trigger: 'iframe .o_we_customize_design_btn',
|
||||
},
|
||||
{
|
||||
content: "Verify that the customize panel is not empty.",
|
||||
trigger: 'iframe .o_we_customize_panel .snippet-option-DesignTab',
|
||||
run: () => null, // it's a check
|
||||
},
|
||||
{
|
||||
content: "Click on the style tab.",
|
||||
trigger: 'iframe .o_we_customize_snippet_btn',
|
||||
},
|
||||
{
|
||||
content: "Click on the 'Design' tab.",
|
||||
trigger: 'iframe .o_we_customize_design_btn',
|
||||
},
|
||||
{
|
||||
content: "Verify that the customize panel is not empty.",
|
||||
trigger: 'iframe .o_we_customize_panel .snippet-option-DesignTab',
|
||||
run: () => null, // it's a check
|
||||
},
|
||||
...tour.stepUtils.discardForm(),
|
||||
]);
|
||||
|
||||
|
||||
tour.register('mass_mailing_snippets_menu_toolbar_new_mailing_mobile', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(), {
|
||||
content: "Select the 'Email Marketing' app.",
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
},
|
||||
{
|
||||
content: "Click on the create button to create a new mailing.",
|
||||
trigger: 'button.o_list_button_add',
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Check templates available in theme selector",
|
||||
trigger: 'iframe .o_mail_theme_selector_new',
|
||||
run: function () {
|
||||
if (this.$anchor[0].querySelector('#empty')) {
|
||||
console.error('The empty template should not be visible on mobile.');
|
||||
}
|
||||
},
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Make sure the toolbar isn't floating",
|
||||
trigger: 'iframe',
|
||||
run: function () {
|
||||
const iframeDocument = this.$anchor[0].contentDocument;
|
||||
if (iframeDocument.querySelector('#toolbar.oe-floating')) {
|
||||
console.error('There should not be a floating toolbar in the iframe');
|
||||
}
|
||||
},
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Click on the 'Start From Scratch' template.",
|
||||
trigger: 'iframe #default',
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Select an editable element",
|
||||
trigger: 'iframe .s_text_block',
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Make sure the snippets menu is hidden",
|
||||
trigger: 'iframe',
|
||||
run: function () {
|
||||
const iframeDocument = this.$anchor[0].contentDocument;
|
||||
if (!iframeDocument.querySelector('#oe_snippets.d-none')) {
|
||||
console.error('The snippet menu should be hidden');
|
||||
}
|
||||
},
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Make sure the toolbar is there, with the tables formating tool",
|
||||
trigger: 'iframe #toolbar.oe-floating #table:not(.d-none)',
|
||||
run: () => null, // it's a check
|
||||
mobile: true,
|
||||
},
|
||||
]);
|
||||
|
||||
tour.register('mass_mailing_snippets_menu_toolbar', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(), {
|
||||
content: "Select the 'Email Marketing' app.",
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
},
|
||||
{
|
||||
content: "Click on the create button to create a new mailing.",
|
||||
trigger: 'button.o_list_button_add',
|
||||
},
|
||||
{
|
||||
content: "Wait for the theme selector to load.",
|
||||
trigger: 'iframe .o_mail_theme_selector_new',
|
||||
},
|
||||
{
|
||||
content: "Make sure there does not exist a floating toolbar",
|
||||
trigger: 'iframe',
|
||||
run: function () {
|
||||
const iframeDocument = this.$anchor[0].contentDocument;
|
||||
if (iframeDocument.querySelector('#toolbar.oe-floating')) {
|
||||
console.error('There should not be a floating toolbar in the iframe');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Make sure the empty template is an option on non-mobile devices.",
|
||||
trigger: 'iframe #empty',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: "Click on the default 'welcome' template.",
|
||||
trigger: 'iframe #default',
|
||||
},
|
||||
{ // necessary to wait for the cursor to be placed in the first p
|
||||
// and to avoid leaving the page before the selection is added
|
||||
content: "Wait for template selection event to be over.",
|
||||
trigger: 'iframe .o_editable.theme_selection_done',
|
||||
},
|
||||
{
|
||||
content: "Make sure the snippets menu is not hidden",
|
||||
trigger: 'iframe #oe_snippets:not(.d-none)',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: "Wait for .s_text_block to be populated",
|
||||
trigger: 'iframe .s_text_block p',
|
||||
run: () => null,
|
||||
},
|
||||
{
|
||||
content: "Click and select p block inside the editor",
|
||||
trigger: 'iframe',
|
||||
run: function () {
|
||||
const iframeWindow = this.$anchor[0].contentWindow;
|
||||
const iframeDocument = iframeWindow.document;
|
||||
const p = iframeDocument.querySelector('.s_text_block p');
|
||||
p.click();
|
||||
const selection = iframeWindow.getSelection();
|
||||
const range = iframeDocument.createRange();
|
||||
range.selectNodeContents(p);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Make sure the toolbar is there",
|
||||
trigger: 'iframe #oe_snippets .o_we_customize_panel #toolbar',
|
||||
run: () => null,
|
||||
},
|
||||
...tour.stepUtils.discardForm(),
|
||||
]);
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
odoo.define('mass_mailing.mass_mailing_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('mass_mailing_tour', {
|
||||
url: '/web',
|
||||
rainbowManMessage: _t('Congratulations, I love your first mailing. :)'),
|
||||
sequence: 200,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
content: _t("Let's try the Email Marketing app."),
|
||||
width: 225,
|
||||
position: 'bottom',
|
||||
edition: 'enterprise',
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
|
||||
content: _t("Let's try the Email Marketing app."),
|
||||
edition: 'community',
|
||||
}, {
|
||||
trigger: '.o_list_button_add',
|
||||
extra_trigger: '.o_mass_mailing_mailing_tree',
|
||||
content: Markup(_t("Start by creating your first <b>Mailing</b>.")),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'div[name="subject"]',
|
||||
content: Markup(_t('Pick the <b>email subject</b>.')),
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="contact_list_ids"] > .o_input_dropdown > input[type="text"]',
|
||||
run: 'click',
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: 'li.ui-menu-item',
|
||||
run: 'click',
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe #newsletter',
|
||||
content: Markup(_t('Choose this <b>theme</b>.')),
|
||||
position: 'left',
|
||||
edition: 'enterprise',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe #default',
|
||||
content: Markup(_t('Choose this <b>theme</b>.')),
|
||||
position: 'right',
|
||||
edition: 'community',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe div.theme_selection_done div.s_text_block',
|
||||
content: _t('Click on this paragraph to edit it.'),
|
||||
position: 'top',
|
||||
edition: 'enterprise',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="body_arch"] iframe div.o_mail_block_title_text',
|
||||
content: _t('Click on this paragraph to edit it.'),
|
||||
position: 'top',
|
||||
edition: 'community',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'button[name="action_set_favorite"]',
|
||||
content: _t('Click on this button to add this mailing to your templates.'),
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'button[name="action_test"]',
|
||||
content: _t("Test this mailing by sending a copy to yourself."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'button[name="send_mail_test"]',
|
||||
content: _t("Check the email address and click send."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: 'button[name="action_launch"]',
|
||||
content: _t("Ready for take-off!"),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.btn-primary:contains("Ok")',
|
||||
content: _t("Don't worry, the mailing contact we created is an internal user."),
|
||||
position: 'bottom',
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: '.o_back_button',
|
||||
content: Markup(_t("By using the <b>Breadcrumb</b>, you can navigate back to the overview.")),
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
}]
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
odoo.define('mass_mailing.wysiwyg', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
var MassMailingSnippetsMenu = require('mass_mailing.snippets.editor');
|
||||
const {closestElement} = require('@web_editor/js/editor/odoo-editor/src/OdooEditor');
|
||||
const Toolbar = require('web_editor.toolbar');
|
||||
|
||||
const MassMailingWysiwyg = Wysiwyg.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
startEdition: async function () {
|
||||
const res = await this._super(...arguments);
|
||||
// Prevent selection change outside of snippets.
|
||||
this.$editable.on('mousedown', e => {
|
||||
if ($(e.target).is('.o_editable:empty') || e.target.querySelector('.o_editable')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
this.snippetsMenuToolbar = this.toolbar;
|
||||
return res;
|
||||
},
|
||||
|
||||
toggleLinkTools(options = {}) {
|
||||
this._super({
|
||||
...options,
|
||||
// Always open the dialog when the sidebar is folded.
|
||||
forceDialog: options.forceDialog || this.snippetsMenu.folded
|
||||
});
|
||||
if (this.snippetsMenu.folded) {
|
||||
// Hide toolbar and avoid it being re-displayed after getDeepRange.
|
||||
this.odooEditor.document.getSelection().collapseToEnd();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets SnippetsMenu fold state and switches toolbar.
|
||||
* Instantiates a new floating Toolbar if needed.
|
||||
*
|
||||
* @param {Boolean} fold
|
||||
*/
|
||||
setSnippetsMenuFolded: async function (fold = true) {
|
||||
if (fold) {
|
||||
this.snippetsMenu.setFolded(true);
|
||||
if (!this.floatingToolbar) {
|
||||
// Instantiate and configure new toolbar.
|
||||
this.floatingToolbar = new Toolbar(this, 'web_editor.toolbar');
|
||||
this.toolbar = this.floatingToolbar;
|
||||
await this.toolbar.appendTo(document.createElement('void'));
|
||||
this._configureToolbar({ snippets: false });
|
||||
this._updateEditorUI();
|
||||
this.setCSSVariables(this.toolbar.el);
|
||||
this.odooEditor.setupToolbar(this.toolbar.el);
|
||||
if (this.odooEditor.isMobile) {
|
||||
document.body.querySelector('.o_mail_body').prepend(this.toolbar.el);
|
||||
} else {
|
||||
document.body.append(this.toolbar.el);
|
||||
}
|
||||
} else {
|
||||
this.toolbar = this.floatingToolbar;
|
||||
}
|
||||
this.toolbar.el.classList.remove('d-none');
|
||||
this.odooEditor.autohideToolbar = true;
|
||||
this.odooEditor.toolbarHide();
|
||||
} else {
|
||||
this.snippetsMenu.setFolded(false);
|
||||
this.toolbar = this.snippetsMenuToolbar;
|
||||
this.odooEditor.autohideToolbar = false;
|
||||
if (this.floatingToolbar) {
|
||||
this.floatingToolbar.el.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
this.odooEditor.toolbar = this.toolbar.el;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
openMediaDialog: function() {
|
||||
this._super(...arguments);
|
||||
// Opening the dialog in the outer document does not trigger the selectionChange
|
||||
// (that would normally hide the toolbar) in the iframe.
|
||||
if (this.snippetsMenu.folded) {
|
||||
this.odooEditor.toolbarHide();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_createSnippetsMenuInstance: function (options={}) {
|
||||
return new MassMailingSnippetsMenu(this, Object.assign({
|
||||
wysiwyg: this,
|
||||
selectorEditableArea: '.o_editable',
|
||||
}, options));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getPowerboxOptions: function () {
|
||||
const options = this._super();
|
||||
const {commands} = options;
|
||||
const linkCommands = commands.filter(command => command.name === 'Link' || command.name === 'Button');
|
||||
for (const linkCommand of linkCommands) {
|
||||
// Remove the command if the selection is within a background-image.
|
||||
const superIsDisabled = linkCommand.isDisabled;
|
||||
linkCommand.isDisabled = () => {
|
||||
if (superIsDisabled && superIsDisabled()) {
|
||||
return true;
|
||||
} else {
|
||||
const selection = this.odooEditor.document.getSelection();
|
||||
const range = selection.rangeCount && selection.getRangeAt(0);
|
||||
return !!range && !!closestElement(range.startContainer, '[style*=background-image]');
|
||||
}
|
||||
}
|
||||
}
|
||||
return {...options, commands};
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_updateEditorUI: function (e) {
|
||||
this._super(...arguments);
|
||||
// Hide the create-link button if the selection is within a
|
||||
// background-image.
|
||||
const selection = this.odooEditor.document.getSelection();
|
||||
const range = selection.rangeCount && selection.getRangeAt(0);
|
||||
const isWithinBackgroundImage = !!range && !!closestElement(range.startContainer, '[style*=background-image]');
|
||||
if (isWithinBackgroundImage) {
|
||||
this.toolbar.$el.find('#create-link').toggleClass('d-none', true);
|
||||
}
|
||||
},
|
||||
_getEditorOptions: function () {
|
||||
const options = this._super(...arguments);
|
||||
const finalOptions = { autoActivateContentEditable: false, ...options };
|
||||
return finalOptions;
|
||||
},
|
||||
});
|
||||
|
||||
return MassMailingWysiwyg;
|
||||
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue