[IMP] web_editor,*: Refactoring the wysiwyg editor and 'html' field
* Creating a new structure by transforming all the plugins in the
library using the odoo inheritance system. Plugins are easier to
implement with the AbstractPlugin to add Odoo behaviors.
* From now on, the methods of the library (in this case Summernote) can
no longer be called by other modules or files. Only the wysiwyg
widgets can access it, to simplify the updating process. The wysiwyg
object serves as an interface.
* Depending on the options the snippets will be loaded or not, the
editor will be in an iframe or not... all of this is transparent from
the outside.
* Regarding iframes, all controllers related to editing have been
removed: the new API no longer needs them. This speeds up loading,
eases testing and removes complexity for the same
features.
PUBLIC FEATURES
There are several public methods on the Wysiwyg class:
* Wysiwyg.prepare (WidgetParent): returns a deferred resolved when the
library (xml, lazy, assets...) is loaded.
* Wysiwyg.getRange (DOM): returns the range (selection in the dom)
* Wysiwyg.setRange (startNode, startOffset, endNode, endOffset): creates
a range (selection in the dom)
* Wysiwyg.setRangeFromNode (DOM, options) that creates a range from an
element (option available to select all, start or end)
A jQuery selector was added: :o_editable, which indicates whether the
current element is editable. That is, if it is contained in a tag with
the attribute 'contentEditable = "true"' or in a tag with the class
o_editable.
Several methods are also present:
* focusIn: makes a focus and places the cursor at the beginning of the
element
* focusInEnd: makes a focus and places the cursor at the end of the
element
* selectContent: makes a focus and selects the content
HTML FIELD
The HTML field can receive different options:
* style-inline: {boolean} transforms a class into an inline style when
saving and vice versa when reading.
* no-attachment: {boolean} prevents the use of attachments (in media
dialog)
* cssEdit: {xml_id} to use a template containing the css to loaded in
an iframe when editing
* cssReadonly: {xml_id} to use a template containing the css to load
into an iframe when viewing in readonly
* snippets: {xml_id} snippets template (can be used with or without
cssEdit)
* wrapper: {template} qweb static template (containing a tag:
id = "wrapper") that will include the content during editing (removed
on save)
MASS MAILING
A widget was created for mass mailing. There are now two fields:
body_html and body_arch.
body_arch contains the code with the class without conversion into
inline style, useful when editing and one with the inline style that is
visible in readonly mode and sent by email.
Advantage: no spreading errors, able to update css/theme, able to do
more changes when converting to inline style so that a maximum of mail
clients have an impeccable rendering.
Co-authored-by: Antoine Guenet <age@odoo.com>
This commit is contained in:
co-authored by
Antoine Guenet
parent
de444335b7
commit
f296992317
@@ -368,7 +368,7 @@ var Discuss = AbstractAction.extend({
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this.$buttons) {
|
||||
this.$buttons.off().destroy();
|
||||
this.$buttons.off().remove();
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -44,5 +44,8 @@
|
||||
'demo': [
|
||||
'data/mass_mailing_demo.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/*.xml',
|
||||
],
|
||||
'application': True,
|
||||
}
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
from . import web_editor
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.web_editor.controllers.main import Web_Editor
|
||||
|
||||
|
||||
class Web_Editor(Web_Editor):
|
||||
|
||||
@http.route(["/website_mass_mailing/field/popup_content"], type='http', auth="user")
|
||||
def mass_mailing_FieldTextHtmlPopupTemplate(self, model=None, res_id=None, field=None, callback=None, **kwargs):
|
||||
kwargs['snippets'] = '/website/snippets'
|
||||
kwargs['template'] = 'mass_mailing.FieldTextHtmlPopupContent'
|
||||
return self.FieldTextHtml(model, res_id, field, callback, **kwargs)
|
||||
|
||||
@http.route('/mass_mailing/field/email_template', type='http', auth="user")
|
||||
def mass_mailing_FieldTextHtmlEmailTemplate(self, model=None, res_id=None, field=None, callback=None, **kwargs):
|
||||
kwargs['snippets'] = '/mass_mailing/snippets'
|
||||
kwargs['template'] = 'mass_mailing.FieldTextHtmlInline'
|
||||
return self.FieldTextHtmlInline(model, res_id, field, callback, **kwargs)
|
||||
|
||||
@http.route(['/mass_mailing/snippets'], type='json', auth="user", website=True)
|
||||
def mass_mailing_snippets(self):
|
||||
values = {'company_id': request.env['res.users'].browse(request.uid).company_id}
|
||||
return request.env.ref('mass_mailing.email_designer_snippets').render(values)
|
||||
@@ -85,65 +85,115 @@
|
||||
<field name="reply_to_mode">email</field>
|
||||
<field name="reply_to">Info <info@yourcompany.example.com></field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 24px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: white; padding: 0; border-collapse:separate;">
|
||||
<tr><td valign="middle">
|
||||
<span style="font-size: 20px; font-weight: bold;">Your Company</span>
|
||||
</td><td valign="middle" align="right">
|
||||
<img src="/logo.png" style="padding: 0px; margin: 0px; height: 48px;" alt="YourCompanyny"/>
|
||||
</td></tr>
|
||||
<tr><td colspan="2" style="text-align:center;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:4px 0px 32px 0px;"/>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td style="min-width: 590px;">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Great stories have personality. Consider telling a great story that provides personality.
|
||||
Writing a story with personality for potential clients will assist with making a relationship connection.
|
||||
This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br />
|
||||
Great stories are for everyone even when only written for just one person.
|
||||
If you try to write with a wide general audience in mind, your story will ring false and be bland.
|
||||
No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.
|
||||
</p>
|
||||
<p style="margin: 32px 0px 32px 0px; text-align: center;">
|
||||
<a href="http://www.example.com"
|
||||
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
Click here for your special promotion !
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px; padding: 0 8px 0 8px; font-size:11px;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 4px 0px;"/>
|
||||
<b>YourCompany</b><br/>
|
||||
<div style="color: #999999;">
|
||||
<a href="/unsubscribe_from_list" style="text-decoration:none; color: #999999;">Unsubscribe</a>
|
||||
|
|
||||
<a href="http://www.example.net/page/contactus" style="text-decoration:none; color: #999999;">Contact</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- POWERED BY -->
|
||||
<tr><td align="center" style="min-width: 590px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com" style="color: #875A7B;">Odoo</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<div class="o_layout o_default_theme">
|
||||
<table class="o_mailWrapper" style="border-collapse:collapse;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="o_mail_no_resize o_not_editable" style="text-align:left;"> </td>
|
||||
<td class="o_mail_no_options o_mailWrapper_td oe_structure" style="text-align:left;width:100%;">
|
||||
<div class="o_mail_block_header_logo">
|
||||
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||
<table align="center" cellspacing="0" cellpadding="0" border="0" class="o_mail_table_styles o_mail_h_padding" style="padding:0 20px 0 20px;width:100%;border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="center" width="30%" class="text-center o_mail_v_padding pb0" style="padding:20px 0 0px 0;vertical-align:middle;text-align:center;">
|
||||
<a href="http://www.example.com" style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);">
|
||||
<img border="0" src="/mass_mailing/static/src/img/theme_default/s_default_image_logo.png" style="border-style:none;height:auto;vertical-align:middle;max-width:400px;width:auto" alt="Your Logo"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_mail_block_footer_separator" style="margin:0 20px 0 20px;">
|
||||
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||
<table align="center" cellspacing="0" cellpadding="0" class="o_mail_table_styles o_mail_full_width_padding" style="width:100%;border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top" style="padding:20px 0 20px 0;text-align:left;vertical-align:top;width:100%;" class="o_mail_v_padding o_mail_no_colorpicker">
|
||||
<div style="background-color:rgb(245, 245, 245);height:2px;width:100%;" class="separator"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_mail_block_paragraph">
|
||||
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||
<table align="center" cellspacing="0" cellpadding="0" border="0" class="o_mail_table_styles" style="width:100%;border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="100%" class="o_mail_h_padding o_mail_v_padding o_mail_no_colorpicker" style="padding:20px;text-align:left;vertical-align:top;">
|
||||
<p style="margin:0px 0 1rem 0;font-size:14px;">
|
||||
Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.
|
||||
<br/>Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_mail_block_footer_social o_mail_footer_social_center">
|
||||
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||
<table align="center" cellspacing="0" cellpadding="0" class="o_mail_table_styles o_mail_full_width_padding" style="border-style:solid none none none;padding:20px 0 20px 0;border-top-color:rgb(245, 245, 245);border-top-width:2px;width:100%;border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="o_mail_footer_social" style="text-align:center;vertical-align:middle;">
|
||||
<a title="Facebook" href="https://www.facebook.com/Odoo" style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);">
|
||||
<img src="/web_editor/font_to_img/61594/rgb(100,89,116)/16" data-class="fa fa-facebook" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>
|
||||
|
||||
</a>
|
||||
<a style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);margin-left:10px" title="Google Plus" href="https://plus.google.com/+Odooapps">
|
||||
<img src="/web_editor/font_to_img/61653/rgb(100,89,116)/16" data-class="fa fa-google-plus" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>
|
||||
</a>
|
||||
<a style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);margin-left:10px" title="LinkedIn" href="https://www.linkedin.com/company/odoo">
|
||||
<img src="/web_editor/font_to_img/61665/rgb(100,89,116)/16" data-class="fa fa-linkedin" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>
|
||||
</a>
|
||||
<a style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);margin-left:10px" title="Twitter" href="https://twitter.com/Odoo">
|
||||
<img src="/web_editor/font_to_img/61593/rgb(100,89,116)/16" data-class="fa fa-twitter" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>
|
||||
</a>
|
||||
<a style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);margin-left:10px" title="Instagram" href="https://www.instagram.com/explore/tags/odoo/">
|
||||
<img src="/web_editor/font_to_img/61805/rgb(100,89,116)/16" data-class="fa fa-instagram" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>
|
||||
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_mail_footer_links o_default_snippet_text" style="padding:10px 0 10px 0;text-align:center;vertical-align:middle;">
|
||||
<a href="/unsubscribe_from_list" class="btn btn-link o_default_snippet_text" style="text-decoration:none;border-radius:0.25rem;border-style:solid;padding:0px;cursor:pointer;line-height:1.5;font-size:12px;border-left-color:transparent;border-bottom-color:transparent;border-right-color:transparent;border-top-color:transparent;border-left-width:1px;border-bottom-width:1px;border-right-width:1px;border-top-width:1px;user-select:none;vertical-align:middle;white-space:nowrap;text-align:center;font-weight:bold;display:inline-block;background-color:transparent;color:rgb(100, 89, 116);">Unsubscribe</a> |
|
||||
|
||||
<a href="/contactus" class="btn btn-link o_default_snippet_text" style="text-decoration:none;border-radius:0.25rem;border-style:solid;padding:0px;cursor:pointer;line-height:1.5;font-size:12px;border-left-color:transparent;border-bottom-color:transparent;border-right-color:transparent;border-top-color:transparent;border-left-width:1px;border-bottom-width:1px;border-right-width:1px;border-top-width:1px;user-select:none;vertical-align:middle;white-space:nowrap;text-align:center;font-weight:bold;display:inline-block;background-color:transparent;color:rgb(100, 89, 116);">Contact</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left;vertical-align:middle;">
|
||||
<p class="o_mail_footer_copy" style="margin:0px 0 1rem 0;text-align:center;font-weight:bold;color:rgb(147, 146, 146);font-size:9px;">
|
||||
<img src="/web_editor/font_to_img/61945/rgb(147,146,146)/9" data-class="fa fa-copyright" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>2018 All Rights Reserved
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table align="center" cellspacing="0" cellpadding="0" class="o_mail_table_styles o_mail_full_width_padding" style="width:100%;border-collapse:separate;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="padding:16px 0 16px 0;" class="pt16 pb16">
|
||||
Powered by
|
||||
|
||||
<a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=email" style="text-decoration:none;background-color:transparent;color:#875A7B;">Odoo</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</field>
|
||||
<field name="attachment_ids" eval="[(4, ref('mass_mail_attach_1'))]"/>
|
||||
</record>
|
||||
|
||||
@@ -456,7 +456,9 @@ class MassMailing(models.Model):
|
||||
default=lambda self: self.env['mail.message']._get_default_from())
|
||||
sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
|
||||
schedule_date = fields.Datetime(string='Schedule in the Future')
|
||||
body_html = fields.Html(string='Body', sanitize_attributes=False)
|
||||
# don't translate 'body_arch', the translations are only on 'body_html'
|
||||
body_arch = fields.Html(string='Body', translate=False)
|
||||
body_html = fields.Html(string='Body converted to be send by mail', sanitize_attributes=False)
|
||||
attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
|
||||
'mass_mailing_id', 'attachment_id', string='Attachments')
|
||||
keep_archives = fields.Boolean(string='Keep Archives')
|
||||
@@ -621,7 +623,6 @@ class MassMailing(models.Model):
|
||||
else:
|
||||
mailing_domain.append((0, '=', 1))
|
||||
self.mailing_domain = repr(mailing_domain)
|
||||
self.body_html = "on_change_model_and_list"
|
||||
|
||||
@api.onchange('subject')
|
||||
def _onchange_subject(self):
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
odoo.define('mass_mailing.mass_mailing', function (require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanRecord = require('web.KanbanRecord');
|
||||
var KanbanColumn = require('web.KanbanColumn');
|
||||
|
||||
KanbanRecord.include({
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_openRecord: function () {
|
||||
if (this.modelName === 'mail.mass_mailing.campaign') {
|
||||
this.$('.oe_mailings').click();
|
||||
} else if (this.modelName === 'mail.mass_mailing.list' &&
|
||||
this.$('.o_mailing_list_kanban_boxes a')) {
|
||||
this.$('.o_mailing_list_kanban_boxes a').first().click();
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
KanbanColumn.include({
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.modelName === 'mail.mass_mailing') {
|
||||
this.draggable = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
odoo.define('mass_mailing.field_text_html', function (require) {
|
||||
|
||||
var FieldTextHtml = require('web_editor.backend').FieldTextHtml;
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
|
||||
var MassMailingFieldTextHtml = FieldTextHtml.extend({
|
||||
/**
|
||||
* The html_frame widget is opened in an iFrame that has its URL encoded
|
||||
* with all the key/values returned by this method.
|
||||
*
|
||||
* Some fields can get very long values and we want to omit them for the URL building
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getDatarecord: function () {
|
||||
return _.omit(this._super(), [
|
||||
'mailing_domain',
|
||||
'contact_list_ids',
|
||||
'body_html',
|
||||
'attachment_ids'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
fieldRegistry.add('mass_mailing_html_frame', MassMailingFieldTextHtml);
|
||||
|
||||
return MassMailingFieldTextHtml;
|
||||
|
||||
});
|
||||
@@ -1,18 +1,10 @@
|
||||
odoo.define('mass_mailing.editor', function (require) {
|
||||
odoo.define('mass_mailing.snippets.options', function (require) {
|
||||
"use strict";
|
||||
|
||||
require('web.dom_ready');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var rte = require('web_editor.rte');
|
||||
var options = require('web_editor.snippets.options');
|
||||
var snippets_editor = require('web_editor.snippet.editor');
|
||||
|
||||
var $editable_area = $('#editable_area');
|
||||
var odoo_top = window.top.odoo;
|
||||
|
||||
// Snippet option for resizing image and column width inline like excel
|
||||
options.registry.sizing_x = options.Class.extend({
|
||||
options.registry.mass_mailing_sizing_x = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -31,7 +23,7 @@ options.registry.sizing_x = options.Class.extend({
|
||||
this.$overlay.find(".oe_snippet_move, .oe_snippet_clone").addClass('d-none');
|
||||
}
|
||||
|
||||
var $body = $(document.body);
|
||||
var $body = $(this.ownerDocument.body);
|
||||
this.$overlay.find(".o_handle").on('mousedown', function (event) {
|
||||
event.preventDefault();
|
||||
var $handle = $(this);
|
||||
@@ -88,7 +80,7 @@ options.registry.sizing_x = options.Class.extend({
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.table_item = options.Class.extend({
|
||||
options.registry.mass_mailing_table_item = options.Class.extend({
|
||||
onClone: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
@@ -134,300 +126,16 @@ options.registry.table_item = options.Class.extend({
|
||||
},
|
||||
});
|
||||
|
||||
var fn_popover_update = $.summernote.eventHandler.modules.popover.update;
|
||||
$.summernote.eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
|
||||
fn_popover_update.call(this, $popover, oStyle, isAirMode);
|
||||
$("span.o_table_handler, div.note-table").remove();
|
||||
};
|
||||
|
||||
ajax.loadXML("/mass_mailing/static/src/xml/mass_mailing.xml", core.qweb);
|
||||
|
||||
snippets_editor.Class.include({
|
||||
_computeSnippetTemplates: function (html) {
|
||||
var self = this;
|
||||
var ret = this._super.apply(this, arguments);
|
||||
|
||||
var $themes = this.$("#email_designer_themes").children();
|
||||
if ($themes.length === 0) return ret;
|
||||
|
||||
/**
|
||||
* Initialize theme parameters.
|
||||
*/
|
||||
var all_classes = "";
|
||||
var themes_params = _.map($themes, function (theme) {
|
||||
var $theme = $(theme);
|
||||
var name = $theme.data("name");
|
||||
var classname = "o_" + name + "_theme";
|
||||
all_classes += " " + classname;
|
||||
var images_info = _.defaults($theme.data("imagesInfo") || {}, {all: {}});
|
||||
_.each(images_info, function (info) {
|
||||
info = _.defaults(info, images_info.all, {module: "mass_mailing", format: "jpg"});
|
||||
});
|
||||
return {
|
||||
name: name,
|
||||
className: classname || "",
|
||||
img: $theme.data("img") || "",
|
||||
template: $theme.html().trim(),
|
||||
nowrap: !!$theme.data('nowrap'),
|
||||
get_image_info: function (filename) {
|
||||
if (images_info[filename]) {
|
||||
return images_info[filename];
|
||||
}
|
||||
return images_info.all;
|
||||
}
|
||||
};
|
||||
});
|
||||
$themes.parent().remove();
|
||||
|
||||
var $body = $(document.body);
|
||||
var $snippets = this.$(".oe_snippet");
|
||||
var $snippets_menu = this.$el.find("#snippets_menu");
|
||||
|
||||
/**
|
||||
* Create theme selection screen and check if it must be forced opened.
|
||||
* Reforce it opened if the last snippet is removed.
|
||||
*/
|
||||
var $dropdown = $(core.qweb.render("mass_mailing.theme_selector", {
|
||||
themes: themes_params
|
||||
}));
|
||||
var first_choice;
|
||||
check_if_must_force_theme_choice();
|
||||
|
||||
/**
|
||||
* Add proposition to install enterprise themes if not installed.
|
||||
*/
|
||||
var $mail_themes_upgrade = $dropdown.find(".o_mass_mailing_themes_upgrade");
|
||||
$mail_themes_upgrade.on("click", function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
odoo_top[window.callback+"_do_action"]("mass_mailing.action_mass_mailing_configuration");
|
||||
});
|
||||
|
||||
/**
|
||||
* Switch theme when a theme button is hovered. Confirm change if the theme button
|
||||
* is pressed.
|
||||
*/
|
||||
var selected_theme = false;
|
||||
$dropdown.on("mouseenter", ".dropdown-item", function (e) {
|
||||
if (first_choice) return;
|
||||
e.preventDefault();
|
||||
var theme_params = themes_params[$(e.currentTarget).index()];
|
||||
switch_theme(theme_params);
|
||||
});
|
||||
$dropdown.on("click", ".dropdown-item", function (e) {
|
||||
e.preventDefault();
|
||||
var theme_params = themes_params[$(e.currentTarget).index()];
|
||||
if (first_choice) {
|
||||
switch_theme(theme_params);
|
||||
$body.removeClass("o_force_mail_theme_choice");
|
||||
first_choice = false;
|
||||
|
||||
if ($mail_themes_upgrade.length) {
|
||||
$dropdown.remove();
|
||||
$snippets_menu.empty();
|
||||
}
|
||||
}
|
||||
|
||||
switch_images(theme_params, $snippets);
|
||||
|
||||
selected_theme = theme_params;
|
||||
|
||||
// Notify form view
|
||||
odoo_top[window.callback+"_downup"]($editable_area.addClass("o_dirty").html());
|
||||
});
|
||||
|
||||
/**
|
||||
* If the user opens the theme selection screen, indicates which one is active and
|
||||
* saves the information...
|
||||
* ... then when the user closes check if the user confirmed its choice and restore
|
||||
* previous state if this is not the case.
|
||||
*/
|
||||
$dropdown.on("shown.bs.dropdown", function () {
|
||||
check_selected_theme();
|
||||
$dropdown.find(".dropdown-item").removeClass("selected").filter(function () {
|
||||
return ($(this).has(".o_thumb[style=\""+ "background-image: url(" + (selected_theme && selected_theme.img) + "_small.png)"+ "\"]").length > 0);
|
||||
}).addClass("selected");
|
||||
});
|
||||
$dropdown.on("hidden.bs.dropdown", function () {
|
||||
switch_theme(selected_theme);
|
||||
});
|
||||
|
||||
/**
|
||||
* On page load, check the selected theme and force switching to it (body needs the
|
||||
* theme style for its edition toolbar).
|
||||
*/
|
||||
check_selected_theme();
|
||||
$body.addClass(selected_theme.className);
|
||||
switch_images(selected_theme, $snippets);
|
||||
|
||||
$dropdown.insertAfter($snippets_menu);
|
||||
|
||||
return ret;
|
||||
|
||||
function check_if_must_force_theme_choice() {
|
||||
first_choice = editable_area_is_empty();
|
||||
$body.toggleClass("o_force_mail_theme_choice", first_choice);
|
||||
}
|
||||
|
||||
function editable_area_is_empty($layout) {
|
||||
$layout = $layout || $editable_area.find(".o_layout");
|
||||
var $mail_wrapper = $layout.children(".o_mail_wrapper");
|
||||
var $mail_wrapper_content = $mail_wrapper.find('.o_mail_wrapper_td');
|
||||
if (!$mail_wrapper_content.length) { // compatibility
|
||||
$mail_wrapper_content = $mail_wrapper;
|
||||
}
|
||||
return (
|
||||
$editable_area.html().trim() === ""
|
||||
|| ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper_content.length > 0 && $mail_wrapper_content.html().trim() === ""))
|
||||
);
|
||||
}
|
||||
|
||||
function check_selected_theme() {
|
||||
var $layout = $editable_area.find(".o_layout");
|
||||
if ($layout.length === 0) {
|
||||
selected_theme = false;
|
||||
} else {
|
||||
_.each(themes_params, function (theme_params) {
|
||||
if ($layout.hasClass(theme_params.className)) {
|
||||
selected_theme = theme_params;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function switch_images(theme_params, $container) {
|
||||
if (!theme_params) return;
|
||||
$container.find("img").each(function () {
|
||||
var $img = $(this);
|
||||
var src = $img.attr("src");
|
||||
|
||||
var 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;
|
||||
|
||||
var file = m[1];
|
||||
var img_info = theme_params.get_image_info(file);
|
||||
|
||||
if (img_info.format) {
|
||||
src = "/" + img_info.module + "/static/src/img/theme_" + theme_params.name + "/s_default_image_" + file + "." + img_info.format;
|
||||
} else {
|
||||
src = "/web/image/" + img_info.module + ".s_default_image_theme_" + theme_params.name + "_" + file;
|
||||
}
|
||||
|
||||
$img.attr("src", src);
|
||||
});
|
||||
}
|
||||
|
||||
function switch_theme(theme_params) {
|
||||
if (!theme_params || switch_theme.last === theme_params) return;
|
||||
switch_theme.last = theme_params;
|
||||
|
||||
$body.removeClass(all_classes).addClass(theme_params.className);
|
||||
|
||||
var $old_layout = $editable_area.find('.o_layout');
|
||||
|
||||
var $new_wrapper;
|
||||
var $new_wrapper_content;
|
||||
if (theme_params.nowrap) {
|
||||
$new_wrapper = $('<div/>', {class: 'oe_structure'});
|
||||
$new_wrapper_content = $new_wrapper;
|
||||
} else {
|
||||
// This wrapper structure is the only way to have a responsive
|
||||
// and centered fixed-width content column on all mail clients
|
||||
$new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
|
||||
$new_wrapper_content = $('<td/>', {class: 'o_mail_no_options o_mail_wrapper_td oe_structure'});
|
||||
$new_wrapper.append($('<tr/>').append(
|
||||
$('<td/>', {class: 'o_mail_no_resize o_not_editable', contenteditable: 'false'}),
|
||||
$new_wrapper_content,
|
||||
$('<td/>', {class: 'o_mail_no_resize o_not_editable', contenteditable: 'false'})
|
||||
));
|
||||
}
|
||||
var $new_layout = $('<div/>', {class: 'o_layout ' + theme_params.className}).append($new_wrapper);
|
||||
|
||||
var $contents;
|
||||
if (first_choice) {
|
||||
$contents = theme_params.template;
|
||||
} else if ($old_layout.length) {
|
||||
$contents = ($old_layout.hasClass('oe_structure') ? $old_layout : $old_layout.find('.oe_structure').first()).contents();
|
||||
} else {
|
||||
$contents = $editable_area.contents();
|
||||
}
|
||||
|
||||
$new_wrapper_content.append($contents);
|
||||
switch_images(theme_params, $new_wrapper_content);
|
||||
$editable_area.empty().append($new_layout);
|
||||
$old_layout.remove();
|
||||
|
||||
if (first_choice) {
|
||||
self._registerDefaultTexts($new_wrapper_content);
|
||||
if(theme_params.name == 'basic') {
|
||||
$editable_area.focusIn();
|
||||
}
|
||||
}
|
||||
self._disableUndroppableSnippets();
|
||||
}
|
||||
},
|
||||
cleanForSave: function () {
|
||||
this._super.apply(this, arguments);
|
||||
// remove font-family from all elements for plain text theme (just like gmail)
|
||||
var $basicTheme = this.$editable.find('.o_basic_theme');
|
||||
if($basicTheme.length && this.$editable.data('oe-model') === 'mail.mass_mailing') {
|
||||
this.$editable.find('*').css('font-family', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var callback = window ? window["callback"] : undefined;
|
||||
odoo_top[callback+"_updown"] = function (value, fields_values, field_name) {
|
||||
if (!window || window.closed) {
|
||||
delete odoo_top[callback+"_updown"];
|
||||
return;
|
||||
}
|
||||
|
||||
var $editable = $("#editable_area");
|
||||
var _val = $editable.prop("innerHTML");
|
||||
var editor_enable = $('body').hasClass('editor_enable');
|
||||
value = value || "";
|
||||
|
||||
if (value !==_val) {
|
||||
if (editor_enable) {
|
||||
if (value !== fields_values[field_name]) {
|
||||
rte.history.recordUndo($editable);
|
||||
}
|
||||
core.bus.trigger('deactivate_snippet');
|
||||
}
|
||||
|
||||
if (value.indexOf('on_change_model_and_list') === -1) {
|
||||
$editable.html(value);
|
||||
|
||||
if (editor_enable) {
|
||||
if (value !== fields_values[field_name]) {
|
||||
$editable.trigger("content_changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fields_values.mailing_model && editor_enable) {
|
||||
if (value.indexOf('on_change_model_and_list') !== -1) {
|
||||
odoo_top[callback+"_downup"](_val);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ($editable_area.html().indexOf('on_change_model_and_list') !== -1) {
|
||||
$editable_area.empty();
|
||||
}
|
||||
// Adding compatibility for the outlook compliance of mailings.
|
||||
// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
|
||||
options.registry.background.include({
|
||||
options.registry.background = options.registry.background.extend({
|
||||
start: function () {
|
||||
this._super();
|
||||
var $table_target = this.$target.find('table:first');
|
||||
if ($table_target) {
|
||||
this.$target = $table_target;
|
||||
if (this.snippets && this.snippets.split('.')[0] === "mass_mailing") {
|
||||
var $table_target = this.$target.find('table:first');
|
||||
if ($table_target.length) {
|
||||
this.$target = $table_target;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
odoo.define('mass_mailing.FieldHtml', function (require) {
|
||||
'use strict';
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var FieldHtml = require('web_editor.field.html');
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
var convertInline = require('web_editor.convertInline');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var MassMailingFieldHtml = FieldHtml.extend({
|
||||
xmlDependencies: (FieldHtml.prototype.xmlDependencies || []).concat(["/mass_mailing/static/src/xml/mass_mailing.xml"]),
|
||||
|
||||
custom_events: _.extend({}, FieldHtml.prototype.custom_events, {
|
||||
snippets_loaded: '_onSnippetsLoaded',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (!this.nodeOptions.snippets) {
|
||||
this.nodeOptions.snippets = 'mass_mailing.email_designer_snippets';
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Commit the change in 'style-inline' on an other field
|
||||
* nodeOptions:
|
||||
* - inline-field: fieldName to save the html value converted into inline code
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
commitChanges: function () {
|
||||
var self = this;
|
||||
var fieldName = this.nodeOptions['inline-field'];
|
||||
|
||||
if (this.$content.find('.o_basic_theme').length) {
|
||||
this.$content.find('*').css('font-family', '');
|
||||
}
|
||||
|
||||
var $editable = this.wysiwyg.getEditable();
|
||||
|
||||
return this.wysiwyg.save().then(function (isDirty) {
|
||||
self._isDirty = isDirty;
|
||||
|
||||
convertInline.attachmentThumbnailToLinkImg($editable);
|
||||
convertInline.fontToImg($editable);
|
||||
convertInline.classToStyle($editable);
|
||||
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: _.object([fieldName], [this._unWrap($editable.html())])
|
||||
});
|
||||
|
||||
if (self._isDirty && self.mode === 'edit') {
|
||||
return self._doAction();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* The html_frame widget is opened in an iFrame that has its URL encoded
|
||||
* with all the key/values returned by this method.
|
||||
*
|
||||
* Some fields can get very long values and we want to omit them for the URL building.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getDatarecord: function () {
|
||||
return _.omit(this._super(), [
|
||||
'mailing_domain',
|
||||
'contact_list_ids',
|
||||
'body_html',
|
||||
'attachment_ids'
|
||||
]);
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add generateOptions options to change wysiwyg configuration:
|
||||
* - Remove table menu in toolbar
|
||||
* - Remove table popover
|
||||
* - Add rules for isEditableNode, the TD must content a not TABLE node as
|
||||
* children to be editable.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_getWysiwygOptions: function () {
|
||||
var options = this._super();
|
||||
options.generateOptions = function (options) {
|
||||
options.toolbar = _.filter(options.toolbar, function (item) {
|
||||
return item[0] !== 'table';
|
||||
});
|
||||
delete options.popover.table;
|
||||
|
||||
var isEditableNode = options.isEditableNode;
|
||||
options.isEditableNode = function (node) {
|
||||
if (node.tagName === "TD" && !$(node).children('*:not(table):first').length) {
|
||||
return false;
|
||||
}
|
||||
return isEditableNode.call(this, node);
|
||||
};
|
||||
|
||||
return options;
|
||||
};
|
||||
return options;
|
||||
},
|
||||
/**
|
||||
* Returns true if must force the user to choose a theme.
|
||||
*
|
||||
* @private
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_checkIfMustForceThemeChoice: function () {
|
||||
var firstChoice = this._editableAreaIsEmpty();
|
||||
this.$content.closest('body').toggleClass("o_force_mail_theme_choice", firstChoice);
|
||||
return firstChoice;
|
||||
},
|
||||
/**
|
||||
* Returns true if the editable area is empty.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} [$layout]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_editableAreaIsEmpty: function ($layout) {
|
||||
$layout = $layout || this.$content.find(".o_layout");
|
||||
var $mailWrapper = $layout.children(".o_mailWrapper");
|
||||
var $mailWrapperContent = $mailWrapper.find('.o_mailWrapper_td');
|
||||
if (!$mailWrapperContent.length) { // compatibility
|
||||
$mailWrapperContent = $mailWrapper;
|
||||
}
|
||||
var value;
|
||||
if ($mailWrapperContent.length > 0) {
|
||||
value = $mailWrapperContent.html();
|
||||
} else if ($layout.length) {
|
||||
value = $layout.html();
|
||||
} else {
|
||||
value = this.wysiwyg.getValue();
|
||||
}
|
||||
var blankEditable = "<p><br></p>";
|
||||
return value === "" || value === blankEditable;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
this._isFromInline = !this.value;
|
||||
if (!this.value) {
|
||||
this.value = this.recordData[this.nodeOptions['inline-field']];
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
_renderTranslateButton: function () {
|
||||
var fieldName = this.nodeOptions['inline-field'];
|
||||
if (_t.database.multi_lang && this.record.fields[fieldName].translate && this.res_id) {
|
||||
return $('<button>', {
|
||||
type: 'button',
|
||||
'class': 'o_field_translate fa fa-globe btn btn-link',
|
||||
})
|
||||
.on('click', this._onTranslate.bind(this));
|
||||
}
|
||||
return $();
|
||||
},
|
||||
/**
|
||||
* Returns the selected theme, if any.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} themesParams
|
||||
* @returns {false|Object}
|
||||
*/
|
||||
_getSelectedTheme: function (themesParams) {
|
||||
var $layout = this.$content.find(".o_layout");
|
||||
var 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: function (themeParams, $container) {
|
||||
if (!themeParams) {
|
||||
return;
|
||||
}
|
||||
$container.find("img").each(function () {
|
||||
var $img = $(this);
|
||||
var src = $img.attr("src");
|
||||
|
||||
var 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;
|
||||
}
|
||||
|
||||
var file = m[1];
|
||||
var img_info = themeParams.get_image_info(file);
|
||||
|
||||
if (img_info.format) {
|
||||
src = "/" + img_info.module + "/static/src/img/theme_" + themeParams.name + "/s_default_image_" + file + "." + img_info.format;
|
||||
} else {
|
||||
src = "/web/image/" + img_info.module + ".s_default_image_theme_" + themeParams.name + "_" + file;
|
||||
}
|
||||
|
||||
$img.attr("src", src);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Switch themes or import first theme.
|
||||
*
|
||||
* @private
|
||||
* @param {Boolean} firstChoice true if this is the first chosen theme (going from no theme to a theme)
|
||||
* @param {Object} themeParams
|
||||
*/
|
||||
_switchThemes: function (firstChoice, themeParams) {
|
||||
if (!themeParams || this.switchThemeLast === themeParams) {
|
||||
return;
|
||||
}
|
||||
this.switchThemeLast = themeParams;
|
||||
|
||||
this.$content.closest('body').removeClass(this._allClasses).addClass(themeParams.className);
|
||||
|
||||
var $old_layout = this.$content.find('.o_layout');
|
||||
|
||||
var $new_wrapper;
|
||||
var $newWrapperContent;
|
||||
if (themeParams.nowrap) {
|
||||
$new_wrapper = $('<div/>', {
|
||||
class: 'oe_structure'
|
||||
});
|
||||
$newWrapperContent = $new_wrapper;
|
||||
} else {
|
||||
// This wrapper structure is the only way to have a responsive
|
||||
// and centered fixed-width content column on all mail clients
|
||||
$new_wrapper = $('<table/>', {
|
||||
class: 'o_mailWrapper'
|
||||
});
|
||||
$newWrapperContent = $('<td/>', {
|
||||
class: 'o_mail_no_options o_mailWrapper_td oe_structure'
|
||||
});
|
||||
$new_wrapper.append($('<tr/>').append(
|
||||
$('<td/>', {
|
||||
class: 'o_mail_no_resize o_not_editable',
|
||||
contenteditable: 'false'
|
||||
}),
|
||||
$newWrapperContent,
|
||||
$('<td/>', {
|
||||
class: 'o_mail_no_resize o_not_editable',
|
||||
contenteditable: 'false'
|
||||
})
|
||||
));
|
||||
}
|
||||
var $newLayout = $('<div/>', {
|
||||
class: 'o_layout ' + themeParams.className
|
||||
}).append($new_wrapper);
|
||||
|
||||
var $contents;
|
||||
if (firstChoice) {
|
||||
$contents = themeParams.template;
|
||||
} else if ($old_layout.length) {
|
||||
$contents = ($old_layout.hasClass('oe_structure') ? $old_layout : $old_layout.find('.oe_structure').first()).contents();
|
||||
} else {
|
||||
$contents = this.$content.find('.note-editable').contents();
|
||||
}
|
||||
|
||||
$newWrapperContent.append($contents);
|
||||
this._switchImages(themeParams, $newWrapperContent);
|
||||
this.$content.find('.note-editable').empty().append($newLayout);
|
||||
$old_layout.remove();
|
||||
|
||||
if (firstChoice) {
|
||||
$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.$content.focusIn();
|
||||
}
|
||||
}
|
||||
this.wysiwyg.snippets.trigger('reload_snippet_dropzones');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onLoadWysiwyg: function () {
|
||||
if (this._isFromInline) {
|
||||
this._fromInline();
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onSnippetsLoaded: function (ev) {
|
||||
var self = this;
|
||||
var $snippetsSideBar = ev.data;
|
||||
var $themes = $snippetsSideBar.find("#email_designer_themes").children();
|
||||
var $snippets = $snippetsSideBar.find(".oe_snippet");
|
||||
var $snippets_menu = $snippetsSideBar.find("#snippets_menu");
|
||||
|
||||
if (config.device.isMobile) {
|
||||
$snippetsSideBar.hide();
|
||||
console.log(this.$content[0]);
|
||||
this.$content.attr('style', 'padding-left: 0px !important');
|
||||
}
|
||||
|
||||
if ($themes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme parameters.
|
||||
*/
|
||||
this._allClasses = "";
|
||||
var themesParams = _.map($themes, function (theme) {
|
||||
var $theme = $(theme);
|
||||
var name = $theme.data("name");
|
||||
var classname = "o_" + name + "_theme";
|
||||
self._allClasses += " " + classname;
|
||||
var imagesInfo = _.defaults($theme.data("imagesInfo") || {}, {
|
||||
all: {}
|
||||
});
|
||||
_.each(imagesInfo, function (info) {
|
||||
info = _.defaults(info, imagesInfo.all, {
|
||||
module: "mass_mailing",
|
||||
format: "jpg"
|
||||
});
|
||||
});
|
||||
return {
|
||||
name: name,
|
||||
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;
|
||||
}
|
||||
};
|
||||
});
|
||||
$themes.parent().remove();
|
||||
|
||||
/**
|
||||
* Create theme selection screen and check if it must be forced opened.
|
||||
* Reforce it opened if the last snippet is removed.
|
||||
*/
|
||||
var $dropdown = $(core.qweb.render("mass_mailing.theme_selector", {
|
||||
themes: themesParams
|
||||
})).dropdown();
|
||||
|
||||
var firstChoice = this._checkIfMustForceThemeChoice();
|
||||
|
||||
/**
|
||||
* Add proposition to install enterprise themes if not installed.
|
||||
*/
|
||||
var $mail_themes_upgrade = $dropdown.find(".o_mass_mailing_themes_upgrade");
|
||||
$mail_themes_upgrade.on("click", function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
self.do_action("mass_mailing.action_mass_mailing_configuration");
|
||||
});
|
||||
|
||||
/**
|
||||
* Switch theme when a theme button is hovered. Confirm change if the theme button
|
||||
* is pressed.
|
||||
*/
|
||||
var selectedTheme = false;
|
||||
$dropdown.on("mouseenter", ".dropdown-item", function (e) {
|
||||
if (firstChoice) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
var themeParams = themesParams[$(e.currentTarget).index()];
|
||||
self._switchThemes(firstChoice, themeParams);
|
||||
});
|
||||
$dropdown.on("mouseleave", ".dropdown-item", function (e) {
|
||||
self._switchThemes(false, selectedTheme);
|
||||
});
|
||||
$dropdown.on("click", '[data-toggle="dropdown"]', function (e) {
|
||||
var $menu = $dropdown.find('.dropdown-menu');
|
||||
var isVisible = $menu.hasClass('show');
|
||||
if (isVisible) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
$menu.removeClass('show');
|
||||
}
|
||||
});
|
||||
|
||||
$dropdown.on("click", ".dropdown-item", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
var themeParams = themesParams[$(e.currentTarget).index()];
|
||||
if (firstChoice) {
|
||||
self._switchThemes(firstChoice, themeParams);
|
||||
self.$content.closest('body').removeClass("o_force_mail_theme_choice");
|
||||
firstChoice = false;
|
||||
|
||||
if ($mail_themes_upgrade.length) {
|
||||
$dropdown.remove();
|
||||
$snippets_menu.empty();
|
||||
}
|
||||
}
|
||||
|
||||
self._switchImages(themeParams, $snippets);
|
||||
|
||||
selectedTheme = themeParams;
|
||||
|
||||
// Notify form view
|
||||
self.wysiwyg._onChange();
|
||||
$dropdown.find('.dropdown-menu').removeClass('show');
|
||||
$dropdown.find('.dropdown-item.selected').removeClass('selected');
|
||||
$dropdown.find('.dropdown-item:eq(' + themesParams.indexOf(selectedTheme) + ')').addClass('selected');
|
||||
});
|
||||
|
||||
/**
|
||||
* If the user opens the theme selection screen, indicates which one is active and
|
||||
* saves the information...
|
||||
* ... then when the user closes check if the user confirmed its choice and restore
|
||||
* previous state if this is not the case.
|
||||
*/
|
||||
$dropdown.on("shown.bs.dropdown", function () {
|
||||
selectedTheme = self._getSelectedTheme(themesParams);
|
||||
$dropdown.find(".dropdown-item").removeClass("selected").filter(function () {
|
||||
return ($(this).has(".o_thumb[style=\"" + "background-image: url(" + (selectedTheme && selectedTheme.img) + "_small.png)" + "\"]").length > 0);
|
||||
}).addClass("selected");
|
||||
});
|
||||
$dropdown.on("hidden.bs.dropdown", function () {
|
||||
self._switchThemes(firstChoice, selectedTheme);
|
||||
});
|
||||
|
||||
/**
|
||||
* On page load, check the selected theme and force switching to it (body needs the
|
||||
* theme style for its edition toolbar).
|
||||
*/
|
||||
selectedTheme = this._getSelectedTheme(themesParams);
|
||||
if (selectedTheme) {
|
||||
this.$content.closest('body').addClass(selectedTheme.className);
|
||||
$dropdown.find('.dropdown-item:eq(' + themesParams.indexOf(selectedTheme) + ')').addClass('selected');
|
||||
this._switchImages(selectedTheme, $snippets);
|
||||
} else if (this.$content.find('.o_layout').length) {
|
||||
themesParams.push({
|
||||
name: 'o_mass_mailing_no_theme',
|
||||
className: 'o_mass_mailing_no_theme',
|
||||
img: "",
|
||||
template: this.$content.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);
|
||||
}
|
||||
|
||||
$dropdown.insertAfter($snippets_menu);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onTranslate: function () {
|
||||
this.trigger_up('translate', {
|
||||
fieldName: this.nodeOptions['inline-field'],
|
||||
id: this.dataPointID
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
fieldRegistry.add('mass_mailing_html', MassMailingFieldHtml);
|
||||
|
||||
return MassMailingFieldHtml;
|
||||
|
||||
});
|
||||
@@ -9,17 +9,18 @@
|
||||
.o_mail_theme_selector {
|
||||
> a {
|
||||
@include o-position-absolute(0, 0, auto, 0);
|
||||
height: $o-navbar-height;
|
||||
line-height: 31px;
|
||||
height: $o-wysiwyg-toolbar-height;
|
||||
line-height: $o-wysiwyg-toolbar-height;
|
||||
border-radius: 0;
|
||||
background-color: $o-we-color-darker;
|
||||
color: $o-we-color-text-normal;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
color: $o-wysiwyg-color;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: $o-we-color-text-light;
|
||||
color: $o-wysiwyg-color-light;
|
||||
}
|
||||
|
||||
i {
|
||||
@@ -29,15 +30,20 @@
|
||||
|
||||
&.show > a {
|
||||
color: white;
|
||||
background-color: $o-we-color-dark;
|
||||
background-color: $o-wysiwyg-color;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@include o-position-absolute($o-navbar-height - 1, -1px, -1px, -1px);
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
top: -3px !important;
|
||||
left: -5px !important;
|
||||
right: 3px !important;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
overflow: auto;
|
||||
background-color: $o-we-color-dark;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
z-index: 1050;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 10px 10px;
|
||||
@@ -49,8 +55,8 @@
|
||||
display: none;
|
||||
background-size: cover;
|
||||
padding-top: 50%;
|
||||
border: 1px solid $o-we-color-light;
|
||||
border-top: 1px solid $o-we-color-text-normal;
|
||||
border: 1px solid $o-wysiwyg-border-color;
|
||||
|
||||
|
||||
&.logo {
|
||||
display: block;
|
||||
@@ -59,16 +65,16 @@
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: $o-we-color-darker;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
|
||||
.o_thumb {
|
||||
border: 1px solid white;
|
||||
border: 1px solid black;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected .o_thumb {
|
||||
border: 2px solid $o-brand-odoo;
|
||||
background-color: $o-we-color-dark;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,8 +107,8 @@ body.o_force_mail_theme_choice {
|
||||
.o_thumb {
|
||||
display: none;
|
||||
padding-top: 107%;
|
||||
border: 1px solid $o-we-color-light;
|
||||
border-top: 1px solid $o-we-color-text-normal;
|
||||
border: 1px solid $o-wysiwyg-color-light;
|
||||
border-top: 1px solid $o-wysiwyg-border-color;
|
||||
box-shadow: 0 5px 10px rgba(black, 0.8);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
@@ -123,7 +129,7 @@ body.o_force_mail_theme_choice {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $o-we-color-dark;
|
||||
background-color: $o-wysiwyg-color;
|
||||
|
||||
.o_thumb {
|
||||
box-shadow: 0 5px 30px 1px rgba(black, 0.6);
|
||||
@@ -157,16 +163,29 @@ body.o_force_mail_theme_choice {
|
||||
}
|
||||
}
|
||||
}
|
||||
.note-editor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#web_editor_inside_iframe {
|
||||
&.o_basic_theme {
|
||||
padding-left: 0px !important;
|
||||
#oe_snippets {
|
||||
display: none;
|
||||
}
|
||||
.o_basic_theme.o_in_iframe {
|
||||
padding-left: 0px !important;
|
||||
#oe_snippets {
|
||||
display: none;
|
||||
}
|
||||
#wrapwrap > main #editable_area {
|
||||
padding: 24px;
|
||||
.note-editor.o_snippets_loaded {
|
||||
padding-left: 0px!important;
|
||||
}
|
||||
}
|
||||
|
||||
.note-editable .o_layout {
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.oe_structure {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ $o-mm-def-color-eta: #87a6b5;
|
||||
$o-mm-def-body-width : 600px;
|
||||
$o-mm-def-body-bobile : 480px;
|
||||
$o-mm-def-b-radius : 2px;
|
||||
$o-mm-def-body-bg : $o-mm-def-color-beta;
|
||||
|
||||
$o-mm-def-font : -apple-system, "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
$o-mm-def-text-color : $o-mm-def-color-delta;
|
||||
@@ -37,9 +38,9 @@ $o-mm-def-btn-text : $o-mm-def-color-beta;
|
||||
background-color: $o-mm-def-color-alpha;
|
||||
}
|
||||
|
||||
td {
|
||||
td:not([align]) {
|
||||
// Default browser style but needed so that alignment works on some mail
|
||||
// clients (see transcoder)
|
||||
// clients (see convert_inline)
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
@@ -54,10 +55,10 @@ td {
|
||||
|
||||
// Forces on <p/> and <hx/> elements as several mail clients does not
|
||||
// correctly inherit font properties
|
||||
&, p {
|
||||
&, p, ol {
|
||||
font-size: 14px; // force bootstrap default
|
||||
}
|
||||
&, p, h1, h2, h3, h4, h5, h6 {
|
||||
&, p, h1, h2, h3, h4, h5, h6, ol {
|
||||
font-family: $o-mm-def-font;
|
||||
color: lighten($o-mm-def-text-color, 20%);
|
||||
}
|
||||
@@ -81,6 +82,7 @@ td {
|
||||
width: 100%;
|
||||
max-width: $o-mm-def-body-width; // should not be necessary thanks to mail wrapper
|
||||
margin: 0 auto;
|
||||
background-color: $o-mm-def-body-bg;
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
color: $o-mm-def-text-color;
|
||||
|
||||
@@ -6,42 +6,21 @@
|
||||
<t t-call="mass_mailing.mass_mailing_mail_style"/>
|
||||
</template>
|
||||
|
||||
<template id="FieldTextHtmlInline" name="Editor layout">
|
||||
<t t-call="web_editor.layout">
|
||||
<t t-set="head_mm_field_text_html_inline">
|
||||
<t t-if="enable_editor">
|
||||
<t t-call-assets="mass_mailing.assets_mail_themes"/>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_editor.js"></script>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<link rel="stylesheet" href="/mass_mailing/static/src/css/basic_theme_readonly.css" type="text/css"/>
|
||||
</t>
|
||||
<base target="_parent"/>
|
||||
</t>
|
||||
<t t-set="head" t-value="head_mm_field_text_html_inline + (head or '')"/>
|
||||
|
||||
<div t-if="not edit_translations" id="editable_area" class="o_editable o_mail_area" t-att-data-oe-model="model" t-att-data-oe-field="field" t-att-data-oe-id="res_id" data-oe-type="html"><t t-raw="content"/></div>
|
||||
<div t-if="edit_translations" id="editable_area" class="o_mail_area"><t t-raw="content"/></div>
|
||||
</t>
|
||||
<template id="assets_mail_themes_edition"> <!-- maybe to remove and convert into a field dumy with attr invisible if the template is not selected -->
|
||||
<t t-call="web._assets_helpers"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/webclient.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/wysiwyg_variables.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.ui.scss"/>
|
||||
</template>
|
||||
|
||||
<template id="FieldTextHtmlPopupContent" name="Editor layout">
|
||||
<t t-call="web_editor.layout">
|
||||
<t t-set="head">
|
||||
<link rel="stylesheet" href="/mass_mailing/static/src/css/mass_mailing_popup.css" type="text/css"/>
|
||||
</t>
|
||||
<div role="dialog" class="modal-dialog modal-md d-block" id="o_newsletter_popup">
|
||||
<div class="modal-content o_popup_modal_content">
|
||||
<div class="o_popup_modal_body text-center">
|
||||
<div class="o_popup_content_dev">
|
||||
<div id="editable_area" class="o_editable o_mail_area" t-att-data-oe-model="model" t-att-data-oe-field="field" t-att-data-oe-id="res_id" data-oe-type="html">
|
||||
<!-- Can be removed once default values are ok for the editor -->
|
||||
<t t-raw="content"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="iframe_css_assets_edit">
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-js="false"/>
|
||||
<t t-call-assets="mass_mailing.assets_mail_themes" t-js="false"/>
|
||||
<t t-call-assets="mass_mailing.assets_mail_themes_edition" t-js="false"/>
|
||||
</template>
|
||||
|
||||
<template id="iframe_css_assets_readonly">
|
||||
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/css/basic_theme_readonly.css"/>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.scss"/>
|
||||
<link rel="stylesheet" href="/mass_mailing/static/src/css/email_template.css"/>
|
||||
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing.js"></script>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_widget.js"></script>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_snippets.js"></script>
|
||||
</xpath>
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js"></script>
|
||||
@@ -12,14 +16,7 @@
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_record.js"></script>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_renderer.js"></script>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_view.js"></script>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_field_text_html.js"></script>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/unsubscribe.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_editor" inherit_id="web_editor.assets_editor">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.ui.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -611,7 +611,14 @@
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Mail Body">
|
||||
<field name="body_html" class="o_mail_body" widget="mass_mailing_html_frame" options="{'editor_url': '/mass_mailing/field/email_template'}"/>
|
||||
<field name="body_html" class="oe_read_only" widget="html" options="{
|
||||
'cssReadonly': 'mass_mailing.iframe_css_assets_readonly',
|
||||
}"/>
|
||||
<field name="body_arch" class="o_mail_body oe_edit_only" widget="mass_mailing_html" options="{
|
||||
'snippets': 'mass_mailing.email_designer_snippets',
|
||||
'cssEdit': 'mass_mailing.iframe_css_assets_edit',
|
||||
'inline-field': 'body_html'
|
||||
}"/>
|
||||
</page>
|
||||
<page string="Options">
|
||||
<group>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- Snippets & Themes Menu -->
|
||||
<template id="email_designer_snippets" inherit_id="web_editor.snippets" primary="True">
|
||||
<xpath expr="//h2[@id='snippets_menu']" position="replace">
|
||||
<t t-set="company_id" t-value="res_company"/>
|
||||
<h2 id="snippets_menu">Select a template</h2>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='o_scroll']" position="replace">
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<t t-call="web_editor.snippet_options"/>
|
||||
<t t-raw="0"/>
|
||||
|
||||
<div data-js="sizing_x"
|
||||
<div data-js="mass_mailing_sizing_x"
|
||||
data-selector="img, .mv, .col_mv, td, th"
|
||||
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
|
||||
|
||||
<div data-js="table_item"
|
||||
<div data-js="mass_mailing_table_item"
|
||||
data-selector="td, th"
|
||||
data-exclude=".o_mail_no_options"/>
|
||||
|
||||
@@ -29,17 +29,17 @@
|
||||
data-drop-near=".col_mv, td, th"/>
|
||||
|
||||
<div data-js="content"
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, .oe_snippet_body"
|
||||
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body"
|
||||
data-exclude=".o_mail_no_options"
|
||||
data-drop-near="[data-oe-field='body_html']:not(:has(.o_layout)) > *, .oe_structure > *"
|
||||
data-drop-in="[data-oe-field='body_html']:not(:has(.o_layout)), .oe_structure"/>
|
||||
|
||||
<div data-js="sizing_y"
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, td, th"
|
||||
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
|
||||
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
|
||||
|
||||
<div data-js="colorpicker"
|
||||
data-selector="[data-oe-field='body_html'] > div:not(.o_layout), [data-oe-field='body_html'] .oe_structure > div, td, th"
|
||||
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
|
||||
data-exclude=".o_mail_no_colorpicker, .o_mail_no_options">
|
||||
<div class="dropdown-submenu">
|
||||
<a tabindex="-1" href="#" class="dropdown-item"><i class="fa fa-eyedropper"/>Background Color</a>
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
<t t-set="head">
|
||||
<t t-call-assets="web.assets_common"/>
|
||||
<t t-call-assets="mass_mailing.assets_backend"/>
|
||||
<script type="text/javascript" src="/mass_mailing/static/src/js/unsubscribe.js"></script>
|
||||
</t>
|
||||
<body class="o_white_body">
|
||||
<header>
|
||||
|
||||
@@ -3,69 +3,67 @@
|
||||
<data noupdate="1">
|
||||
<record id="note_1" model="note.note">
|
||||
<field name="name">Customer report #349872</field>
|
||||
<field name="memo"><![CDATA[<b>Customer report #349872</b>
|
||||
<br/><br/>* Calendar app in Home
|
||||
<br/>* The calendar module should create a menu in Home, like described above.
|
||||
<br/>* This module should become a main application (in the first screen at installation)
|
||||
<br/>* We should use the term Calendar, not Meeting.
|
||||
]]>
|
||||
<field name="memo"><![CDATA[<p><b>Customer report #349872</b></p>
|
||||
<p><br/></p>
|
||||
<p>* Calendar app in Home</p>
|
||||
<p>* The calendar module should create a menu in Home, like described above.</p>
|
||||
<p>* This module should become a main application (in the first screen at installation)</p>
|
||||
<p>* We should use the term Calendar, not Meeting.</p>]]>
|
||||
</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="color">2</field>
|
||||
</record>
|
||||
|
||||
<record id="note_2" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>Call Raoulette</b>
|
||||
<br/><br/>* Followed by the telephone conversation and mail about D.544.3
|
||||
]]>
|
||||
<field name="memo"><![CDATA[<p><b>Call Raoulette</b></p>
|
||||
<p><br/></p>
|
||||
<p>* Followed by the telephone conversation and mail about D.544.3</p>]]>
|
||||
</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="note_4" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>Project N.947.5</b>
|
||||
<br/><br/>]]>
|
||||
<field name="memo"><![CDATA[<p><b>Project N.947.5</b></p>]]>
|
||||
</field>
|
||||
<field name="stage_id" ref="note_stage_02"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<record id="note_5" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>Shop for family dinner</b>
|
||||
<br/>* stuffed turkey
|
||||
<br/>* wine
|
||||
]]>
|
||||
<field name="memo"><![CDATA[<p><b>Shop for family dinner</b></p>
|
||||
<p>* stuffed turkey</p>
|
||||
<p>* wine</p>]]>
|
||||
</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="note_6" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>Idea to develop</b>
|
||||
<br/><br/>* Create a module note_pad
|
||||
it transforms the html editable memo text field into widget='pad', similar to project_pad depends on 'memo' and 'pad' modules
|
||||
]]>
|
||||
<field name="memo"><![CDATA[<p><b>Idea to develop</b></p>
|
||||
<p><br/></p>
|
||||
<p>* Create a module note_pad
|
||||
it transforms the html editable memo text field into widget='pad', similar to project_pad depends on 'memo' and 'pad' modules</p>]]>
|
||||
</field>
|
||||
<field name="stage_id" ref="note_stage_02"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<record id="note_8" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>New computer specs</b>
|
||||
<br/><br/>* Motherboard
|
||||
<field name="memo"><![CDATA[<p><b>New computer specs</b></p>
|
||||
<p><br/></p>
|
||||
<p>* Motherboard
|
||||
according to processor
|
||||
<br/>* Processor
|
||||
</p><p>* Processor
|
||||
need to decide
|
||||
<br/>* Graphic card
|
||||
</p><p>* Graphic card
|
||||
with great performance for games !
|
||||
<br/>* Hard drive
|
||||
</p><p>* Hard drive
|
||||
big, for lot of internet backups
|
||||
<br/>* Tower
|
||||
</p><p>* Tower
|
||||
silent, better when watching films
|
||||
<br/>* Blueray drive ?
|
||||
</p><p>* Blueray drive ?
|
||||
is it interesting yet ?
|
||||
<br/>* Screen
|
||||
a big one, full of pixels, of course !
|
||||
]]>
|
||||
</p><p>* Screen
|
||||
a big one, full of pixels, of course !</p>]]>
|
||||
</field>
|
||||
<field name="stage_id" ref="note_stage_03"/>
|
||||
<field name="color">3</field>
|
||||
@@ -73,19 +71,19 @@
|
||||
</record>
|
||||
|
||||
<record id="note_9" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>Read those books</b>
|
||||
<br/><br/>* Odoo: a modern approach to integrated business management
|
||||
<br/>* Odoo for Retail and Industrial Management
|
||||
]]>
|
||||
<field name="memo"><![CDATA[<p><b>Read those books</b></p>
|
||||
<p><br/></p>
|
||||
<p>* Odoo: a modern approach to integrated business management</p>
|
||||
<p>* Odoo for Retail and Industrial Management</p>]]>
|
||||
</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="note_12" model="note.note">
|
||||
<field name="memo"><![CDATA[<b>Read some documentation about Odoo before diving into the code</b>
|
||||
<br/><br/>* Odoo: a modern approach to integrated business management
|
||||
<br/>* Odoo for Retail and Industrial Management
|
||||
]]>
|
||||
<field name="memo"><![CDATA[<p><b>Read some documentation about Odoo before diving into the code</b></p>
|
||||
<p><br/></p>
|
||||
<p>* Odoo: a modern approach to integrated business management</p>
|
||||
<p>* Odoo for Retail and Industrial Management</p>]]>
|
||||
</field>
|
||||
<field name="color">7</field>
|
||||
<field name="stage_ids" eval="['note_stage_03']"/>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
from . import main
|
||||
from . import web_editor
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.web_editor.controllers.main import Web_Editor
|
||||
|
||||
|
||||
class WebEditorPointOfSale(Web_Editor):
|
||||
@http.route('/point_of_sale/field/customer_facing_display_template', type='http', auth="user")
|
||||
def get_field_text_html(self, model=None, res_id=None, field=None, callback=None, **kwargs):
|
||||
kwargs['snippets'] = '/point_of_sale/snippets'
|
||||
kwargs['template'] = 'point_of_sale.FieldTextHtml'
|
||||
extra_head = request.env.ref('point_of_sale.extra_head').render(None)
|
||||
|
||||
return self.FieldTextHtml(model=model, res_id=res_id, field=field, callback=callback, head=extra_head, **kwargs)
|
||||
|
||||
@http.route(['/point_of_sale/snippets'], type='json', auth="user", website=True)
|
||||
def get_snippets(self):
|
||||
return request.env.ref('point_of_sale.customer_facing_display_snippets').render(None)
|
||||
@@ -450,9 +450,6 @@ body.editor_enable .pos-hidden {
|
||||
top: 35px;
|
||||
}
|
||||
}
|
||||
body.editor_enable #web_editor-toolbars .btn-group:not(.note-history) {
|
||||
display: none;
|
||||
}
|
||||
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > ul {
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
odoo.define('point_of_sale.fieldtexthtml', function (require) {
|
||||
'use strict';
|
||||
|
||||
var FieldTextHtml = require('web_editor.backend').FieldTextHtml;
|
||||
|
||||
FieldTextHtml.include({
|
||||
// avoid '414 Request-URI Too Large' errors to
|
||||
// /point_of_sale/field/customer_facing_display_template by
|
||||
// filtering out biggest fields
|
||||
getDatarecord: function () {
|
||||
var datarecord = this._super();
|
||||
if (this.model === 'pos.config') {
|
||||
datarecord = _.omit(datarecord, function (val, key) {
|
||||
return _.isObject(val) || key === 'customer_facing_display_html';
|
||||
});
|
||||
}
|
||||
return datarecord;
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -432,13 +432,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// =========== EDITOR CUSTOMIZATIONS ===========
|
||||
#web_editor-toolbars {
|
||||
.btn-group:not(.note-history) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > ul {
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
|
||||
@@ -25,23 +25,10 @@
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/tests.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="FieldTextHtml">
|
||||
<t t-set="head">
|
||||
<t t-raw="head"/>
|
||||
<t t-if="enable_editor">
|
||||
<link rel="stylesheet" href="/point_of_sale/static/src/css/customer_facing_display_editor_thumb.css" type="text/css"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="web_editor.layout">
|
||||
<div id="editable_area"
|
||||
class="o_editable"
|
||||
t-att-data-oe-model="model"
|
||||
t-att-data-oe-field="field"
|
||||
t-att-data-oe-id="res_id"
|
||||
data-oe-type="html">
|
||||
<t t-raw="content" />
|
||||
</div>
|
||||
</t>
|
||||
<template id="wysiwyg_iframe_css_assets" name="CSS assets for wysiwyg iframe content">
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_backend" t-js="false"/>
|
||||
<link rel="stylesheet" href="/point_of_sale/static/src/css/customer_facing_display_editor_thumb.css" type="text/css"/>
|
||||
</template>
|
||||
|
||||
<template id="customer_facing_display_html" >
|
||||
|
||||
@@ -181,7 +181,10 @@
|
||||
<div class="text-muted" attrs="{'invisible' : ['|', ('iface_customer_facing_display', '=', False), '&', ('id', '!=', False), ('customer_facing_display_html', '!=', '')]}">Save this configuration to see and edit the customer display</div>
|
||||
<div class="row mt16 o_settings_container" attrs="{'invisible' : ['|', '|',('iface_customer_facing_display', '=', False), ('id', '=', False), ('customer_facing_display_html', '=', '')]}">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<field name="customer_facing_display_html" widget="html_frame" nolabel="1" options="{'editor_url': '/point_of_sale/field/customer_facing_display_template'}"/>
|
||||
<field name="customer_facing_display_html" widget="html" nolabel="1" options="{
|
||||
'snippets': 'point_of_sale.customer_facing_display_snippets',
|
||||
'cssEdit': 'point_of_sale.wysiwyg_iframe_css_assets'
|
||||
}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
|
||||
<t t-call-assets="web.assets_common" t-css="false"/>
|
||||
<t t-call-assets="web.assets_backend" t-css="false"/>
|
||||
<t t-call-assets="web_editor.summernote" t-css="false"/>
|
||||
<t t-call-assets="web_editor.assets_editor" t-css="false"/>
|
||||
<t t-call-assets="point_of_sale.assets"/>
|
||||
|
||||
<script type="text/javascript" id="loading-script" t-raw="init">
|
||||
@@ -71,13 +69,7 @@
|
||||
</script>
|
||||
</template>
|
||||
|
||||
<template id="pos_editor_fieldtexthtml_assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/field_text_html.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="pos_editor_assets" inherit_id="web_editor.assets_editor">
|
||||
<template id="pos_editor_assets" inherit_id="web_editor.wysiwyg_snippets">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/pos.web_editor.js"/>
|
||||
</xpath>
|
||||
|
||||
@@ -103,43 +103,43 @@ a, .btn-link {
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
li > ul {
|
||||
ul ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
li > * > li > ul {
|
||||
ul ul ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
li > * > li > * > li > ul {
|
||||
ul ul ul ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
li > * > li > * > li > * > li > ul {
|
||||
ul ul ul ul ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
li > * > li > * > li > * > li > * > li > ul {
|
||||
ul ul ul ul ul ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
li > * > li > * > li > * > li > * > li > * > li > ul {
|
||||
ul ul ul ul ul ul ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
li > ol {
|
||||
ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
li > * > li > ol {
|
||||
ol ol ol {
|
||||
list-style-type: lower-greek;
|
||||
}
|
||||
li > * > li > * > li > ol {
|
||||
ol ol ol ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
li > * > li > * > li > * > li > ol {
|
||||
ol ol ol ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
li > * > li > * > li > * > li > * > li > ol {
|
||||
ol ol ol ol ol ol {
|
||||
list-style-type: lower-greek;
|
||||
}
|
||||
li > * > li > * > li > * > li > * > li > * > li > ol {
|
||||
ol ol ol ol ol ol ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
li > p {
|
||||
|
||||
@@ -129,7 +129,8 @@ var ProductConfiguratorFormController = FormController.extend({
|
||||
pricelistId: self.renderer.pricelistId,
|
||||
okButtonText: _t('Confirm'),
|
||||
cancelButtonText: _t('Back'),
|
||||
title: _t('Configure')
|
||||
title: _t('Configure'),
|
||||
context: self.initialState.context,
|
||||
}).open();
|
||||
|
||||
self.optionalProductsModal.on('options_empty', null,
|
||||
|
||||
@@ -5,7 +5,6 @@ var ajax = require('web.ajax');
|
||||
var Dialog = require('web.Dialog');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
|
||||
var weContext = require('web_editor.context');
|
||||
|
||||
var productNameMap = {};
|
||||
var optionalProductsMap = {};
|
||||
@@ -62,6 +61,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi
|
||||
|
||||
this._super(parent, options);
|
||||
|
||||
this.context = params.context;
|
||||
this.rootProduct = params.rootProduct;
|
||||
this.container = parent;
|
||||
this.pricelistId = params.pricelistId;
|
||||
@@ -88,7 +88,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi
|
||||
kwargs: {
|
||||
context: _.extend({
|
||||
'quantity': self.rootProduct.quantity
|
||||
}, weContext.get()),
|
||||
}, this.context),
|
||||
}
|
||||
})
|
||||
.then(function (modalContent) {
|
||||
|
||||
@@ -100,7 +100,7 @@ var ProjectPlan = AbstractAction.extend({
|
||||
_updateControlPanel: function (buttons) {
|
||||
// set actions button
|
||||
if (this.$buttons) {
|
||||
this.$buttons.off().destroy();
|
||||
this.$buttons.off().remove();
|
||||
}
|
||||
buttons = buttons || [];
|
||||
this.$buttons = $(QWeb.render("project.plan.ControlButtons", {'buttons': buttons}));
|
||||
|
||||
@@ -6,7 +6,6 @@ var core = require('web.core');
|
||||
var time = require('web.time');
|
||||
var ajax = require('web.ajax');
|
||||
var base = require('web_editor.base');
|
||||
var context = require('web_editor.context');
|
||||
var field_utils = require('web.field_utils');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -166,7 +165,7 @@ if(!the_form.length) {
|
||||
});
|
||||
|
||||
function load_locale(){
|
||||
var url = "/web/webclient/locale/" + context.get().lang || 'en_US';
|
||||
var url = "/web/webclient/locale/" + (document.documentElement.getAttribute('lang') || 'en_US').replace('-', '_');
|
||||
return ajax.loadJS(url);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
odoo.define('web.ajax', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var utils = require('web.utils');
|
||||
var time = require('web.time');
|
||||
@@ -503,6 +504,55 @@ var loadXML = (function () {
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Loads a template file according to the given xmlId.
|
||||
*
|
||||
* @param {string} [xmlId] - the template xmlId
|
||||
* @returns {Deferred} resolved with an object
|
||||
* cssLibs: list of css files
|
||||
* cssContents: list of style tag contents
|
||||
* jsLibs: list of JS files
|
||||
* jsContents: list of script tag contents
|
||||
*/
|
||||
var loadAsset = (function () {
|
||||
var cache = {};
|
||||
|
||||
var load = function loadAsset(xmlId) {
|
||||
if (cache[xmlId]) {
|
||||
return $.when(cache[xmlId]);
|
||||
}
|
||||
var params = {
|
||||
args: [xmlId, {
|
||||
debug: config.debug
|
||||
}],
|
||||
kwargs: {
|
||||
context: odoo.session_info.user_context,
|
||||
},
|
||||
method: 'render_template',
|
||||
model: 'ir.ui.view',
|
||||
};
|
||||
return rpc('/web/dataset/call_kw/ir.ui.view/render_template', params).then(function (xml) {
|
||||
var $xml = $(xml);
|
||||
cache[xmlId] = {
|
||||
cssLibs: $xml.filter('link[href]:not([type="image/x-icon"])').map(function () {
|
||||
return $(this).attr('href');
|
||||
}).get(),
|
||||
cssContents: $xml.filter('style').map(function () {
|
||||
return $(this).html();
|
||||
}).get(),
|
||||
jsLibs: $xml.filter('script[src]').map(function () {
|
||||
return $(this).attr('src');
|
||||
}).get(),
|
||||
jsContents: $xml.filter('script:not([src])').map(function () {
|
||||
return $(this).html();
|
||||
}).get(),
|
||||
};
|
||||
return cache[xmlId];
|
||||
});
|
||||
};
|
||||
|
||||
return load;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Loads the given js and css libraries. Note that the ajax loadJS and loadCSS methods
|
||||
@@ -515,6 +565,8 @@ var loadXML = (function () {
|
||||
* parallel.
|
||||
* @param {Array<string>} [libs.cssLibs=[]] A list of css files, to be loaded in
|
||||
* parallel
|
||||
* @param {Array<string>} [libs.assetLibs=[]] A list of xmlId. The loaded template
|
||||
* contains the script and link to be loaded
|
||||
*
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
@@ -534,6 +586,11 @@ function loadLibs (libs) {
|
||||
_.each(libs.cssLibs || [], function (url) {
|
||||
defs.push(ajax.loadCSS(url));
|
||||
});
|
||||
_.each(libs.assetLibs || [], function (xmlId) {
|
||||
defs.push(loadAsset(xmlId).then(function (asset) {
|
||||
return loadLibs(asset);
|
||||
}));
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
}
|
||||
|
||||
@@ -544,6 +601,7 @@ var ajax = {
|
||||
loadCSS: loadCSS,
|
||||
loadJS: loadJS,
|
||||
loadXML: loadXML,
|
||||
loadAsset: loadAsset,
|
||||
loadLibs: loadLibs,
|
||||
get_file: get_file,
|
||||
post: post,
|
||||
|
||||
@@ -104,8 +104,13 @@ var Domain = collections.Tree.extend({
|
||||
);
|
||||
case "like":
|
||||
return (fieldValue.toLowerCase().indexOf(this._data[2].toLowerCase()) >= 0);
|
||||
case "=like":
|
||||
var regExp = new RegExp(this._data[2].toLowerCase().replace(/([.\[\]\{\}\+\*])/g, '\\\$1').replace(/%/g, '.*'));
|
||||
return regExp.test(fieldValue.toLowerCase());
|
||||
case "ilike":
|
||||
return (fieldValue.indexOf(this._data[2]) >= 0);
|
||||
case "=ilike":
|
||||
return new RegExp(this._data[2].replace(/%/g, '.*'), 'i').test(fieldValue);
|
||||
default:
|
||||
throw new Error(_.str.sprintf(
|
||||
"Domain %s uses an unsupported operator",
|
||||
|
||||
@@ -2,7 +2,6 @@ odoo.define('report.client_action', function (require) {
|
||||
'use strict';
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var utils = require('report.utils');
|
||||
@@ -11,8 +10,6 @@ var QWeb = core.qweb;
|
||||
|
||||
|
||||
var AUTHORIZED_MESSAGES = [
|
||||
'report.editor:save_ok',
|
||||
'report.editor:discard_ok',
|
||||
'report:do_action',
|
||||
];
|
||||
|
||||
@@ -28,8 +25,6 @@ var ReportAction = AbstractAction.extend({
|
||||
this.action_manager = parent;
|
||||
this._title = options.display_name || options.name;
|
||||
|
||||
this.edit_mode_available = false;
|
||||
this.in_edit_mode = false;
|
||||
this.report_url = options.report_url;
|
||||
|
||||
// Extra info that will be useful to build a qweb-pdf action.
|
||||
@@ -49,20 +44,12 @@ var ReportAction = AbstractAction.extend({
|
||||
self.trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
|
||||
|
||||
self.$buttons = $(QWeb.render('report.client_action.ControlButtons', {}));
|
||||
self.$buttons.on('click', '.o_report_edit', self.on_click_edit);
|
||||
self.$buttons.on('click', '.o_report_print', self.on_click_print);
|
||||
self.$buttons.on('click', '.o_report_save', self.on_click_save);
|
||||
self.$buttons.on('click', '.o_report_discard', self.on_click_discard);
|
||||
|
||||
self._update_control_panel();
|
||||
|
||||
// Load the report in the iframe. Note that we use a relative URL.
|
||||
self.iframe.src = self.report_url;
|
||||
|
||||
// Once the iframe is loaded, check if we can edit the report.
|
||||
self.iframe.onload = function () {
|
||||
self._on_iframe_loaded();
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -82,31 +69,12 @@ var ReportAction = AbstractAction.extend({
|
||||
$(window).off('message', this.on_message_received);
|
||||
},
|
||||
|
||||
_on_iframe_loaded: function () {
|
||||
var editable = $(this.iframe).contents().find('html').data('editable');
|
||||
if (editable === 1) {
|
||||
this.edit_mode_available = true;
|
||||
this._update_control_panel();
|
||||
}
|
||||
},
|
||||
|
||||
_update_control_panel: function () {
|
||||
this.updateControlPanel({
|
||||
cp_content: {
|
||||
$buttons: this.$buttons,
|
||||
},
|
||||
});
|
||||
this._update_control_panel_buttons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper allowing to toggle groups of buttons in the control panel
|
||||
* according to the `this.in_edit_mode` flag.
|
||||
*/
|
||||
_update_control_panel_buttons: function () {
|
||||
this.$buttons.filter('div.o_report_edit_mode').toggle(this.in_edit_mode);
|
||||
this.$buttons.filter('div.o_report_no_edit_mode').toggle(! this.in_edit_mode);
|
||||
this.$buttons.filter('div.o_edit_mode_available').toggle(config.debug && this.edit_mode_available && ! this.in_edit_mode);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -128,18 +96,6 @@ var ReportAction = AbstractAction.extend({
|
||||
}
|
||||
|
||||
switch(message) {
|
||||
case 'report.editor:save_ok':
|
||||
// Reload the iframe in order to disable the editor.
|
||||
this.iframe.src = this.report_url;
|
||||
this.in_edit_mode = false;
|
||||
this._update_control_panel_buttons();
|
||||
break;
|
||||
case 'report.editor:discard_ok':
|
||||
// Reload the iframe in order to disable the editor.
|
||||
this.iframe.src = this.report_url;
|
||||
this.in_edit_mode = false;
|
||||
this._update_control_panel_buttons();
|
||||
break;
|
||||
case 'report:do_action':
|
||||
return this.do_action(ev.originalEvent.data.action);
|
||||
default:
|
||||
@@ -157,25 +113,6 @@ var ReportAction = AbstractAction.extend({
|
||||
this.iframe.contentWindow.postMessage(message, this.trusted_origin);
|
||||
},
|
||||
|
||||
on_click_edit: function () {
|
||||
// We reload the iframe with a special query string to enable the editor.
|
||||
if (this.report_url.indexOf('?') === -1) {
|
||||
this.iframe.src = this.report_url + '?enable_editor=1';
|
||||
} else {
|
||||
this.iframe.src = this.report_url + '&enable_editor=1';
|
||||
}
|
||||
this.in_edit_mode = true;
|
||||
this._update_control_panel_buttons();
|
||||
},
|
||||
|
||||
on_click_discard: function () {
|
||||
this._post_message('report.editor:ask_discard');
|
||||
},
|
||||
|
||||
on_click_save: function () {
|
||||
this._post_message('report.editor:ask_save');
|
||||
},
|
||||
|
||||
on_click_print: function () {
|
||||
var action = {
|
||||
'type': 'ir.actions.report',
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
odoo.define('report.editor', function (require) {
|
||||
'use strict';
|
||||
|
||||
require('web.dom_ready')
|
||||
var core = require('web.core');
|
||||
var utils = require('report.utils');
|
||||
var editor = require('web_editor.editor');
|
||||
var options = require('web_editor.snippets.options');
|
||||
|
||||
var web_base_url = $('html').attr('web-base-url');
|
||||
var trusted_host = utils.get_host_from_url(web_base_url);
|
||||
var trusted_protocol = utils.get_protocol_from_url(web_base_url);
|
||||
var trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
|
||||
|
||||
// Patch the editor's behavior when it is launched inside an iframe.
|
||||
if (window.self !== window.top) {
|
||||
|
||||
// And now we chain some deferred to `save` and `cancel` in order to inform
|
||||
// the report's client action that the actions are done.
|
||||
editor.Class.include({
|
||||
save: function () {
|
||||
// Force to not reload
|
||||
return this._super(false).then(function () {
|
||||
window.parent.postMessage('report.editor:save_ok', trusted_origin);
|
||||
});
|
||||
},
|
||||
cancel: function () {
|
||||
// Force to not reload
|
||||
return this._super(false).then(function () {
|
||||
window.parent.postMessage('report.editor:discard_ok', trusted_origin);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// postMessage logic.
|
||||
var AUTHORIZED_MESSAGES = [
|
||||
'report.editor:ask_discard',
|
||||
'report.editor:ask_save',
|
||||
];
|
||||
|
||||
window.addEventListener('message', function (ev) {
|
||||
// Check the origin of the received message.
|
||||
var message_origin = utils.build_origin(ev.source.location.protocol, ev.source.location.host);
|
||||
if (message_origin === trusted_origin) {
|
||||
// Check the syntax of the received message.
|
||||
var message = ev.data;
|
||||
if (! _.isString(message) || (_.isString(message) && ! _.contains(AUTHORIZED_MESSAGES, message))) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case 'report.editor:ask_save':
|
||||
core.bus.trigger('editor_save_request');
|
||||
break;
|
||||
case 'report.editor:ask_discard':
|
||||
core.bus.trigger('editor_discard_request');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
options.registry.many2one.include({
|
||||
_selectRecord: function ($li) {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.$target.data('oe-field') !== 'partner_id') {
|
||||
return;
|
||||
}
|
||||
|
||||
var $img = $('.header .row img:first');
|
||||
var css = window.getComputedStyle($img[0]);
|
||||
$img.css('max-height', css.height+'px');
|
||||
$img.attr('src', '/web/image/res.partner/' + this.ID + '/image');
|
||||
_.defer(function () {
|
||||
$img.removeClass('o_dirty');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -15,26 +15,26 @@ var trusted_host = utils.get_host_from_url(web_base_url);
|
||||
var trusted_protocol = utils.get_protocol_from_url(web_base_url);
|
||||
var trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
|
||||
|
||||
// Allow to send commands to the webclient when the editor is disabled.
|
||||
if (window.location.search.indexOf('enable_editor') === -1) {
|
||||
// `do_action` command
|
||||
$('[res-id][res-model][view-type]')
|
||||
.wrap('<a/>')
|
||||
.attr('href', '#')
|
||||
.on('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
var action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': $(this).attr('view-type'),
|
||||
'view_mode': $(this).attr('view-mode') || $(this).attr('view-type'),
|
||||
'res_id': Number($(this).attr('res-id')),
|
||||
'res_model': $(this).attr('res-model'),
|
||||
'views': [[$(this).attr('view-id') || false, $(this).attr('view-type')]],
|
||||
};
|
||||
window.parent.postMessage({
|
||||
'message': 'report:do_action',
|
||||
'action': action,
|
||||
}, trusted_origin);
|
||||
});
|
||||
}
|
||||
// Allow sending commands to the webclient
|
||||
// `do_action` command
|
||||
$('[res-id][res-model][view-type]')
|
||||
.wrap('<a/>')
|
||||
.attr('href', '#')
|
||||
.on('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
var action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': $(this).attr('view-type'),
|
||||
'view_mode': $(this).attr('view-mode') || $(this).attr('view-type'),
|
||||
'res_id': Number($(this).attr('res-id')),
|
||||
'res_model': $(this).attr('res-model'),
|
||||
'views': [
|
||||
[$(this).attr('view-id') || false, $(this).attr('view-type')],
|
||||
],
|
||||
};
|
||||
window.parent.postMessage({
|
||||
'message': 'report:do_action',
|
||||
'action': action,
|
||||
}, trusted_origin);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
// Block fields
|
||||
&.note-editor, &.oe_form_field_html, &.oe_form_field_html_text, &.oe_form_field_text, &.o_field_domain, &.o_graph_linechart, &.o_graph_barchart {
|
||||
&.note-editor, &.oe_form_field_html, &.oe_form_field_text, &.o_field_domain, &.o_graph_linechart, &.o_graph_barchart {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.o_editable {
|
||||
&:not(:empty), &[data-oe-type] {
|
||||
&[date-oe-model="ir.ui.view"] {
|
||||
&, th, td {
|
||||
&:hover {
|
||||
-moz-box-shadow: #DDCC33 0 0 5px 2px inset;
|
||||
-webkit-box-shadow: #DDCC33 0 0 5px 2px inset;
|
||||
box-shadow: #DDCC33 0 0 5px 2px inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.o_in_iframe {
|
||||
margin-top: 40px!important;
|
||||
div#web_editor-top-edit {
|
||||
> form {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header, .footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,6 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.o_report_no_edit_mode, .o_edit_mode_available, .o_report_edit_mode {
|
||||
.o_report_buttons {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -7,15 +7,8 @@
|
||||
|
||||
<!-- Buttons of the Control Panel -->
|
||||
<t t-name="report.client_action.ControlButtons">
|
||||
<div class="o_report_no_edit_mode">
|
||||
<div class="o_report_buttons">
|
||||
<button type="button" class="btn btn-primary o_report_print" title="Print">Print</button>
|
||||
</div>
|
||||
<div class="o_edit_mode_available">
|
||||
<button type="button" class="btn btn-secondary o_report_edit" title="Edit">Edit</button>
|
||||
</div>
|
||||
<div class="o_report_edit_mode">
|
||||
<button type="button" class="btn btn-primary o_report_save" title="Save">Save</button>
|
||||
<button type="button" class="btn btn-secondary o_report_discard" title="Discard">Discard</button>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/layout_boxed.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/layout_clean.scss"/>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/chrome/public_root_widget.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/services/session.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
|
||||
<script type="text/javascript" src="/web_editor/static/src/js/content/body_manager.js"/>
|
||||
<script type="text/javascript" src="/web_editor/static/src/js/root_widget.js"/>
|
||||
<script type="text/javascript" src='/web/static/src/js/report/report.js' />
|
||||
</template>
|
||||
|
||||
@@ -34,23 +33,10 @@
|
||||
<link href="/web/static/src/css/reset.min.css" rel="stylesheet"/>
|
||||
</template>
|
||||
|
||||
<template id="report_assets_editor">
|
||||
<t t-call="web._assets_helpers"/>
|
||||
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/report.editor.scss"/>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/services/session.js"/>
|
||||
<script type="text/javascript" src='/web/static/src/js/report/report.editor.js' />
|
||||
</template>
|
||||
|
||||
<!-- Private templates used internally by reports -->
|
||||
|
||||
<template id="report_layout" name="Report layout"><!DOCTYPE html>
|
||||
<html t-att-lang="lang and lang.replace('_', '-')"
|
||||
t-att-data-editable="'1' if editable else None"
|
||||
t-att-data-translatable="'1' if translatable else None"
|
||||
t-att-data-edit_translations="'1' if edit_translations else None"
|
||||
t-att-data-main-object="repr(main_object) if editable else None"
|
||||
t-att-data-report-margin-top="data_report_margin_top"
|
||||
t-att-data-report-header-spacing="data_report_header_spacing"
|
||||
t-att-data-report-dpi="data_report_dpi"
|
||||
@@ -60,27 +46,13 @@
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="initial-scale=1"/>
|
||||
<title><t t-esc="title or 'Odoo Report'"/></title>
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.report_assets_common" t-js="false"/>
|
||||
<t t-if="editable">
|
||||
<t t-call-assets="web_editor.summernote" t-js="false"/>
|
||||
<t t-call-assets="web_editor.assets_editor" t-js="false"/>
|
||||
<t t-call-assets="web.report_assets_editor" t-js="false"/>
|
||||
</t>
|
||||
<t t-call-assets="web.assets_common" t-css="false"/>
|
||||
<t t-call-assets="web.report_assets_common" t-css="false"/>
|
||||
<t t-if="editable">
|
||||
<t t-call-assets="web_editor.summernote" t-css="false"/>
|
||||
<t t-call-assets="web_editor.assets_editor" t-css="false"/>
|
||||
<t t-call-assets="web.report_assets_editor" t-css="false"/>
|
||||
|
||||
<script type="text/javascript" src="/web_editor/static/src/js/iframe.js"></script>
|
||||
<script t-if="enable_editor and inline_mode" type="text/javascript" src="/web_editor/static/src/js/inline.js"></script>
|
||||
</t>
|
||||
</head>
|
||||
<body>
|
||||
<body t-att-class="'container' if not full_width else 'container-fluid'">
|
||||
<div id="wrapwrap">
|
||||
<main t-att-class="'container' if not full_width else 'container-fluid'">
|
||||
<main>
|
||||
<t t-raw="0"/>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/services/session_storage_service.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/ram_storage.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/rainbow_man.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/colorpicker.js"/>
|
||||
</template>
|
||||
|
||||
<template id="assets_backend" name="Backend Assets (used in backend interface)">
|
||||
|
||||
@@ -13,7 +13,6 @@ Odoo Web Editor widget.
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/editor.xml',
|
||||
'views/iframe.xml',
|
||||
'views/snippets.xml',
|
||||
],
|
||||
'qweb': [
|
||||
|
||||
@@ -19,54 +19,6 @@ from odoo.modules.module import get_resource_path, get_module_path
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Web_Editor(http.Controller):
|
||||
#------------------------------------------------------
|
||||
# Backend snippet
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/snippets', type='json', auth="user")
|
||||
def snippets(self, **kwargs):
|
||||
return request.env.ref('web_editor.snippets').render(None)
|
||||
|
||||
#------------------------------------------------------
|
||||
# Backend html field
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/field/html', type='http', auth="user")
|
||||
def FieldTextHtml(self, model=None, res_id=None, field=None, callback=None, **kwargs):
|
||||
kwargs.update(
|
||||
model=model,
|
||||
res_id=res_id,
|
||||
field=field,
|
||||
datarecord=json.loads(kwargs['datarecord']),
|
||||
debug=request.debug)
|
||||
|
||||
for k in kwargs:
|
||||
if isinstance(kwargs[k], str) and kwargs[k].isdigit():
|
||||
kwargs[k] = int(kwargs[k])
|
||||
|
||||
trans = dict(
|
||||
lang=kwargs.get('lang', request.env.context.get('lang')),
|
||||
translatable=kwargs.get('translatable'),
|
||||
edit_translations=kwargs.get('edit_translations'),
|
||||
editable=kwargs.get('enable_editor'))
|
||||
|
||||
kwargs.update(trans)
|
||||
|
||||
record = None
|
||||
if model and kwargs.get('res_id'):
|
||||
record = request.env[model].with_context(trans).browse(kwargs.get('res_id'))
|
||||
|
||||
kwargs.update(content=record and getattr(record, field) or "")
|
||||
|
||||
return request.render(kwargs.get("template") or "web_editor.FieldTextHtml", kwargs, uid=request.uid)
|
||||
|
||||
#------------------------------------------------------
|
||||
# Backend html field in inline mode
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/field/html/inline', type='http', auth="user")
|
||||
def FieldTextHtmlInline(self, model=None, res_id=None, field=None, callback=None, **kwargs):
|
||||
kwargs['inline_mode'] = True
|
||||
kwargs['dont_load_assets'] = not kwargs.get('enable_editor') and not kwargs.get('edit_translations')
|
||||
return self.FieldTextHtml(model, res_id, field, callback, **kwargs)
|
||||
|
||||
#------------------------------------------------------
|
||||
# convert font into picture
|
||||
#------------------------------------------------------
|
||||
@@ -136,6 +88,106 @@ class Web_Editor(http.Controller):
|
||||
|
||||
return response
|
||||
|
||||
#------------------------------------------------------
|
||||
# Update a checklist in the editor on check/uncheck
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/checklist', type='json', auth='user')
|
||||
def update_checklist(self, res_model, res_id, filename, checklistId, checked, **kwargs):
|
||||
record = request.env[res_model].browse(res_id)
|
||||
value = getattr(record, filename, False)
|
||||
htmlelem = etree.fromstring("<div>%s</div>" % value, etree.HTMLParser())
|
||||
checked = bool(checked)
|
||||
|
||||
li = htmlelem.find(".//li[@id='checklist-id-" + str(checklistId) + "']")
|
||||
|
||||
if not self._update_checklist_recursive(li, checked, children=True, ancestors=True):
|
||||
return value
|
||||
|
||||
value = etree.tostring(htmlelem[0][0], encoding='utf-8', method='html')[5:-6]
|
||||
record.write({filename: value})
|
||||
|
||||
return value
|
||||
|
||||
def _update_checklist_recursive (self, li, checked, children=False, ancestors=False):
|
||||
if 'checklist-id-' not in li.get('id', ''):
|
||||
return False
|
||||
|
||||
classname = li.get('class', '')
|
||||
if ('o_checked' in classname) == checked:
|
||||
return False
|
||||
|
||||
# check / uncheck
|
||||
if checked:
|
||||
classname = '%s o_checked' % classname
|
||||
else:
|
||||
classname = re.sub(r"\s?o_checked\s?", '', classname)
|
||||
li.set('class', classname)
|
||||
|
||||
# propagate to children
|
||||
if children:
|
||||
node = li.getnext()
|
||||
ul = None
|
||||
if node is not None:
|
||||
if node.tag == 'ul':
|
||||
ul = node
|
||||
if node.tag == 'li' and len(node.getchildren()) == 1 and node.getchildren()[0].tag == 'ul':
|
||||
ul = node.getchildren()[0]
|
||||
|
||||
if ul is not None:
|
||||
for child in ul.getchildren():
|
||||
if child.tag == 'li':
|
||||
self._update_checklist_recursive(child, checked, children=True)
|
||||
|
||||
# propagate to ancestors
|
||||
if ancestors:
|
||||
allSelected = True
|
||||
ul = li.getparent()
|
||||
if ul.tag == 'li':
|
||||
ul = ul.getparent()
|
||||
|
||||
for child in ul.getchildren():
|
||||
if child.tag == 'li' and 'o_checked' not in child.get('class', ''):
|
||||
allSelected = False
|
||||
|
||||
node = ul.getprevious()
|
||||
if node is not None and node.tag == 'li':
|
||||
self._update_checklist_recursive(node, allSelected, ancestors=True)
|
||||
|
||||
return True
|
||||
|
||||
#------------------------------------------------------
|
||||
# upload an image as base64
|
||||
#------------------------------------------------------
|
||||
@http.route('/web_editor/add_image_base64', type='json', auth='user', methods=['POST'], website=True)
|
||||
def add_image_base64(self, res_model, res_id, image_base64, filename, disable_optimization=None, **kwargs):
|
||||
data = base64.b64decode(image_base64)
|
||||
attachment = self._image_to_attachment(res_model, res_id, data, filename, filename, disable_optimization=disable_optimization)
|
||||
return attachment.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token'])[0]
|
||||
|
||||
def _image_to_attachment(self, res_model, res_id, data, name, datas_fname, disable_optimization=None):
|
||||
Attachments = request.env['ir.attachment']
|
||||
try:
|
||||
image = Image.open(io.BytesIO(data))
|
||||
w, h = image.size
|
||||
if w*h > 42e6: # Nokia Lumia 1020 photo resolution
|
||||
raise ValueError(
|
||||
u"Image size excessive, uploaded images must be smaller "
|
||||
u"than 42 million pixel")
|
||||
if not disable_optimization and image.format in ('PNG', 'JPEG'):
|
||||
data = tools.image_save_for_web(image)
|
||||
except IOError:
|
||||
pass
|
||||
attachment = Attachments.create({
|
||||
'name': name,
|
||||
'datas_fname': datas_fname,
|
||||
'datas': base64.b64encode(data),
|
||||
'public': res_model == 'ir.ui.view',
|
||||
'res_id': res_id,
|
||||
'res_model': res_model,
|
||||
})
|
||||
attachment.generate_access_token()
|
||||
return attachment
|
||||
|
||||
#------------------------------------------------------
|
||||
# add attachment (images or link)
|
||||
#------------------------------------------------------
|
||||
@@ -175,32 +227,11 @@ class Web_Editor(http.Controller):
|
||||
attachments = request.env['ir.attachment']
|
||||
for c_file in request.httprequest.files.getlist('upload'):
|
||||
data = c_file.read()
|
||||
try:
|
||||
image = Image.open(io.BytesIO(data))
|
||||
w, h = image.size
|
||||
if w*h > 42e6: # Nokia Lumia 1020 photo resolution
|
||||
raise ValueError(
|
||||
u"Image size excessive, uploaded images must be smaller "
|
||||
u"than 42 million pixel")
|
||||
if not disable_optimization and image.format in ('PNG', 'JPEG'):
|
||||
data = tools.image_save_for_web(image)
|
||||
except IOError as e:
|
||||
pass
|
||||
|
||||
name = c_file.filename
|
||||
datas_fname = name
|
||||
if filters:
|
||||
datas_fname = filters + '_' + datas_fname
|
||||
attachment = Attachments.create({
|
||||
'name': name,
|
||||
'datas': base64.b64encode(data),
|
||||
'datas_fname': datas_fname,
|
||||
'public': res_model == 'ir.ui.view',
|
||||
'res_id': res_id,
|
||||
'res_model': res_model,
|
||||
})
|
||||
attachment.generate_access_token()
|
||||
attachments += attachment
|
||||
attachments += self._image_to_attachment(res_model, res_id, data, name, datas_fname, disable_optimization=disable_optimization)
|
||||
uploads += attachments.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token'])
|
||||
except Exception as e:
|
||||
logger.exception("Failed to upload image to attachment")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
odoo.define('web_editor.transcoder', function (require) {
|
||||
odoo.define('web_editor.convertInline', function (require) {
|
||||
'use strict';
|
||||
|
||||
var base = require('web_editor.base');
|
||||
|
||||
var rulesCache = [];
|
||||
var fonts = require('wysiwyg.fonts');
|
||||
var FieldHtml = require('web_editor.field.html');
|
||||
|
||||
/**
|
||||
* Returns the css rules which applies on an element, tweaked so that they are
|
||||
@@ -14,8 +13,11 @@ var rulesCache = [];
|
||||
*/
|
||||
function getMatchedCSSRules(a) {
|
||||
var i, r, k;
|
||||
var doc = a.ownerDocument;
|
||||
var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []);
|
||||
|
||||
if (!rulesCache.length) {
|
||||
var sheets = document.styleSheets;
|
||||
var sheets = doc.styleSheets;
|
||||
for (i = sheets.length-1 ; i >= 0 ; i--) {
|
||||
var rules;
|
||||
// try...catch because browser may not able to enumerate rules for cross-domain sheets
|
||||
@@ -190,8 +192,8 @@ function fontToImg($editable) {
|
||||
$editable.find('.fa').each(function () {
|
||||
var $font = $(this);
|
||||
var icon, content;
|
||||
_.find(base.fontIcons, function (font) {
|
||||
return _.find(base.getCssSelectors(font.parser), function (data) {
|
||||
_.find(fonts.fontIcons, function (font) {
|
||||
return _.find(fonts.getCssSelectors(font.parser), function (data) {
|
||||
if ($font.is(data.selector.replace(/::?before/g, ''))) {
|
||||
icon = data.names[0].split('-').shift();
|
||||
content = data.css.match(/content:\s*['"]?(.)['"]?/)[1];
|
||||
@@ -265,9 +267,6 @@ function applyOverDescendants(node, func) {
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
function classToStyle($editable) {
|
||||
if (!rulesCache.length) {
|
||||
getMatchedCSSRules($editable[0]);
|
||||
}
|
||||
applyOverDescendants($editable[0], function (node) {
|
||||
var $target = $(node);
|
||||
var css = getMatchedCSSRules(node);
|
||||
@@ -328,9 +327,7 @@ function styleToClass($editable) {
|
||||
$(this).after($('a,img', this));
|
||||
}).remove();
|
||||
|
||||
getMatchedCSSRules($editable[0]);
|
||||
|
||||
var $c = $('<span/>').appendTo(document.body);
|
||||
var $c = $('<span/>').appendTo($editable[0].ownerDocument.body);
|
||||
|
||||
applyOverDescendants($editable[0], function (node) {
|
||||
var $target = $(node);
|
||||
@@ -342,7 +339,7 @@ function styleToClass($editable) {
|
||||
}
|
||||
});
|
||||
css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
|
||||
style = $target.attr('style') || '';
|
||||
style = ($target.attr('style') || '').replace(/\s*:\s*/, ':').replace(/\s*;\s*/, ';');
|
||||
_.each(css, function (v) {
|
||||
style = style.replace(v, '');
|
||||
});
|
||||
@@ -364,13 +361,13 @@ function styleToClass($editable) {
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
function attachmentThumbnailToLinkImg($editable) {
|
||||
$editable.find('a[href*="/web/content/"][data-mimetype]:empty').each(function () {
|
||||
$editable.find('a[href*="/web/content/"][data-mimetype]').filter(':empty, :containsExact( )').each(function () {
|
||||
var $link = $(this);
|
||||
var $img = $('<img/>')
|
||||
.attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, ''))
|
||||
.css('height', Math.max(1, $link.height()) + 'px')
|
||||
.css('width', Math.max(1, $link.width()) + 'px');
|
||||
$link.append($img);
|
||||
$link.prepend($img);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -384,6 +381,86 @@ function linkImgToAttachmentThumbnail($editable) {
|
||||
$editable.find('a[href*="/web/content/"][data-mimetype] > img').remove();
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
|
||||
FieldHtml.include({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
commitChanges: function () {
|
||||
if (!this.wysiwyg) {
|
||||
return this._super();
|
||||
}
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
this._toInline();
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Converts CSS dependencies to CSS-independent HTML.
|
||||
* - CSS display for attachment link -> real image
|
||||
* - Font icons -> images
|
||||
* - CSS styles -> inline styles
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toInline: function () {
|
||||
var $editable = this.wysiwyg.getEditable();
|
||||
var html = this.wysiwyg.getValue();
|
||||
$editable.html(html);
|
||||
|
||||
attachmentThumbnailToLinkImg($editable);
|
||||
fontToImg($editable);
|
||||
classToStyle($editable);
|
||||
this.wysiwyg.setValue($editable.html(), {
|
||||
notifyChange: false,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Revert _toInline changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fromInline: function () {
|
||||
var $editable = this.wysiwyg.getEditable();
|
||||
var html = this.wysiwyg.getValue();
|
||||
$editable.html(html);
|
||||
|
||||
styleToClass($editable);
|
||||
imgToFont($editable);
|
||||
linkImgToAttachmentThumbnail($editable);
|
||||
this.wysiwyg.setValue($editable.html(), {
|
||||
notifyChange: false,
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onLoadWysiwyg: function () {
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
this._fromInline();
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
fontToImg: fontToImg,
|
||||
imgToFont: imgToFont,
|
||||
|
||||
@@ -1,37 +1,57 @@
|
||||
odoo.define('web_editor.backend', function (require) {
|
||||
odoo.define('web_editor.field.html', function (require) {
|
||||
'use strict';
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var ajax = require('web.ajax');
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
var field_registry = require('web.field_registry');
|
||||
var SummernoteManager = require('web_editor.rte.summernote');
|
||||
var transcoder = require('web_editor.transcoder');
|
||||
|
||||
var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
/**
|
||||
* FieldTextHtmlSimple Widget
|
||||
* Intended to display HTML content. This widget uses the summernote editor
|
||||
* FieldHtml Widget
|
||||
* Intended to display HTML content. This widget uses the wysiwyg editor
|
||||
* improved by odoo.
|
||||
*
|
||||
* nodeOptions:
|
||||
* - style-inline => convert class to inline style (no re-edition) => for sending by email
|
||||
* - no-attachment
|
||||
* - cssEdit
|
||||
* - cssReadonly
|
||||
* - snippets
|
||||
* - wrapper
|
||||
*/
|
||||
var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
className: 'oe_form_field oe_form_field_html_text',
|
||||
var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
className: 'oe_form_field oe_form_field_html',
|
||||
supportedFieldTypes: ['html'],
|
||||
|
||||
custom_events: {
|
||||
wysiwyg_focus: '_onWysiwygFocus',
|
||||
wysiwyg_blur: '_onWysiwygBlur',
|
||||
wysiwyg_change: '_onChange',
|
||||
wysiwyg_attachment: '_onAttachmentChange',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
new SummernoteManager(this);
|
||||
return this._super.apply(this, arguments);
|
||||
willStart: function () {
|
||||
this._onUpdateIframeId = 'onLoad_' + _.uniqueId('FieldHtml');
|
||||
var defAsset = this.nodeOptions.cssReadonly && ajax.loadAsset(this.nodeOptions.cssReadonly);
|
||||
return $.when(this._super().then(Wysiwyg.prepare.bind(Wysiwyg, this)), defAsset);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
delete window.top[this._onUpdateIframeId];
|
||||
if (this.$iframe) {
|
||||
this.$iframe.remove();
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -39,22 +59,31 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Summernote doesn't notify for changes done in code mode. We override
|
||||
* @override
|
||||
*/
|
||||
activate: function (options) {
|
||||
if (this.wysiwyg) {
|
||||
this.wysiwyg.focus();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Wysiwyg doesn't notify for changes done in code mode. We override
|
||||
* commitChanges to manually switch back to normal mode before committing
|
||||
* changes, so that the widget is aware of the changes done in code mode.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
commitChanges: function () {
|
||||
// switch to WYSIWYG mode if currently in code mode to get all changes
|
||||
if (config.debug && this.mode === 'edit') {
|
||||
var layoutInfo = this.$textarea.data('layoutInfo');
|
||||
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
|
||||
var self = this;
|
||||
if (!this.wysiwyg) {
|
||||
return this._super();
|
||||
}
|
||||
if (this._getValue() !== this.value) {
|
||||
this._isDirty = true;
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
var _super = this._super.bind(this);
|
||||
return this.wysiwyg.save().then(function (isDirty, html) {
|
||||
self._isDirty = isDirty;
|
||||
_super();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -66,7 +95,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
return this.$content || this._super.apply(this, arguments);
|
||||
return this.$target || $();
|
||||
},
|
||||
/**
|
||||
* Do not re-render this field if it was the origin of the onchange call.
|
||||
@@ -75,11 +104,16 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
*/
|
||||
reset: function (record, event) {
|
||||
this._reset(record, event);
|
||||
var value = this.value;
|
||||
if (this.nodeOptions.wrapper) {
|
||||
value = this._wrap(value);
|
||||
}
|
||||
value = this._textToHtml(value);
|
||||
if (!event || event.target !== this) {
|
||||
if (this.mode === 'edit') {
|
||||
this.$content.html(this._textToHtml(this.value));
|
||||
this.wysiwyg.setValue(value);
|
||||
} else {
|
||||
this._renderReadonly();
|
||||
this.$content.html(value);
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
@@ -89,106 +123,75 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the domain for attachments used in media dialog.
|
||||
* We look for attachments related to the current document. If there is a value for the model
|
||||
* field, it is used to search attachments, and the attachments from the current document are
|
||||
* filtered to display only user-created documents.
|
||||
* In the case of a wizard such as mail, we have the documents uploaded and those of the model
|
||||
*
|
||||
* @private
|
||||
* @returns {Array} "ir.attachment" odoo domain.
|
||||
*/
|
||||
_getAttachmentsDomain: function () {
|
||||
var domain = ['|', ['id', 'in', _.pluck(this.attachments, 'id')]];
|
||||
var attachedDocumentDomain = [
|
||||
'&',
|
||||
['res_model', '=', this.model],
|
||||
['res_id', '=', this.res_id|0]
|
||||
];
|
||||
// if the document is not yet created, do not see the documents of other users
|
||||
if (!this.res_id) {
|
||||
attachedDocumentDomain.unshift('&');
|
||||
attachedDocumentDomain.push(['create_uid', '=', session.uid]);
|
||||
}
|
||||
if (this.recordData.res_model || this.recordData.model) {
|
||||
var relatedDomain = ['&',
|
||||
['res_model', '=', this.recordData.res_model || this.recordData.model],
|
||||
['res_id', '=', this.recordData.res_id|0]];
|
||||
if (!this.recordData.res_id) {
|
||||
relatedDomain.unshift('&');
|
||||
relatedDomain.push(['create_uid', '=', session.uid]);
|
||||
}
|
||||
domain = domain.concat(['|'], attachedDocumentDomain, relatedDomain);
|
||||
} else {
|
||||
domain = domain.concat(attachedDocumentDomain);
|
||||
}
|
||||
return domain;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object} the summernote configuration
|
||||
*/
|
||||
_getSummernoteConfig: function () {
|
||||
var summernoteConfig = {
|
||||
model: this.model,
|
||||
id: this.res_id,
|
||||
focus: false,
|
||||
height: 180,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'italic', 'underline', 'clear']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', this.nodeOptions['no-attachment'] ? ['link'] : ['link', 'picture']],
|
||||
['history', ['undo', 'redo']]
|
||||
],
|
||||
prettifyHtml: false,
|
||||
styleWithSpan: false,
|
||||
inlinemedia: ['p'],
|
||||
lang: "odoo",
|
||||
onChange: this._doDebouncedAction.bind(this),
|
||||
disableDragAndDrop: !!this.nodeOptions['no-attachment'],
|
||||
};
|
||||
|
||||
var fieldNameAttachment =_.chain(this.recordData)
|
||||
.pairs()
|
||||
.find(function (value) {
|
||||
return _.isObject(value[1]) && value[1].model === "ir.attachment";
|
||||
})
|
||||
.first()
|
||||
.value();
|
||||
|
||||
if (fieldNameAttachment) {
|
||||
this.fieldNameAttachment = fieldNameAttachment;
|
||||
this.attachments = [];
|
||||
summernoteConfig.onUpload = this._onUpload.bind(this);
|
||||
}
|
||||
summernoteConfig.getMediaDomain = this._getAttachmentsDomain.bind(this);
|
||||
|
||||
|
||||
if (config.debug) {
|
||||
summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]);
|
||||
}
|
||||
return summernoteConfig;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getValue: function () {
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
transcoder.attachmentThumbnailToLinkImg(this.$content);
|
||||
transcoder.fontToImg(this.$content);
|
||||
transcoder.classToStyle(this.$content);
|
||||
var value = this.$target.val();
|
||||
if (this.nodeOptions.wrapper) {
|
||||
return this._unWrap(value);
|
||||
}
|
||||
return this.$content.html();
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* Create the wysiwyg instance with the target (this.$target)
|
||||
* then add the editable content (this.$content).
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_createWysiwygIntance: function () {
|
||||
var self = this;
|
||||
this.wysiwyg = new Wysiwyg(this, this._getWysiwygOptions());
|
||||
|
||||
// by default this is synchronous because the assets are already loaded in willStart
|
||||
// but it can be async in the case of options such as iframe, snippets...
|
||||
return this.wysiwyg.attachTo(this.$target).then(function () {
|
||||
self.$content = self.wysiwyg.$el;
|
||||
self._onLoadWysiwyg();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get wysiwyg options to create wysiwyg instance.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getWysiwygOptions: function () {
|
||||
return {
|
||||
recordInfo: {
|
||||
context: this.record.getContext(this.recordParams),
|
||||
res_model: this.model,
|
||||
res_id: this.res_id,
|
||||
},
|
||||
noAttachment: this.nodeOptions['no-attachment'],
|
||||
inIframe: !!this.nodeOptions.cssEdit,
|
||||
iframeCssAssets: this.nodeOptions.cssEdit,
|
||||
snippets: this.nodeOptions.snippets,
|
||||
|
||||
tabSize: 0,
|
||||
keyMap: {
|
||||
pc: {
|
||||
'TAB': null,
|
||||
'SHIFT+TAB': null,
|
||||
},
|
||||
mac: {
|
||||
'TAB': null,
|
||||
'SHIFT+TAB': null,
|
||||
},
|
||||
},
|
||||
generateOptions: function (options) {
|
||||
var para = _.find(options.toolbar, function (item) {
|
||||
return item[0] === 'para';
|
||||
});
|
||||
para[1].splice(2, 0, 'checklist');
|
||||
return options;
|
||||
},
|
||||
};
|
||||
},
|
||||
/**
|
||||
* trigger_up 'field_changed' add record into the "ir.attachment" field found in the view.
|
||||
* This method is called when an image is uploaded by the media dialog.
|
||||
* This method is called when an image is uploaded via the media dialog.
|
||||
*
|
||||
* For e.g. when sending email, this allows people to add attachments with the content
|
||||
* editor interface and that they appear in the attachment list.
|
||||
@@ -196,16 +199,12 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
* when closing the wizard.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} attachments
|
||||
*/
|
||||
_onUpload: function (attachments) {
|
||||
var self = this;
|
||||
attachments = _.filter(attachments, function (attachment) {
|
||||
return !_.findWhere(self.attachments, {id: attachment.id});
|
||||
});
|
||||
if (!attachments.length) {
|
||||
_onAttachmentChange: function (attachments) {
|
||||
if (!this.fieldNameAttachment) {
|
||||
return;
|
||||
}
|
||||
this.attachments = this.attachments.concat(attachments);
|
||||
this.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: _.object([this.fieldNameAttachment], [{
|
||||
@@ -216,59 +215,108 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
this.$textarea = $('<textarea>');
|
||||
this.$textarea.appendTo(this.$el);
|
||||
this.$textarea.summernote(this._getSummernoteConfig());
|
||||
this.$content = this.$('.note-editable:first');
|
||||
this.$content.html(this._textToHtml(this.value));
|
||||
// trigger a mouseup to refresh the editor toolbar
|
||||
var mouseupEvent = $.Event('mouseup', {'setStyleInfoFromEditable': true});
|
||||
this.$content.trigger(mouseupEvent);
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
transcoder.styleToClass(this.$content);
|
||||
transcoder.imgToFont(this.$content);
|
||||
transcoder.linkImgToAttachmentThumbnail(this.$content);
|
||||
var value = this._textToHtml(this.value);
|
||||
if (this.nodeOptions.wrapper) {
|
||||
value = this._wrap(value);
|
||||
}
|
||||
// reset the history (otherwise clicking on undo before editing the
|
||||
// value will empty the editor)
|
||||
var history = this.$content.data('NoteHistory');
|
||||
if (history) {
|
||||
history.reset();
|
||||
this.$target = $('<textarea>').val(value).hide();
|
||||
this.$target.appendTo(this.$el);
|
||||
|
||||
var fieldNameAttachment = _.chain(this.recordData)
|
||||
.pairs()
|
||||
.find(function (value) {
|
||||
return _.isObject(value[1]) && value[1].model === "ir.attachment";
|
||||
})
|
||||
.first()
|
||||
.value();
|
||||
if (fieldNameAttachment) {
|
||||
this.fieldNameAttachment = fieldNameAttachment;
|
||||
}
|
||||
|
||||
if (this.nodeOptions.cssEdit) {
|
||||
// must be async because the target must be append in the DOM
|
||||
this._createWysiwygIntance();
|
||||
} else {
|
||||
return this._createWysiwygIntance();
|
||||
}
|
||||
this.$('.note-toolbar').append(this._renderTranslateButton());
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderReadonly: function () {
|
||||
var self = this;
|
||||
this.$el.empty();
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
var $iframe = $('<iframe class="o_readonly"/>');
|
||||
$iframe.on('load', function () {
|
||||
self.$content = $($iframe.contents()[0]).find("body");
|
||||
self.$content.html(self._textToHtml(self.value));
|
||||
self._resize();
|
||||
});
|
||||
$iframe.appendTo(this.$el);
|
||||
} else {
|
||||
this.$content = $('<div class="o_readonly"/>');
|
||||
this.$content.html(this._textToHtml(this.value));
|
||||
this.$content.appendTo(this.$el);
|
||||
var value = this._textToHtml(this.value);
|
||||
if (this.nodeOptions.wrapper) {
|
||||
value = this._wrap(value);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Sets the height of the iframe.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_resize: function () {
|
||||
var height = this.$content[0] ? this.$content[0].scrollHeight : 0;
|
||||
this.$('iframe').css('height', Math.max(30, Math.min(height, 500)) + 'px');
|
||||
|
||||
this.$el.empty();
|
||||
|
||||
var def = $.Deferred();
|
||||
if (this.nodeOptions.cssReadonly) {
|
||||
this.$iframe = $('<iframe class="o_readonly"/>');
|
||||
this.$iframe.appendTo(this.$el);
|
||||
|
||||
var avoidDoubleLoad = 0; // this bug only appears on some computers with some chrome version.
|
||||
|
||||
// inject content in iframe
|
||||
|
||||
this.$iframe.data('load-def', def); // for unit test
|
||||
window.top[this._onUpdateIframeId] = function (_avoidDoubleLoad) {
|
||||
if (_avoidDoubleLoad !== avoidDoubleLoad) {
|
||||
console.warn('Wysiwyg iframe double load detected');
|
||||
return;
|
||||
}
|
||||
self.$content = $('#iframe_target', self.$iframe[0].contentWindow.document.body);
|
||||
def.resolve();
|
||||
};
|
||||
|
||||
this.$iframe.one('load', function onLoad() {
|
||||
var _avoidDoubleLoad = ++avoidDoubleLoad;
|
||||
ajax.loadAsset(self.nodeOptions.cssReadonly).then(function (asset) {
|
||||
if (_avoidDoubleLoad !== avoidDoubleLoad) {
|
||||
console.warn('Wysiwyg immediate iframe double load detected');
|
||||
return;
|
||||
}
|
||||
var cwindow = self.$iframe[0].contentWindow;
|
||||
cwindow.document
|
||||
.open("text/html", "replace")
|
||||
.write(
|
||||
'<head>' +
|
||||
'<meta charset="utf-8"/>' +
|
||||
'<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n' +
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>\n' +
|
||||
_.map(asset.cssLibs, function (cssLib) {
|
||||
return '<link type="text/css" rel="stylesheet" href="' + cssLib + '"/>';
|
||||
}).join('\n') + '\n' +
|
||||
_.map(asset.cssContents, function (cssContent) {
|
||||
return '<style type="text/css">' + cssContent + '</style>';
|
||||
}).join('\n') + '\n' +
|
||||
'</head>\n' +
|
||||
'<body class="o_in_iframe o_readonly">\n' +
|
||||
'<div id="iframe_target">' + value + '</div>\n' +
|
||||
'<script type="text/javascript">' +
|
||||
'if (window.top.' + self._onUpdateIframeId + ') {' +
|
||||
'window.top.' + self._onUpdateIframeId + '(' + _avoidDoubleLoad + ')' +
|
||||
'}' +
|
||||
'</script>\n' +
|
||||
'</body>');
|
||||
|
||||
var height = cwindow.document.body.scrollHeight;
|
||||
self.$iframe.css('height', Math.max(30, Math.min(height, 500)) + 'px');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$content = $('<div class="o_readonly"/>').html(value);
|
||||
this.$content.appendTo(this.$el);
|
||||
def.resolve();
|
||||
}
|
||||
|
||||
def.then(function () {
|
||||
self.$content.on('click', 'ul.o_checklist > li', self._onReadonlyClickChecklist.bind(self));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -277,6 +325,9 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
*/
|
||||
_textToHtml: function (text) {
|
||||
var value = text || "";
|
||||
if (/%\send/.test(value)) { // is jinja
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
$(text)[0].innerHTML; // crashes if text isn't html
|
||||
} catch (e) {
|
||||
@@ -294,239 +345,132 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* Move HTML contents out of their wrapper.
|
||||
*
|
||||
* @private
|
||||
* @returns {jQueryElement}
|
||||
* @param {string} html content
|
||||
* @returns {string} html content
|
||||
*/
|
||||
_renderTranslateButton: function () {
|
||||
if (_t.database.multi_lang && this.field.translate && this.res_id) {
|
||||
return $(QWeb.render('web_editor.FieldTextHtml.button.translate', {widget: this}))
|
||||
.on('click', this._onTranslate.bind(this));
|
||||
}
|
||||
return $();
|
||||
_unWrap: function (html) {
|
||||
var $wrapper = $(html).find('#wrapper');
|
||||
return $wrapper.length ? $wrapper.html() : html;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
var FieldTextHtml = AbstractField.extend({
|
||||
template: 'web_editor.FieldTextHtml',
|
||||
supportedFieldTypes: ['html'],
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this.editorLoadedDeferred = $.Deferred();
|
||||
this.contentLoadedDeferred = $.Deferred();
|
||||
this.callback = _.uniqueId('FieldTextHtml_');
|
||||
window.odoo[this.callback+"_editor"] = function (EditorBar) {
|
||||
setTimeout(function () {
|
||||
self.on_editor_loaded(EditorBar);
|
||||
},0);
|
||||
};
|
||||
window.odoo[this.callback+"_content"] = function () {
|
||||
self.on_content_loaded();
|
||||
};
|
||||
window.odoo[this.callback+"_updown"] = null;
|
||||
window.odoo[this.callback+"_downup"] = function () {
|
||||
self.resize();
|
||||
};
|
||||
|
||||
// init jqery objects
|
||||
this.$iframe = this.$el.find('iframe');
|
||||
this.document = null;
|
||||
this.$body = $();
|
||||
this.$content = $();
|
||||
|
||||
this.$iframe.css('min-height', 'calc(100vh - 360px)');
|
||||
|
||||
// init resize
|
||||
this.resize = function resize() {
|
||||
if (self.mode === 'edit') {
|
||||
if ($("body").hasClass("o_field_widgetTextHtml_fullscreen")) {
|
||||
self.$iframe.css('height', (document.body.clientHeight - self.$iframe.offset().top) + 'px');
|
||||
} else {
|
||||
self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px");
|
||||
}
|
||||
}
|
||||
};
|
||||
$(window).on('resize', this.resize);
|
||||
|
||||
this.old_initialize_content();
|
||||
var def = this._super.apply(this, arguments);
|
||||
return def;
|
||||
},
|
||||
getDatarecord: function () {
|
||||
return this.recordData;
|
||||
},
|
||||
get_url: function (_attr) {
|
||||
var src = this.nodeOptions.editor_url || "/mass_mailing/field/email_template";
|
||||
var k;
|
||||
var datarecord = this.getDatarecord();
|
||||
var attr = {
|
||||
'model': this.model,
|
||||
'field': this.name,
|
||||
'res_id': datarecord.id || '',
|
||||
'callback': this.callback
|
||||
};
|
||||
_attr = _attr || {};
|
||||
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
attr.inline_mode = 1;
|
||||
}
|
||||
if (this.nodeOptions.snippets) {
|
||||
attr.snippets = this.nodeOptions.snippets;
|
||||
}
|
||||
if (this.nodeOptions.template) {
|
||||
attr.template = this.nodeOptions.template;
|
||||
}
|
||||
if (this.mode === "edit") {
|
||||
attr.enable_editor = 1;
|
||||
}
|
||||
if (session.debug) {
|
||||
attr.debug = session.debug;
|
||||
}
|
||||
|
||||
for (k in _attr) {
|
||||
attr[k] = _attr[k];
|
||||
}
|
||||
|
||||
if (src.indexOf('?') === -1) {
|
||||
src += "?";
|
||||
}
|
||||
|
||||
for (k in attr) {
|
||||
if (attr[k] !== null) {
|
||||
src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// delete datarecord[this.name];
|
||||
src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord));
|
||||
return src;
|
||||
},
|
||||
old_initialize_content: function () {
|
||||
this.$el.closest('.modal-body').css('max-height', 'none');
|
||||
this.$iframe = this.$el.find('iframe');
|
||||
this.document = null;
|
||||
this.$body = $();
|
||||
this.$content = $();
|
||||
this.editor = false;
|
||||
window.odoo[this.callback+"_updown"] = null;
|
||||
this.$iframe.attr("src", this.get_url());
|
||||
},
|
||||
on_content_loaded: function () {
|
||||
var self = this;
|
||||
this.document = this.$iframe.contents()[0];
|
||||
this.$body = $("body", this.document);
|
||||
this.$content = this.$body.find("#editable_area");
|
||||
this.render();
|
||||
this.add_button();
|
||||
this.contentLoadedDeferred.resolve();
|
||||
setTimeout(self.resize, 0);
|
||||
},
|
||||
on_editor_loaded: function (EditorBar) {
|
||||
var self = this;
|
||||
this.editor = EditorBar;
|
||||
if (this.value && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) {
|
||||
this.render();
|
||||
}
|
||||
this.editorLoadedDeferred.resolve();
|
||||
setTimeout(function () {
|
||||
setTimeout(self.resize,0);
|
||||
}, 0);
|
||||
},
|
||||
add_button: function () {
|
||||
var self = this;
|
||||
var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first();
|
||||
|
||||
$(QWeb.render('web_editor.FieldTextHtml.fullscreen'))
|
||||
.appendTo($to)
|
||||
.on('click', '.o_fullscreen', function () {
|
||||
$("body").toggleClass("o_field_widgetTextHtml_fullscreen");
|
||||
var full = $("body").hasClass("o_field_widgetTextHtml_fullscreen");
|
||||
self.$iframe.parents().toggleClass('o_form_fullscreen_ancestor', full);
|
||||
$(window).trigger("resize"); // induce a resize() call and let other backend elements know (the navbar extra items management relies on this)
|
||||
});
|
||||
|
||||
this.$body.on('click', '[data-action="cancel"]', function (event) {
|
||||
event.preventDefault();
|
||||
self.old_initialize_content();
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var value = (this.value || "").replace(/^<p[^>]*>(\s*|<br\/?>)<\/p>$/, '');
|
||||
if (!this.$content) {
|
||||
return;
|
||||
}
|
||||
if (this.mode === "edit") {
|
||||
if (window.odoo[this.callback+"_updown"]) {
|
||||
// FIXME
|
||||
// window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name);
|
||||
this.resize();
|
||||
}
|
||||
} else {
|
||||
this.$content.html(value);
|
||||
if (this.$iframe[0].contentWindow) {
|
||||
this.$iframe.css("height", (this.$body.height()+20) + "px");
|
||||
}
|
||||
}
|
||||
},
|
||||
has_no_value: function () {
|
||||
return this.value === false || !this.$content.html() || !this.$content.html().match(/\S/);
|
||||
},
|
||||
destroy: function () {
|
||||
$(window).off('resize', this.resize);
|
||||
delete window.odoo[this.callback+"_editor"];
|
||||
delete window.odoo[this.callback+"_content"];
|
||||
delete window.odoo[this.callback+"_updown"];
|
||||
delete window.odoo[this.callback+"_downup"];
|
||||
/**
|
||||
* Wrap HTML in order to create a custom display.
|
||||
*
|
||||
* The wrapper (this.nodeOptions.wrapper) must be a static
|
||||
* XML template with content id="wrapper".
|
||||
*
|
||||
* @private
|
||||
* @param {string} html content
|
||||
* @returns {string} html content
|
||||
*/
|
||||
_wrap: function (html) {
|
||||
return $(QWeb.render(this.nodeOptions.wrapper))
|
||||
.find('#wrapper').html(html)
|
||||
.end().prop('outerHTML');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set the value when the widget is fully loaded (content + editor).
|
||||
* Method called when wysiwyg triggers a change.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
commitChanges: function () {
|
||||
var self = this;
|
||||
var result = this._super.bind(this, arguments);
|
||||
if (this.mode === 'readonly') {
|
||||
_onChange: function (ev) {
|
||||
this._doDebouncedAction.apply(this, arguments);
|
||||
|
||||
var $lis = this.$content.find('.note-editable ul.o_checklist > li:not(:has(> ul.o_checklist))');
|
||||
if (!$lis.length) {
|
||||
return;
|
||||
}
|
||||
return $.when(this.contentLoadedDeferred, this.editorLoadedDeferred, result).then(function () {
|
||||
// switch to WYSIWYG mode if currently in code mode to get all changes
|
||||
if (config.debug && self.editor.rte) {
|
||||
var layoutInfo = self.editor.rte.editable().data('layoutInfo');
|
||||
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
|
||||
var max = 0;
|
||||
var ids = [];
|
||||
$lis.map(function () {
|
||||
var checklistId = parseInt(($(this).attr('id') || '0').replace(/^checklist-id-/, ''));
|
||||
if (ids.indexOf(checklistId) === -1) {
|
||||
if (checklistId > max) {
|
||||
max = checklistId;
|
||||
}
|
||||
ids.push(checklistId);
|
||||
} else {
|
||||
$(this).removeAttr('id');
|
||||
}
|
||||
var $ancestors = self.$iframe.filter(':not(:visible)').parentsUntil(':visible').addBack();
|
||||
var ancestorsStyle = [];
|
||||
// temporarily force displaying iframe (needed for firefox)
|
||||
_.each($ancestors, function (el) {
|
||||
var $el = $(el);
|
||||
ancestorsStyle.unshift($el.attr('style') || null);
|
||||
$el.css({display: 'initial', visibility: 'hidden', height: 1});
|
||||
});
|
||||
self.editor.snippetsMenu && self.editor.snippetsMenu.cleanForSave();
|
||||
_.each($ancestors, function (el) {
|
||||
var $el = $(el);
|
||||
$el.attr('style', ancestorsStyle.pop());
|
||||
});
|
||||
self._setValue(self.$content.html());
|
||||
});
|
||||
$lis.not('[id]').each(function () {
|
||||
$(this).attr('id', 'checklist-id-' + (++max));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Method called when wysiwyg triggers a change.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onReadonlyClickChecklist: function (ev) {
|
||||
var self = this;
|
||||
if (ev.offsetX > 0) {
|
||||
return;
|
||||
}
|
||||
var checked = $(ev.target).hasClass('o_checked');
|
||||
var checklistId = parseInt(($(ev.target).attr('id') || '0').replace(/^checklist-id-/, ''));
|
||||
|
||||
this._rpc({
|
||||
route: '/web_editor/checklist',
|
||||
params: {
|
||||
res_model: this.model,
|
||||
res_id: this.res_id,
|
||||
filename: this.name,
|
||||
checklistId: checklistId,
|
||||
checked: !checked,
|
||||
},
|
||||
}).then(function (value) {
|
||||
self._setValue(value);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Method called when the wysiwyg instance is loaded.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onLoadWysiwyg: function () {
|
||||
var $button = this._renderTranslateButton();
|
||||
$button.css({
|
||||
'font-size': '15px',
|
||||
position: 'absolute',
|
||||
right: '+5px',
|
||||
});
|
||||
var $toolbar = this.$content.closest('.note-editor').find('.note-toolbar');
|
||||
$toolbar.css('position', 'relative');
|
||||
$toolbar.append($button);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onWysiwygBlur: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._doAction();
|
||||
if (ev.data.key === 'TAB') {
|
||||
this.trigger_up('navigation_move', {
|
||||
direction: ev.data.shiftKey ? 'left' : 'right',
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onWysiwygFocus: function (ev) {},
|
||||
});
|
||||
|
||||
field_registry
|
||||
.add('html', FieldTextHtmlSimple)
|
||||
.add('html_frame', FieldTextHtml);
|
||||
|
||||
return {
|
||||
FieldTextHtmlSimple: FieldTextHtmlSimple,
|
||||
FieldTextHtml: FieldTextHtml,
|
||||
};
|
||||
field_registry.add('html', FieldHtml);
|
||||
|
||||
|
||||
return FieldHtml;
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var weContext = require('web_editor.context');
|
||||
var localStorage = require('web.local_storage');
|
||||
var session = require('web.session');
|
||||
|
||||
@@ -157,6 +156,8 @@ var ViewEditor = Widget.extend({
|
||||
init: function (parent, viewKey, options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.context = options.context;
|
||||
|
||||
this.viewKey = viewKey;
|
||||
this.options = _.defaults({}, options, {
|
||||
position: 'right',
|
||||
|
||||
@@ -1,749 +0,0 @@
|
||||
odoo.define('web_editor.rte', function (require) {
|
||||
'use strict';
|
||||
|
||||
var base = require('web_editor.base');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
var weContext = require('web_editor.context');
|
||||
var summernote = require('web_editor.summernote');
|
||||
var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
// Summernote Lib (neek change to make accessible: method and object)
|
||||
var dom = summernote.core.dom;
|
||||
var range = summernote.core.range;
|
||||
|
||||
// Change History to have a global History for all summernote instances
|
||||
var History = function History($editable) {
|
||||
var aUndo = [];
|
||||
var pos = 0;
|
||||
var toSnap;
|
||||
|
||||
this.makeSnap = function (event, rng) {
|
||||
rng = rng || range.create();
|
||||
var elEditable = $(rng && rng.sc).closest('.o_editable')[0];
|
||||
if (!elEditable) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
event: event,
|
||||
editable: elEditable,
|
||||
contents: elEditable.innerHTML,
|
||||
bookmark: rng && rng.bookmark(elEditable),
|
||||
scrollTop: $(elEditable).scrollTop()
|
||||
};
|
||||
};
|
||||
|
||||
this.applySnap = function (oSnap) {
|
||||
var $editable = $(oSnap.editable);
|
||||
|
||||
if (document.documentMode) {
|
||||
$editable.removeAttr('contentEditable').removeProp('contentEditable');
|
||||
}
|
||||
|
||||
$editable.trigger('content_will_be_destroyed');
|
||||
$editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
|
||||
$editable.trigger('content_was_recreated');
|
||||
|
||||
$('.oe_overlay').remove();
|
||||
$('.note-control-selection').hide();
|
||||
|
||||
$editable.trigger('content_changed');
|
||||
|
||||
try {
|
||||
var r = oSnap.editable.innerHTML === '' ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
|
||||
r.select();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
$(document).trigger('click');
|
||||
$('.o_editable *').filter(function () {
|
||||
var $el = $(this);
|
||||
if ($el.data('snippet-editor')) {
|
||||
$el.removeData();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
_.defer(function () {
|
||||
var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
$editable.trigger('applySnap');
|
||||
|
||||
var evt = document.createEvent('MouseEvents');
|
||||
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
|
||||
target.dispatchEvent(evt);
|
||||
|
||||
$editable.trigger('keyup');
|
||||
});
|
||||
};
|
||||
|
||||
this.undo = function () {
|
||||
if (!pos) { return; }
|
||||
var _toSnap = toSnap;
|
||||
if (_toSnap) {
|
||||
this.saveSnap();
|
||||
}
|
||||
if (!aUndo[pos] && (!aUndo[pos] || aUndo[pos].event !== 'undo')) {
|
||||
var temp = this.makeSnap('undo');
|
||||
if (temp && (!pos || temp.contents !== aUndo[pos-1].contents)) {
|
||||
aUndo[pos] = temp;
|
||||
} else {
|
||||
pos--;
|
||||
}
|
||||
} else if (_toSnap) {
|
||||
pos--;
|
||||
}
|
||||
this.applySnap(aUndo[Math.max(--pos,0)]);
|
||||
while (pos && (aUndo[pos].event === 'blur' || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
|
||||
this.applySnap(aUndo[--pos]);
|
||||
}
|
||||
};
|
||||
|
||||
this.hasUndo = function () {
|
||||
return (toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo')) ||
|
||||
!!_.find(aUndo.slice(0, pos+1), function (undo) {
|
||||
return undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo';
|
||||
});
|
||||
};
|
||||
|
||||
this.getEditableHasUndo = function () {
|
||||
var editable = [];
|
||||
if ((toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo'))) {
|
||||
editable.push(toSnap.editable);
|
||||
}
|
||||
_.each(aUndo.slice(0, pos+1), function (undo) {
|
||||
if (undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo') {
|
||||
editable.push(undo.editable);
|
||||
}
|
||||
});
|
||||
return _.uniq(editable);
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
if (!aUndo[pos+1]) { return; }
|
||||
this.applySnap(aUndo[++pos]);
|
||||
while (aUndo[pos+1] && aUndo[pos].event === 'active') {
|
||||
this.applySnap(aUndo[pos++]);
|
||||
}
|
||||
};
|
||||
|
||||
this.hasRedo = function () {
|
||||
return aUndo.length > pos+1;
|
||||
};
|
||||
|
||||
this.recordUndo = function ($editable, event, internal_history) {
|
||||
var self = this;
|
||||
if (!$editable) {
|
||||
var rng = range.create();
|
||||
if (!rng) return;
|
||||
$editable = $(rng.sc).closest('.o_editable');
|
||||
}
|
||||
|
||||
if (aUndo[pos] && (event === 'applySnap' || event === 'activate')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!internal_history) {
|
||||
if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === 'activate') { // don't trigger change for all keypress
|
||||
setTimeout(function () {
|
||||
$editable.trigger('content_changed');
|
||||
},0);
|
||||
}
|
||||
}
|
||||
|
||||
if (aUndo[pos]) {
|
||||
pos = Math.min(pos, aUndo.length);
|
||||
aUndo.splice(pos, aUndo.length);
|
||||
}
|
||||
|
||||
// => make a snap when the user change editable zone (because: don't make snap for each keydown)
|
||||
if (toSnap && (toSnap.split || !event || toSnap.event !== event || toSnap.editable !== $editable[0])) {
|
||||
this.saveSnap();
|
||||
}
|
||||
|
||||
if (pos && aUndo[pos-1].editable !== $editable[0]) {
|
||||
var snap = this.makeSnap('blur', range.create(aUndo[pos-1].editable, 0));
|
||||
pos++;
|
||||
aUndo.push(snap);
|
||||
}
|
||||
|
||||
if (range.create()) {
|
||||
toSnap = self.makeSnap(event);
|
||||
} else {
|
||||
toSnap = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.splitNext = function () {
|
||||
if (toSnap) {
|
||||
toSnap.split = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.saveSnap = function () {
|
||||
if (toSnap) {
|
||||
if (!aUndo[pos]) {
|
||||
pos++;
|
||||
}
|
||||
aUndo.push(toSnap);
|
||||
delete toSnap.split;
|
||||
toSnap = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
var history = new History();
|
||||
|
||||
// jQuery extensions
|
||||
$.extend($.expr[':'], {
|
||||
o_editable: function (node, i, m) {
|
||||
while (node) {
|
||||
if (node.className && _.isString(node.className)) {
|
||||
if (node.className.indexOf('o_not_editable')!==-1 ) {
|
||||
return false;
|
||||
}
|
||||
if (node.className.indexOf('o_editable')!==-1 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
$.fn.extend({
|
||||
focusIn: function () {
|
||||
if (this.length) {
|
||||
range.create(dom.firstChild(this[0]), 0).select();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
focusInEnd: function () {
|
||||
if (this.length) {
|
||||
var last = dom.lastChild(this[0]);
|
||||
range.create(last, dom.nodeLength(last)).select();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
selectContent: function () {
|
||||
if (this.length) {
|
||||
var next = dom.lastChild(this[0]);
|
||||
range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
// RTE
|
||||
var RTEWidget = Widget.extend({
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, getConfig) {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.init_bootstrap_carousel = $.fn.carousel;
|
||||
this.edit_bootstrap_carousel = function () {
|
||||
var res = self.init_bootstrap_carousel.apply(this, arguments);
|
||||
// off bootstrap keydown event to remove event.preventDefault()
|
||||
// and allow to change cursor position
|
||||
$(this).off('keydown.bs.carousel');
|
||||
return res;
|
||||
};
|
||||
|
||||
this._getConfig = getConfig || this._getDefaultConfig;
|
||||
|
||||
base.computeFonts();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this.saving_mutex = new concurrency.Mutex();
|
||||
|
||||
$.fn.carousel = this.edit_bootstrap_carousel;
|
||||
|
||||
$(document).on('click.rte keyup.rte', function () {
|
||||
var current_range = {};
|
||||
try {
|
||||
current_range = range.create() || {};
|
||||
} catch (e) {
|
||||
// if range is on Restricted element ignore error
|
||||
}
|
||||
var $popover = $(current_range.sc).closest('[contenteditable]');
|
||||
var popover_history = ($popover.data()||{}).NoteHistory;
|
||||
if (!popover_history || popover_history === history) return;
|
||||
var editor = $popover.parent('.note-editor');
|
||||
$('button[data-event="undo"]', editor).attr('disabled', !popover_history.hasUndo());
|
||||
$('button[data-event="redo"]', editor).attr('disabled', !popover_history.hasRedo());
|
||||
});
|
||||
$(document).on('mousedown.rte activate.rte', this, this._onMousedown.bind(this));
|
||||
$(document).on('mouseup.rte', this, this._onMouseup.bind(this));
|
||||
|
||||
$('.o_not_editable').attr('contentEditable', false);
|
||||
|
||||
var $editable = this.editable();
|
||||
|
||||
// When a undo/redo is performed, the whole DOM is changed so we have
|
||||
// to prepare for it (website will restart animations for example)
|
||||
// TODO should be better handled
|
||||
$editable.on('content_will_be_destroyed', function (ev) {
|
||||
self.trigger_up('content_will_be_destroyed', {
|
||||
$target: $(ev.currentTarget),
|
||||
});
|
||||
});
|
||||
$editable.on('content_was_recreated', function (ev) {
|
||||
self.trigger_up('content_was_recreated', {
|
||||
$target: $(ev.currentTarget),
|
||||
});
|
||||
});
|
||||
|
||||
$editable.addClass('o_editable')
|
||||
.data('rte', this)
|
||||
.each(function () {
|
||||
var $node = $(this);
|
||||
|
||||
// fallback for firefox iframe display:none see https://github.com/odoo/odoo/pull/22610
|
||||
var computedStyles = window.getComputedStyle(this) || window.parent.getComputedStyle(this);
|
||||
// add class to display inline-block for empty t-field
|
||||
if (computedStyles.display === 'inline' && $node.data('oe-type') !== 'image') {
|
||||
$node.addClass('o_is_inline_editable');
|
||||
}
|
||||
});
|
||||
|
||||
// start element observation
|
||||
$(document).on('content_changed', '.o_editable', function (ev) {
|
||||
self.trigger_up('rte_change', {target: ev.target});
|
||||
if (!ev.__isDirtyHandled) {
|
||||
$(this).addClass('o_dirty');
|
||||
ev.__isDirtyHandled = true;
|
||||
}
|
||||
});
|
||||
|
||||
$('#wrapwrap, .o_editable').on('click.rte', '*', this, this._onClick.bind(this));
|
||||
|
||||
$('body').addClass('editor_enable');
|
||||
|
||||
$(document.body)
|
||||
.tooltip({
|
||||
selector: '[data-oe-readonly]',
|
||||
container: 'body',
|
||||
trigger: 'hover',
|
||||
delay: { 'show': 1000, 'hide': 100 },
|
||||
placement: 'bottom',
|
||||
title: _t("Readonly field")
|
||||
})
|
||||
.on('click', function () {
|
||||
$(this).tooltip('hide');
|
||||
});
|
||||
|
||||
$(document).trigger('mousedown');
|
||||
this.trigger('rte:start');
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this.cancel();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Stops the RTE.
|
||||
*/
|
||||
cancel: function () {
|
||||
if (this.$last) {
|
||||
this.$last.destroy();
|
||||
this.$last = null;
|
||||
}
|
||||
|
||||
$.fn.carousel = this.init_bootstrap_carousel;
|
||||
|
||||
$(document).off('.rte');
|
||||
$('#wrapwrap, .o_editable').off('.rte');
|
||||
|
||||
$('.o_not_editable').removeAttr('contentEditable');
|
||||
|
||||
$(document).off('click.rte keyup.rte mousedown.rte activate.rte mouseup.rte');
|
||||
$(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
|
||||
$(document).tooltip('dispose');
|
||||
$('body').removeClass('editor_enable');
|
||||
this.trigger('rte:stop');
|
||||
},
|
||||
/**
|
||||
* Returns the editable areas on the page.
|
||||
*
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
editable: function () {
|
||||
return $('#wrapwrap [data-oe-model]')
|
||||
.not('.o_not_editable')
|
||||
.filter(function () {
|
||||
return !$(this).closest('.o_not_editable').length;
|
||||
})
|
||||
.not('link, script')
|
||||
.not('[data-oe-readonly]')
|
||||
.not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
|
||||
.not('.oe_snippet_editor')
|
||||
.add('.o_editable');
|
||||
},
|
||||
/**
|
||||
* Records the current state of the given $target to be able to undo future
|
||||
* changes.
|
||||
*
|
||||
* @see History.recordUndo
|
||||
* @param {jQuery} $target
|
||||
* @param {string} event
|
||||
* @param {boolean} internal_history
|
||||
*/
|
||||
historyRecordUndo: function ($target, event, internal_history) {
|
||||
$target = $($target);
|
||||
var rng = range.create();
|
||||
var $editable = $(rng && rng.sc).closest('.o_editable');
|
||||
if (!rng || !$editable.length) {
|
||||
$editable = $target.closest('.o_editable');
|
||||
rng = range.create($target.closest('*')[0],0);
|
||||
} else {
|
||||
rng = $editable.data('range') || rng;
|
||||
}
|
||||
try {
|
||||
// TODO this line might break for unknown reasons. I suppose that
|
||||
// the created range is an invalid one. As it might be tricky to
|
||||
// adapt that line and that it is not a critical one, temporary fix
|
||||
// is to ignore the errors that this generates.
|
||||
rng.select();
|
||||
} catch (e) {
|
||||
console.log('error', e);
|
||||
}
|
||||
history.recordUndo($editable, event, internal_history);
|
||||
},
|
||||
/**
|
||||
* Searches all the dirty element on the page and saves them one by one. If
|
||||
* one cannot be saved, this notifies it to the user and restarts rte
|
||||
* edition.
|
||||
*
|
||||
* @param {Object} [context] - the context to use for saving rpc, default to
|
||||
* the editor context found on the page
|
||||
* @return {Deferred} rejected if the save cannot be done
|
||||
*/
|
||||
save: function (context) {
|
||||
var self = this;
|
||||
|
||||
$('.o_editable')
|
||||
.destroy()
|
||||
.removeClass('o_editable o_is_inline_editable');
|
||||
|
||||
var $dirty = $('.o_dirty');
|
||||
$dirty
|
||||
.removeAttr('contentEditable')
|
||||
.removeClass('o_dirty oe_carlos_danger o_is_inline_editable');
|
||||
var defs = _.map($dirty, function (el) {
|
||||
var $el = $(el);
|
||||
|
||||
$el.find('[class]').filter(function () {
|
||||
if (!this.getAttribute('class').match(/\S/)) {
|
||||
this.removeAttribute('class');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Add a queue with concurrency limit in webclient
|
||||
// https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
|
||||
return self.saving_mutex.exec(function () {
|
||||
return self._saveElement($el, context || weContext.get())
|
||||
.then(function () {
|
||||
$el.removeClass('o_dirty');
|
||||
}, function (response) {
|
||||
// because ckeditor regenerates all the dom, we can't just
|
||||
// setup the popover here as everything will be destroyed by
|
||||
// the DOM regeneration. Add markings instead, and returns a
|
||||
// new rejection with all relevant info
|
||||
var id = _.uniqueId('carlos_danger_');
|
||||
$el.addClass('o_dirty oe_carlos_danger ' + id);
|
||||
var html = (response.data.exception_type === 'except_osv');
|
||||
if (html) {
|
||||
var msg = $('<div/>', {text: response.data.message}).html();
|
||||
var data = msg.substring(3, msg.length -2).split(/', u'/);
|
||||
response.data.message = '<b>' + data[0] + '</b>' + data[1];
|
||||
}
|
||||
$('.o_editable.' + id)
|
||||
.removeClass(id)
|
||||
.popover({
|
||||
html: html,
|
||||
trigger: 'hover',
|
||||
content: response.data.message,
|
||||
placement: 'auto top',
|
||||
})
|
||||
.popover('show');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
window.onbeforeunload = null;
|
||||
}, function (failed) {
|
||||
// If there were errors, re-enable edition
|
||||
self.cancel();
|
||||
self.start();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* When the users clicks on an editable element, this function allows to add
|
||||
* external behaviors.
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
_enableEditableArea: function ($editable) {
|
||||
if ($editable.data('oe-type') === "monetary") {
|
||||
$editable.attr('contenteditable', false);
|
||||
$editable.find('.oe_currency_value').attr('contenteditable', true);
|
||||
}
|
||||
if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) {
|
||||
$editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* When an element enters edition, summernote is initialized on it. This
|
||||
* function returns the default configuration for the summernote instance.
|
||||
*
|
||||
* @see _getConfig
|
||||
* @private
|
||||
* @param {jQuery} $editable
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getDefaultConfig: function ($editable) {
|
||||
return {
|
||||
'airMode' : true,
|
||||
'focus': false,
|
||||
'airPopover': [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'italic', 'underline', 'clear']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture']],
|
||||
['history', ['undo', 'redo']],
|
||||
],
|
||||
'styleWithSpan': false,
|
||||
'inlinemedia' : ['p'],
|
||||
'lang': 'odoo',
|
||||
'onChange': function (html, $editable) {
|
||||
$editable.trigger('content_changed');
|
||||
},
|
||||
'colors': summernoteCustomColors,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Gets jQuery cloned element with internal text nodes escaped for XML
|
||||
* storage.
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $el
|
||||
* @return {jQuery}
|
||||
*/
|
||||
_getEscapedElement: function ($el) {
|
||||
var escaped_el = $el.clone();
|
||||
var to_escape = escaped_el.find('*').addBack();
|
||||
to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
|
||||
to_escape.contents().each(function () {
|
||||
if (this.nodeType === 3) {
|
||||
this.nodeValue = $('<div />').text(this.nodeValue).html();
|
||||
}
|
||||
});
|
||||
return escaped_el;
|
||||
},
|
||||
/**
|
||||
* Saves one (dirty) element of the page.
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $el - the element to save
|
||||
* @param {Object} context - the context to use for the saving rpc
|
||||
* @param {boolean} [withLang=false]
|
||||
* false if the lang must be omitted in the context (saving "master"
|
||||
* page element)
|
||||
*/
|
||||
_saveElement: function ($el, context, withLang) {
|
||||
return this._rpc({
|
||||
model: 'ir.ui.view',
|
||||
method: 'save',
|
||||
args: [
|
||||
$el.data('oe-id'),
|
||||
this._getEscapedElement($el).prop('outerHTML'),
|
||||
$el.data('oe-xpath') || null,
|
||||
],
|
||||
context: context,
|
||||
}, withLang ? undefined : {
|
||||
noContextKeys: 'lang',
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when any editable element is clicked -> Prevents default browser
|
||||
* action for the element.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} e
|
||||
*/
|
||||
_onClick: function (e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
/**
|
||||
* Called when the mouse is pressed on the document -> activate element
|
||||
* edition.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onMousedown: function (ev) {
|
||||
var $target = $(ev.target);
|
||||
var $editable = $target.closest('.o_editable');
|
||||
|
||||
if (!$editable.length || $.summernote.core.dom.isContentEditableFalse($target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Removes strange _moz_abspos attribute when it appears. Cannot
|
||||
// find another solution which works in all cases. A grabber still
|
||||
// appears at the same time which I did not manage to remove.
|
||||
// TODO find a complete and better solution
|
||||
_.defer(function () {
|
||||
$editable.find('[_moz_abspos]').removeAttr('_moz_abspos');
|
||||
});
|
||||
|
||||
if ($target.is('a')) {
|
||||
/**
|
||||
* Remove content editable everywhere and add it on the link only so that characters can be added
|
||||
* and removed at the start and at the end of it.
|
||||
*/
|
||||
$target.attr('contenteditable', true);
|
||||
_.defer(function () {
|
||||
$editable.not($target).attr('contenteditable', false);
|
||||
$target.focus();
|
||||
});
|
||||
|
||||
// Once clicked outside, remove contenteditable on link and reactive all
|
||||
$(document).on('mousedown.reactivate_contenteditable', function (e) {
|
||||
if ($target.is(e.target)) return;
|
||||
$target.removeAttr('contenteditable');
|
||||
$editable.attr('contenteditable', true);
|
||||
$(document).off('mousedown.reactivate_contenteditable');
|
||||
});
|
||||
}
|
||||
|
||||
if (this && this.$last && (!$editable.length || this.$last[0] !== $editable[0])) {
|
||||
var $destroy = this.$last;
|
||||
history.splitNext();
|
||||
|
||||
_.delay(function () {
|
||||
var id = $destroy.data('note-id');
|
||||
$destroy.destroy().removeData('note-id').removeAttr('data-note-id');
|
||||
$('#note-popover-'+id+', #note-handle-'+id+', #note-dialog-'+id+'').remove();
|
||||
}, 150); // setTimeout to remove flickering when change to editable zone (re-create an editor)
|
||||
this.$last = null;
|
||||
}
|
||||
if ($editable.length && (!this.$last || this.$last[0] !== $editable[0])) {
|
||||
$editable.summernote(this._getConfig($editable));
|
||||
|
||||
$editable.data('NoteHistory', history);
|
||||
this.$last = $editable;
|
||||
|
||||
// firefox & IE fix
|
||||
try {
|
||||
document.execCommand('enableObjectResizing', false, false);
|
||||
document.execCommand('enableInlineTableEditing', false, false);
|
||||
document.execCommand('2D-position', false, false);
|
||||
} catch (e) { /* */ }
|
||||
document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;});
|
||||
document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;});
|
||||
document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;});
|
||||
|
||||
if (!range.create()) {
|
||||
$editable.focusIn();
|
||||
}
|
||||
|
||||
if (dom.isImg($target[0])) {
|
||||
$target.trigger('mousedown'); // for activate selection on picture
|
||||
}
|
||||
|
||||
this._enableEditableArea($editable);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when the mouse is unpressed on the document.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onMouseup: function (ev) {
|
||||
var $target = $(ev.target);
|
||||
var $editable = $target.closest('.o_editable');
|
||||
|
||||
if (!$editable.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
_.defer(function () {
|
||||
self.historyRecordUndo($target, 'activate', true);
|
||||
});
|
||||
|
||||
// Browsers select different content from one to another after a
|
||||
// triple click (especially: if triple-clicking on a paragraph on
|
||||
// Chrome, blank characters of the element following the paragraph are
|
||||
// selected too)
|
||||
//
|
||||
// The triple click behavior is reimplemented for all browsers here
|
||||
if (ev.originalEvent.detail === 3) {
|
||||
// Select the whole content inside the deepest DOM element that was
|
||||
// triple-clicked
|
||||
range.create(ev.target, 0, ev.target, ev.target.childNodes.length).select();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
Class: RTEWidget,
|
||||
history: history,
|
||||
};
|
||||
});
|
||||
|
||||
odoo.define('web_editor.rte.summernote_custom_colors', function (require) {
|
||||
'use strict';
|
||||
|
||||
return [
|
||||
['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
|
||||
['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
|
||||
['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
|
||||
['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
|
||||
['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
|
||||
['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
|
||||
['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
|
||||
['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
|
||||
];
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,153 +0,0 @@
|
||||
odoo.define('web_editor.iframe', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var editor = require('web_editor.editor');
|
||||
var translator = require('web_editor.translate');
|
||||
var rte = require('web_editor.rte');
|
||||
|
||||
var callback = window ? window['callback'] : undefined;
|
||||
window.top.odoo[callback + '_updown'] = function (value, fields_values, field_name) {
|
||||
var $editable = $('#editable_area');
|
||||
if (value === $editable.prop('innerHTML')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('body').hasClass('editor_enable')) {
|
||||
if (value !== fields_values[field_name]) {
|
||||
rte.history.recordUndo($editable);
|
||||
}
|
||||
core.bus.trigger('deactivate_snippet');
|
||||
}
|
||||
|
||||
$editable.html(value);
|
||||
|
||||
if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
|
||||
$editable.trigger('content_changed');
|
||||
}
|
||||
};
|
||||
|
||||
editor.Class.include({
|
||||
start: function () {
|
||||
this.on('rte:start', this, function () {
|
||||
this.$('form').hide();
|
||||
|
||||
if (window.top.odoo[callback + '_editor']) {
|
||||
window.top.odoo[callback + '_editor'](this);
|
||||
}
|
||||
|
||||
var $editable = $('#editable_area');
|
||||
setTimeout(function () {
|
||||
$($editable.find('*').filter(function () {return !this.children.length;}).first()[0] || $editable)
|
||||
.focusIn().trigger('mousedown').trigger('keyup');
|
||||
},0);
|
||||
|
||||
$editable.on('content_changed', this, function () {
|
||||
if (window.top.odoo[callback + '_downup']) {
|
||||
window.top.odoo[callback + '_downup']($editable.prop('innerHTML'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
$(window.top).trigger('resize'); // TODO check, probably useless
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rte.Class.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getDefaultConfig: function ($editable) {
|
||||
var config = this._super.apply(this, arguments);
|
||||
if ($.deparam($.param.querystring()).debug !== undefined) {
|
||||
config.airPopover.splice(7, 0, ['view', ['codeview']]);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
translator.Class.include({
|
||||
start: function () {
|
||||
var res = this._super.apply(this, arguments);
|
||||
$('button[data-action=save]').hide();
|
||||
if (window.top.odoo[callback + '_editor']) {
|
||||
window.top.odoo[callback + '_editor'](this);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
|
||||
odoo.define('root.widget', function (require) {
|
||||
'use strict';
|
||||
|
||||
require('web.dom_ready');
|
||||
var iframeRootData = require('web_editor.IframeRoot');
|
||||
|
||||
var iframeRoot = new iframeRootData.IframeRoot(null);
|
||||
return iframeRoot.attachTo(document.body).then(function () {
|
||||
return iframeRoot;
|
||||
});
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
|
||||
odoo.define('web_editor.IframeRoot', function (require) {
|
||||
'use strict';
|
||||
|
||||
var BodyManager = require('web_editor.BodyManager');
|
||||
var weContext = require('web_editor.context');
|
||||
var editor = require('web_editor.editor');
|
||||
var rootWidget = require('web_editor.root_widget');
|
||||
var translate = require('web_editor.translate');
|
||||
|
||||
var iframeRootRegistry = new rootWidget.RootWidgetRegistry();
|
||||
|
||||
var IframeRoot = BodyManager.extend({
|
||||
/**
|
||||
* @override
|
||||
* @todo this is somehow a duplicate of website features
|
||||
*/
|
||||
start: function () {
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
|
||||
var ctx = weContext.getExtra();
|
||||
|
||||
if (ctx.editable && window.location.search.indexOf('enable_editor') >= 0) {
|
||||
var editorInstance = new (editor.Class)(this);
|
||||
defs.push(editorInstance.prependTo(this.$el));
|
||||
}
|
||||
|
||||
if (ctx.edit_translations) {
|
||||
var translator = new (translate.Class)(this, this.$('#wrapwrap'));
|
||||
defs.push(translator.prependTo(this.$el));
|
||||
}
|
||||
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* As the IframeRoot instance is designed to be unique, the associated
|
||||
* registry has been instantiated outside of the class and is simply
|
||||
* returned here.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_getRegistry: function () {
|
||||
return iframeRootRegistry;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
IframeRoot: IframeRoot,
|
||||
iframeRootRegistry: iframeRootRegistry,
|
||||
};
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
odoo.define('web_editor.inline', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var editor = require('web_editor.editor');
|
||||
var rte = require('web_editor.rte');
|
||||
var weWidgets = require('web_editor.widget');
|
||||
var transcoder = require('web_editor.transcoder');
|
||||
var snippet_editor = require('web_editor.snippet.editor');
|
||||
|
||||
weWidgets.MediaDialog.include({
|
||||
start: function () {
|
||||
this.$('[href="#editor-media-video"]').addClass('d-none');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
editor.Class.include({
|
||||
start: function () {
|
||||
if (window.location.search.indexOf('enable_editor') !== -1) {
|
||||
this.on('rte:start', this, function () {
|
||||
// move the caret at the end of the text when click after all content
|
||||
$('#wrapwrap').on('click', function (event) {
|
||||
if ($(event.target).is('#wrapwrap') || $(event.target).is('#editable_area:empty')) {
|
||||
_.defer(function () {
|
||||
var node = $('#editable_area *')
|
||||
.filter(function () { return this.textContent.match(/\S|\u00A0/); })
|
||||
.add($('#editable_area'))
|
||||
.last()[0];
|
||||
$.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
snippet_editor.Class.include({
|
||||
start: function () {
|
||||
_.defer(function () {
|
||||
var $editable = $('#editable_area');
|
||||
transcoder.linkImgToAttachmentThumbnail($editable);
|
||||
transcoder.imgToFont($editable);
|
||||
transcoder.styleToClass($editable);
|
||||
|
||||
// fix outlook image rendering bug
|
||||
$editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
cleanForSave: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var $editable = $('#editable_area');
|
||||
transcoder.attachmentThumbnailToLinkImg($editable);
|
||||
transcoder.fontToImg($editable);
|
||||
transcoder.classToStyle($editable);
|
||||
|
||||
// fix outlook image rendering bug
|
||||
_.each(['width', 'height'], function (attribute) {
|
||||
$editable.find('img[style*="width"], img[style*="height"]').attr(attribute, function () {
|
||||
return $(this)[attribute]();
|
||||
}).css(attribute, function () {
|
||||
return $(this).get(0).style[attribute] || 'auto';
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var callback = window ? window['callback'] : undefined;
|
||||
window.top.odoo[callback + '_updown'] = function (value, fields_values) {
|
||||
var $editable = $('#editable_area');
|
||||
value = value || '';
|
||||
if (value.indexOf('on_change_model_and_list') === -1 && value !== $editable.html()) {
|
||||
rte.history.recordUndo($editable, null, true);
|
||||
core.bus.trigger('deactivate_snippet');
|
||||
|
||||
$editable.html(value);
|
||||
|
||||
transcoder.imgToFont($editable);
|
||||
transcoder.styleToClass($editable);
|
||||
|
||||
// fix outlook image rendering bug
|
||||
$editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
|
||||
} else {
|
||||
$editable.trigger('content_changed');
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,325 +0,0 @@
|
||||
odoo.define('web_editor.tour', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var tour = require('web_tour.tour');
|
||||
var base = require('web_editor.base');
|
||||
|
||||
/**
|
||||
* Simulates a click event of given type on the given element.
|
||||
*
|
||||
* @param {DOMElement} el
|
||||
* @param {string} type - 'click', 'mouseup', ...
|
||||
*/
|
||||
function simulateClickEvent(el, type) {
|
||||
var evt = document.createEvent('MouseEvents');
|
||||
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
|
||||
el.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
tour.register('rte', {
|
||||
url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
|
||||
test: true,
|
||||
wait_for: base.ready(),
|
||||
}, [{
|
||||
content: "Change html for this test",
|
||||
trigger: "#editable_area",
|
||||
run: function () {
|
||||
var html = '\n'+
|
||||
'<section>\n'+
|
||||
' <div class="container" style="width: 600px;">\n'+
|
||||
' <div class="row">\n'+
|
||||
' <div class="col-lg-6 mt16">\n'+
|
||||
'<h1 id="text_content_id">Batnae municipium in Anthemusia</h1> \n'+
|
||||
' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
|
||||
' <p> Quam <img style="width: 25%" src="/web/static/src/img/logo.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
|
||||
' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
|
||||
'<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
|
||||
' </div>\n'+
|
||||
' <div class="col-lg-6 mt16">\n'+
|
||||
' <img class="img img-fluid shadow mb16" src="/web/static/src/img/logo.png" alt="Odoo text and image block">\n'+
|
||||
' </div>\n'+
|
||||
' </div>\n'+
|
||||
'</section>\n';
|
||||
this.$anchor.html(html);
|
||||
}
|
||||
}, {
|
||||
content: "simulate triple click and change text bg-color",
|
||||
trigger: '#editable_area > section .row > div:first',
|
||||
run: function () {
|
||||
var $h1 = $('h1', this.$anchor);
|
||||
$.summernote.core.range.create($h1[0].firstChild, 0, $('p', this.$anchor)[0], 0).select();
|
||||
simulateClickEvent($h1[0], 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "change text bg-color after triple click",
|
||||
trigger: '.note-color button.dropdown-toggle:has(.fa-paint-brush)',
|
||||
extra_trigger: '#editable_area > section .row > div:first',
|
||||
}, {
|
||||
content: "change text backColor",
|
||||
trigger: "button[data-event=backColor]:visible:eq(2)",
|
||||
}, {
|
||||
content: "change selection to change text color",
|
||||
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
|
||||
run: function () {
|
||||
$.summernote.core.range.create(this.$anchor[0].firstChild, 5, this.$anchor[0].firstChild, 10).select();
|
||||
simulateClickEvent(this.$anchor[0], 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "open color dropdown",
|
||||
trigger: ".note-color button.dropdown-toggle:has(.fa-font)",
|
||||
}, {
|
||||
content: "change text color",
|
||||
trigger: "button[data-event=foreColor]:visible:first",
|
||||
}, {
|
||||
content: "change selection to change text bg-color again",
|
||||
trigger: '#editable_area > section .row > div:first h1 font:eq(2)',
|
||||
run: function () {
|
||||
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor[0].firstChild, 10).select();
|
||||
simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "open color dropdown",
|
||||
trigger: ".note-color button.dropdown-toggle:has(.fa-paint-brush)",
|
||||
}, {
|
||||
content: "change text backColor again",
|
||||
trigger: "button[data-event=backColor]:visible:first",
|
||||
}, {
|
||||
content: "change selection (h1 and p) to change text color with class",
|
||||
trigger: '#editable_area > section .row > div:first h1 font:eq(4)',
|
||||
run: function () {
|
||||
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor.parent("h1").next("p")[0].firstChild, 30).select();
|
||||
simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "open color dropdown",
|
||||
trigger: ".note-color button.dropdown-toggle:has(.fa-font)",
|
||||
}, {
|
||||
content: "change text color again",
|
||||
trigger: "button[data-event=foreColor]:visible:eq(3)",
|
||||
}, {
|
||||
content: "delete selection",
|
||||
trigger: '.o_editable.note-editable.o_dirty',
|
||||
extra_trigger: '#editable_area > section .row > div:first p font',
|
||||
run: "keydown 46", // delete
|
||||
}, {
|
||||
content: "create an other selection to delete",
|
||||
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
|
||||
extra_trigger: '.o_editable.note-editable.o_dirty',
|
||||
run: function () {
|
||||
$.summernote.core.range.createFromNode(this.$anchor.next("p")[0]).clean();
|
||||
$.summernote.core.range.create(this.$anchor.find('font:containsExact(ici)')[0].firstChild, 1, this.$anchor.next().next()[0].firstChild, 5).select();
|
||||
simulateClickEvent(this.$anchor.find('font:last')[0], 'mouseup');
|
||||
},
|
||||
}, {
|
||||
content: "clean and delete (backspace) an other selection",
|
||||
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
|
||||
run: "keydown 8", // backspace
|
||||
}, {
|
||||
content: "an other selection",
|
||||
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
|
||||
extra_trigger: '.o_editable.note-editable.o_dirty',
|
||||
run: function () {
|
||||
$.summernote.core.range.create(this.$anchor.find('font:first')[0].firstChild, 3, this.$anchor.next("p")[0].childNodes[2], 8).select();
|
||||
simulateClickEvent(this.$anchor.find('font:first')[0], 'mouseup');
|
||||
},
|
||||
}, {
|
||||
content: "delete an other selection",
|
||||
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
|
||||
run: "keydown 46",
|
||||
}, {
|
||||
content: "undo",
|
||||
trigger: '.note-image-popover button[data-event="undo"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
|
||||
}, {
|
||||
content: "undo again",
|
||||
trigger: '.note-air-popover button[data-event="undo"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
|
||||
}, {
|
||||
content: "delete (backspace) after undo",
|
||||
trigger: '.o_editable.note-editable.o_dirty',
|
||||
extra_trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
|
||||
run: "keydown 8", // backspace
|
||||
}, {
|
||||
content: "click on image",
|
||||
trigger: '#editable_area > section .row > div:first img[style*="25%"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
|
||||
}, {
|
||||
content: "Click on resize half",
|
||||
trigger: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
|
||||
}, {
|
||||
content: "Click on edit picture",
|
||||
trigger: '.note-image-popover:visible button[data-event="showImageDialog"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first img[style*="50%"]',
|
||||
}, {
|
||||
content: "Click on pictogram tab",
|
||||
trigger: 'a[data-toggle="tab"]:contains(Pictogram)',
|
||||
extra_trigger: '#editor-media-image',
|
||||
}, {
|
||||
content: "select a pictogram",
|
||||
trigger: '#editor-media-icon.active span.fa:first',
|
||||
}, {
|
||||
content: "save pictogram",
|
||||
trigger: '.modal-footer > .btn-primary',
|
||||
extra_trigger: '#editor-media-icon.active span.o_selected',
|
||||
}, {
|
||||
content: "select a size for the pictogram",
|
||||
trigger: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
|
||||
}, {
|
||||
content: "click on float right",
|
||||
trigger: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
|
||||
extra_trigger: '#wrapwrap span.fa-3x',
|
||||
}, {
|
||||
content: "click on create link",
|
||||
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first span.fa.float-right',
|
||||
}, {
|
||||
content: "insert a link url",
|
||||
trigger: 'input[name="url"]',
|
||||
run: "text http://www.odoo.com",
|
||||
}, {
|
||||
content: "click on color style",
|
||||
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
|
||||
extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.float-right',
|
||||
}, {
|
||||
content: "save link",
|
||||
trigger: '.modal-footer > .btn-primary',
|
||||
extra_trigger: 'a#link-preview.btn.btn-success span.fa.fa-3x.float-right',
|
||||
}, {
|
||||
content: "click on other picture",
|
||||
trigger: '#editable_area > section .row > div:last img',
|
||||
extra_trigger: 'body:not(:has(#link-preview)) a.btn[href^="http://"]:has(span.fa.fa-3x.float-right)',
|
||||
}, {
|
||||
content: "click on create link again",
|
||||
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first span.fa.float-right',
|
||||
}, {
|
||||
content: "insert an email",
|
||||
trigger: 'input[name="url"]',
|
||||
run: "text test@test.test",
|
||||
}, {
|
||||
content: "click on color style again",
|
||||
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
|
||||
extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
|
||||
}, {
|
||||
content: "save link",
|
||||
trigger: '.modal-footer > .btn-primary',
|
||||
extra_trigger: 'a#link-preview.btn.btn-success[href="mailto:test@test.test"]:containsRegex(/^<img [^>]+>$/) img',
|
||||
}, {
|
||||
content: "select for triple enter then double backspace",
|
||||
trigger: '#editable_area > section .row > div:first p:eq(2)',
|
||||
extra_trigger: 'body:not(:has(#link-preview)) #editable_area > section .row > div:eq(1) > a > img',
|
||||
run: function () {
|
||||
var p = this.$anchor[0].firstChild;
|
||||
$.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
|
||||
simulateClickEvent(p, 'mouseup');
|
||||
},
|
||||
}, {
|
||||
content: "triple enter then double backspace",
|
||||
trigger: '#editable_area > section .row > div:first p:eq(2)',
|
||||
run: "keydown 66,13,66,13,13,8,8", // B enter B enter enter backspace backspace
|
||||
}, {
|
||||
content: "add ul content",
|
||||
trigger: '#editable_area > section .row > div:first',
|
||||
extra_trigger: 'body:not(:has(#editable_area > section .row > div:first p:eq(5), #editable_area > section .row > div:eq(3))) #editable_area > section .row > div:first p:eq(3)',
|
||||
run: function () {
|
||||
var html = ' <ul> '+
|
||||
'\n <li> <p>Batnae municipium. </p></li>'+
|
||||
'\n <li> Seleucia praeter.</li>'+
|
||||
'\n <li><p>Et hanc quidem.</p></li>'+
|
||||
'\n </ul>';
|
||||
this.$anchor.append(html);
|
||||
var node = this.$anchor.find('ul li p:last')[0].firstChild;
|
||||
$.summernote.core.range.create(node, 6).select();
|
||||
simulateClickEvent(node, 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "click on style dropdown",
|
||||
trigger: '.note-air-popover .note-style button.dropdown-toggle',
|
||||
extra_trigger: '#editable_area > section .row > div:first ul li p:first',
|
||||
}, {
|
||||
content: "select h3",
|
||||
trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
|
||||
}, {
|
||||
content: "select h3",
|
||||
trigger: '#editable_area > section .row > div:first > ul > li > h3',
|
||||
run: function () {
|
||||
var node = this.$anchor[0].firstChild;
|
||||
$.summernote.core.range.create(node, 0).select();
|
||||
simulateClickEvent(node, 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "double tabulation",
|
||||
trigger: '#editable_area > section .row > div:first > ul > li > h3',
|
||||
run: "keydown 9,9", // tabulation
|
||||
}, {
|
||||
content: "click on order list",
|
||||
trigger: '.note-air-popover button[data-event="insertOrderedList"]',
|
||||
extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
|
||||
}, {
|
||||
content: "select for enter in ul",
|
||||
trigger: '#editable_area > section .row > div:first ul li > p:last',
|
||||
run: function () {
|
||||
this.$anchor[0].firstChild.textContent += "";
|
||||
$.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
|
||||
simulateClickEvent(this.$anchor[0], 'mouseup');
|
||||
}
|
||||
}, {
|
||||
content: "enter in ul",
|
||||
trigger: '#editable_area > section .row > div:first ul li > p:last',
|
||||
run: "keydown 66,13", // enter
|
||||
}, {
|
||||
trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
|
||||
content: "backspace in list",
|
||||
run: "keydown 8",
|
||||
}, {
|
||||
content: "end",
|
||||
trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
|
||||
}]);
|
||||
|
||||
tour.register('rte_inline', {
|
||||
url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
|
||||
test: true,
|
||||
wait_for: base.ready(),
|
||||
}, [{
|
||||
content: "Change html for this test",
|
||||
trigger: "#editable_area",
|
||||
run: function () {
|
||||
var html = '\n'+
|
||||
'<div>\n'+
|
||||
' <table cellspacing="0" cellpadding="0" width="100%">\n'+
|
||||
' <tbody>\n'+
|
||||
' <tr>\n'+
|
||||
' <td valign="center" width="270">\n'+
|
||||
' <img src="/logo.png" alt="Your Logo" class="rounded-circle img-thumbnail">\n'+
|
||||
' </td>\n'+
|
||||
' <td valign="center" width="270">\n'+
|
||||
' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
|
||||
' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square float-right"></span>\n'+
|
||||
' </td>\n'+
|
||||
' </tr>\n'+
|
||||
' </tbody>\n'+
|
||||
' </table>\n'+
|
||||
'</div>';
|
||||
this.$anchor.html(html);
|
||||
}
|
||||
}, {
|
||||
content: "call clean for save",
|
||||
trigger: '#wrapwrap table',
|
||||
run: function () {
|
||||
core.bus.trigger('snippet_editor_clean_for_save');
|
||||
}
|
||||
}, {
|
||||
content: "check the image style",
|
||||
trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
|
||||
}, {
|
||||
content: "check the font image src",
|
||||
trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(0,160,157)/28"]',
|
||||
}, {
|
||||
content: "check the font class to css",
|
||||
trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
|
||||
}, {
|
||||
content: "check the second font class to css",
|
||||
trigger: '#wrapwrap img:eq(2)[style*="float: right"],#wrapwrap img:eq(2)[style*="float:right"]',
|
||||
}]);
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
odoo.define('web_editor.base', function (require) {
|
||||
odoo.define('wysiwyg.fonts', function (require) {
|
||||
'use strict';
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var session = require('web.session');
|
||||
|
||||
var domReady = $.Deferred();
|
||||
$(domReady.resolve.bind(domReady));
|
||||
|
||||
return {
|
||||
/**
|
||||
* Retrieves all the CSS rules which match the given parser (Regex).
|
||||
@@ -101,57 +95,5 @@ return {
|
||||
data.alias = _.flatten(_.map(data.cssData, _.property('names')));
|
||||
});
|
||||
}),
|
||||
/**
|
||||
* If a widget needs to be instantiated on page loading, it needs to wait
|
||||
* for appropriate resources to be loaded. This function returns a Deferred
|
||||
* which is resolved when the dom is ready, the session is bound
|
||||
* (translations loaded) and the XML is loaded. This should however not be
|
||||
* necessary anymore as widgets should not be parentless and should then be
|
||||
* instantiated (directly or not) by the page main component (webclient,
|
||||
* website root, editor bar, ...). The DOM will be ready then, the main
|
||||
* component is in charge of waiting for the session and the XML can be
|
||||
* lazy loaded thanks to the @see Widget.xmlDependencies key.
|
||||
*
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
ready: function () {
|
||||
return $.when(domReady, session.is_bound, ajax.loadXML());
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
|
||||
odoo.define('web_editor.context', function (require) {
|
||||
'use strict';
|
||||
|
||||
function getContext(context) {
|
||||
var html = document.documentElement;
|
||||
return _.extend({
|
||||
lang: (html.getAttribute('lang') || 'en_US').replace('-', '_'),
|
||||
}, context || {});
|
||||
}
|
||||
function getExtraContext(context) {
|
||||
var html = document.documentElement;
|
||||
return _.extend(getContext(), {
|
||||
editable: !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python
|
||||
translatable: !!html.dataset.translatable,
|
||||
edit_translations: !!html.dataset.edit_translations,
|
||||
}, context || {});
|
||||
}
|
||||
|
||||
return {
|
||||
get: getContext,
|
||||
getExtra: getExtraContext,
|
||||
};
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
|
||||
odoo.define('web_editor.ready', function (require) {
|
||||
'use strict';
|
||||
|
||||
var base = require('web_editor.base');
|
||||
|
||||
return base.ready();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
odoo.define('web_editor.wysiwyg.options', function (require) {
|
||||
'use strict';
|
||||
|
||||
return $.summernote.options;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.abstract', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Class = require('web.Class');
|
||||
var mixins = require('web.mixins');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
|
||||
var wysiwygOptions = require('web_editor.wysiwyg.options');
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// AbstractPlugin for summernote module API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
var AbstractPlugin = Class.extend(mixins.EventDispatcherMixin, ServicesMixin).extend({
|
||||
/**
|
||||
* Use this prop if you want to extend a summernote plugin.
|
||||
*/
|
||||
init: function (context) {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.setParent(context.options.parent);
|
||||
this.context = context;
|
||||
|
||||
if (!this.context.invoke) {
|
||||
// for use outside of wysiwyg/summernote
|
||||
this.context.invoke = function () {};
|
||||
}
|
||||
|
||||
this.$editable = context.layoutInfo.editable;
|
||||
this.editable = this.$editable[0];
|
||||
this.document = this.editable.ownerDocument;
|
||||
this.window = this.document.defaultView;
|
||||
this.summernote = this.window._summernoteSlave || $.summernote; // if the target is in iframe
|
||||
this.ui = this.summernote.ui;
|
||||
this.$editingArea = context.layoutInfo.editingArea;
|
||||
this.options = _.defaults(context.options || {}, wysiwygOptions);
|
||||
this.lang = wysiwygTranslation;
|
||||
this._addButtons();
|
||||
if (this.events) {
|
||||
this.events = _.clone(this.events);
|
||||
_.each(_.keys(this.events), function (key) {
|
||||
var value = self.events[key];
|
||||
if (typeof value === 'string') {
|
||||
value = self[value].bind(self);
|
||||
}
|
||||
if (key.indexOf('summernote.') === 0) {
|
||||
self.events[key] = value;
|
||||
} else {
|
||||
delete self.events[key];
|
||||
key = key.split(' ');
|
||||
if (key.length > 1) {
|
||||
self.context.layoutInfo.editor.on(key[0], key.slice(1).join(' '), value);
|
||||
} else {
|
||||
self.context.layoutInfo.editor.on(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public summernote module API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
shouldInitialize: function () {
|
||||
return true;
|
||||
},
|
||||
initialize: function () {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Override to add buttons.
|
||||
*/
|
||||
_addButtons: function () {},
|
||||
/**
|
||||
* Creates a dropdown button with its contents and behavior.
|
||||
*
|
||||
* @param {str} optionName
|
||||
* @param {str} buttonIcon (ex.: 'note-icon-align')
|
||||
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
|
||||
* @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
|
||||
* @param {function} onclick
|
||||
*/
|
||||
_createDropdownButton: function (optionName, buttonIcon, buttonTooltip, values, onclick) {
|
||||
var self = this;
|
||||
|
||||
if (!onclick) {
|
||||
onclick = function (e) {
|
||||
var classNames = _.map(values, function (item) {
|
||||
return item.value;
|
||||
}).join(' ');
|
||||
var $target = $(self.context.invoke('editor.restoreTarget'));
|
||||
$target.removeClass(classNames);
|
||||
if ($(e.target).data('value')) {
|
||||
$target.addClass($(e.target).data('value'));
|
||||
}
|
||||
};
|
||||
}
|
||||
if (optionName) {
|
||||
this.context.memo('button.' + optionName, function () {
|
||||
return self._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
|
||||
});
|
||||
} else {
|
||||
return this._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Creates a button to toggle a class.
|
||||
*
|
||||
* @param {str} optionName
|
||||
* @param {str} buttonIcon (ex.: 'note-icon-align')
|
||||
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
|
||||
* @param {str} className
|
||||
*/
|
||||
_createToggleButton: function (optionName, buttonIcon, buttonTooltip, className) {
|
||||
var self = this;
|
||||
return this._createButton(optionName, buttonIcon, buttonTooltip, function () {
|
||||
var $target = $(self.context.invoke('editor.restoreTarget'));
|
||||
$target.toggleClass(className);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Creates a button.
|
||||
*
|
||||
* @param {str} optionName
|
||||
* @param {str} buttonIcon (ex.: 'note-icon-align')
|
||||
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
|
||||
* @param {function} onclick
|
||||
*/
|
||||
_createButton: function (optionName, buttonIcon, buttonTooltip, onclick) {
|
||||
var self = this;
|
||||
if (optionName) {
|
||||
this.context.memo('button.' + optionName, function () {
|
||||
return self._renderButton(buttonIcon, buttonTooltip, onclick);
|
||||
});
|
||||
} else {
|
||||
return this._renderButton(buttonIcon, buttonTooltip, onclick);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Helper function to _createButton: renders the button.
|
||||
*
|
||||
* @param {str} buttonIcon (ex.: 'note-icon-align')
|
||||
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
|
||||
* @param {function} onclick
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
_renderButton: function (buttonIcon, buttonTooltip, onclick) {
|
||||
var self = this;
|
||||
return this.context.invoke('buttons.button', {
|
||||
contents: buttonIcon.indexOf('<') === -1 ? this.ui.icon(buttonIcon) : buttonIcon,
|
||||
tooltip: buttonTooltip,
|
||||
click: function (e) {
|
||||
e.preventDefault();
|
||||
self.context.invoke('editor.beforeCommand');
|
||||
onclick(e);
|
||||
self.editable.normalize();
|
||||
self.context.invoke('editor.saveRange');
|
||||
self.context.invoke('editor.afterCommand');
|
||||
},
|
||||
}).render();
|
||||
},
|
||||
/**
|
||||
* Helper function to _createDropdownButton: renders the dropdown button.
|
||||
*
|
||||
* @param {str} buttonIcon (ex.: 'note-icon-align')
|
||||
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
|
||||
* @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
|
||||
* @param {function} onclick
|
||||
* @param {JQuery}
|
||||
*/
|
||||
_renderDropdownButton: function (buttonIcon, buttonTooltip, values, onclick) {
|
||||
return this.ui.buttonGroup([
|
||||
this.context.invoke('buttons.button', {
|
||||
className: 'dropdown-toggle',
|
||||
contents: buttonIcon.indexOf('<') === -1 ?
|
||||
this.ui.dropdownButtonContents(this.ui.icon(buttonIcon), this.options) : buttonIcon,
|
||||
tooltip: buttonTooltip,
|
||||
data: {
|
||||
toggle: 'dropdown'
|
||||
}
|
||||
}),
|
||||
this.ui.dropdown({
|
||||
items: values,
|
||||
template: function (item) {
|
||||
return item.string;
|
||||
},
|
||||
click: this._wrapCommand(function (e) {
|
||||
e.preventDefault();
|
||||
onclick(e);
|
||||
}),
|
||||
})
|
||||
]).render();
|
||||
},
|
||||
/**
|
||||
* Wraps a given function between common actions required
|
||||
* for history (undo/redo) and the maintenance of the DOM/range.
|
||||
*
|
||||
* @param {function} fn
|
||||
* @returns {any} the return of fn
|
||||
*/
|
||||
_wrapCommand: function (fn) {
|
||||
var self = this;
|
||||
return function () {
|
||||
self.context.invoke('editor.restoreRange');
|
||||
self.context.invoke('editor.beforeCommand');
|
||||
var res = fn.apply(self, arguments);
|
||||
self.editable.normalize();
|
||||
self.context.invoke('editor.saveRange');
|
||||
self.context.invoke('editor.afterCommand');
|
||||
return res;
|
||||
};
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
return AbstractPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,565 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.bullet', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
|
||||
var wysiwygOptions = require('web_editor.wysiwyg.options');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
|
||||
|
||||
var _t = core._t;
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
wysiwygOptions.icons.checklist = 'fa fa-check-square';
|
||||
wysiwygOptions.keyMap.pc['CTRL+SHIFT+NUM9'] = 'insertCheckList';
|
||||
wysiwygOptions.keyMap.mac['CMD+SHIFT+NUM9'] = 'insertCheckList';
|
||||
wysiwygTranslation.lists.checklist = _t('Checklist');
|
||||
wysiwygTranslation.help.checklist = _t('Toggle checkbox list');
|
||||
|
||||
|
||||
var BulletPlugin = AbstractPlugin.extend({
|
||||
events: {
|
||||
'summernote.mousedown': '_onMouseDown',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Insert an ordered list, an unordered list or a checklist.
|
||||
* If already in list, remove the list or convert it to the given type.
|
||||
*
|
||||
* @param {string('ol'|'ul'|'checklist')} type the type of list to insert
|
||||
* @returns {false|Node[]} contents of the ul/ol or content of the converted/removed list
|
||||
*/
|
||||
insertList: function (type) {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
var res;
|
||||
var start = range.getStartPoint();
|
||||
var end = range.getEndPoint();
|
||||
|
||||
var isInList = dom.ancestor(range.sc, dom.isList);
|
||||
if (isInList) {
|
||||
res = this._convertList(false, [], start, end, type);
|
||||
} else {
|
||||
var ul = this._createList(type);
|
||||
res = [].slice.call(ul.children);
|
||||
}
|
||||
|
||||
var startLeaf = this.context.invoke('HelperPlugin.firstLeaf', start.node);
|
||||
var endLeaf = this.context.invoke('HelperPlugin.firstLeaf', end.node);
|
||||
range = this.context.invoke('editor.setRange', startLeaf, start.offset, endLeaf, end.offset);
|
||||
range.select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
|
||||
return res;
|
||||
},
|
||||
/**
|
||||
* Indent a node (list or format node).
|
||||
*
|
||||
* @returns {false|Node[]} contents of list/indented item
|
||||
*/
|
||||
indent: function () {
|
||||
return this._indent();
|
||||
},
|
||||
/**
|
||||
* Outdent a node (list or format node).
|
||||
*
|
||||
* @returns {false|Node[]} contents of list/outdented item
|
||||
*/
|
||||
outdent: function () {
|
||||
return this._indent(true);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add checklist buttons.
|
||||
*/
|
||||
_addButtons: function () {
|
||||
var self = this;
|
||||
this._super();
|
||||
this.context.memo('help.checklist', this.lang.help.checklist);
|
||||
this._createButton('checklist', this.options.icons.checklist, this.lang.lists.checklist, function (e) {
|
||||
e.preventDefault();
|
||||
self.context.invoke('editor.insertCheckList');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Convert ul<->ol or remove ul/ol.
|
||||
*
|
||||
* @param {boolean} isWithinElem true if selection already inside the LI
|
||||
* @param {DOM[]} nodes selected nodes
|
||||
* @param {Object} startPoint
|
||||
* @param {Object} endPoint
|
||||
* @param {boolean} sorted
|
||||
* @returns {boolean} isWithinElem
|
||||
*/
|
||||
_convertList: function (isWithinElem, nodes, startPoint, endPoint, sorted) {
|
||||
var ol = dom.ancestor(startPoint.node, dom.isList);
|
||||
var parent = ol.parentNode;
|
||||
|
||||
// get selected lis
|
||||
|
||||
var lis = [];
|
||||
var lisBefore = [];
|
||||
var lisAfter = [];
|
||||
_.each(ol.children, function (li) {
|
||||
if (!isWithinElem && (li === startPoint.node || $.contains(li, startPoint.node))) {
|
||||
isWithinElem = true;
|
||||
}
|
||||
if (isWithinElem) {
|
||||
lis.push(li);
|
||||
} else if (lis.length) {
|
||||
lisAfter.push(li);
|
||||
} else {
|
||||
lisBefore.push(li);
|
||||
}
|
||||
if (isWithinElem && (li === endPoint.node || $.contains(li, endPoint.node))) {
|
||||
isWithinElem = false;
|
||||
}
|
||||
});
|
||||
|
||||
var res = lis;
|
||||
|
||||
if (lisBefore.length) {
|
||||
var ulBefore = this.document.createElement(ol.tagName);
|
||||
ulBefore.className = ol.className;
|
||||
|
||||
if (dom.isLi(ol.parentNode)) {
|
||||
var li = this.document.createElement('li');
|
||||
li.className = ol.parentNode.className;
|
||||
$(li).insertBefore(ol.parentNode);
|
||||
li.appendChild(ulBefore);
|
||||
} else {
|
||||
$(ulBefore).insertBefore(ol);
|
||||
}
|
||||
|
||||
$(ulBefore).append(lisBefore);
|
||||
}
|
||||
if (lisAfter.length) {
|
||||
var ulAfter = this.document.createElement(ol.tagName);
|
||||
ulAfter.className = ol.className;
|
||||
|
||||
if (dom.isLi(ol.parentNode)) {
|
||||
var li = this.document.createElement('li');
|
||||
li.className = ol.parentNode.className;
|
||||
$(li).insertAfter(ol.parentNode);
|
||||
li.appendChild(ulAfter);
|
||||
} else {
|
||||
$(ulAfter).insertAfter(ol);
|
||||
}
|
||||
|
||||
$(ulAfter).append(lisAfter);
|
||||
}
|
||||
|
||||
// convert ul<->ol or remove list
|
||||
var current = ol.tagName === 'UL' && ol.className.indexOf('o_checklist') !== -1 ? 'checklist' : ol.tagName.toLowerCase();
|
||||
if (current !== sorted) {
|
||||
// convert ul <-> ol
|
||||
|
||||
var ul;
|
||||
if (sorted === 'checklist' && current === "ul") {
|
||||
ul = ol;
|
||||
} else if (sorted === 'ul' && current === 'checklist') {
|
||||
$(ol).removeClass('o_checklist');
|
||||
ul = ol;
|
||||
} else {
|
||||
ul = this.document.createElement(sorted === "ol" ? "ol" : "ul");
|
||||
ul.className = ol.className;
|
||||
$(ul).insertBefore(ol).append(lis);
|
||||
parent.removeChild(ol);
|
||||
}
|
||||
if (sorted === 'checklist') {
|
||||
$(ul).addClass('o_checklist');
|
||||
}
|
||||
|
||||
this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
|
||||
this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
|
||||
|
||||
} else {
|
||||
// remove ol/ul
|
||||
|
||||
if (dom.isLi(parent) || dom.isList(parent)) {
|
||||
if (dom.isLi(parent)) {
|
||||
ol = parent;
|
||||
parent = ol.parentNode;
|
||||
}
|
||||
$(lis).insertBefore(ol);
|
||||
} else {
|
||||
res = [];
|
||||
_.each(lis, function (li) {
|
||||
res.push.apply(res, li.childNodes);
|
||||
$(li.childNodes).insertBefore(ol);
|
||||
});
|
||||
|
||||
// wrap in p
|
||||
|
||||
var hasNode = _.find(res, function (node) {
|
||||
return node.tagName && node.tagName !== "BR" && !dom.isMedia(node);
|
||||
});
|
||||
if (!hasNode) {
|
||||
var p = this.document.createElement('p');
|
||||
$(p).insertBefore(ol).append(res);
|
||||
res = [p];
|
||||
}
|
||||
}
|
||||
parent.removeChild(ol);
|
||||
|
||||
}
|
||||
|
||||
nodes.push.apply(nodes, res);
|
||||
|
||||
return isWithinElem;
|
||||
},
|
||||
/**
|
||||
* Create a list if allowed.
|
||||
*
|
||||
* @param {string('ol'|'ul'|'checklist')} type the type of list to insert
|
||||
* @returns {false|Node} the list, if any
|
||||
*/
|
||||
_createList: function (type) {
|
||||
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
|
||||
var formatNodes = this._filterEditableFormatNodes(nodes);
|
||||
if (!formatNodes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ul = this._createListElement(type);
|
||||
$(formatNodes[0][0] || formatNodes[0]).before(ul);
|
||||
this._fillListElementWith(ul, formatNodes);
|
||||
this._deleteListElementEdges(ul);
|
||||
|
||||
return ul;
|
||||
},
|
||||
/**
|
||||
* Create a list element of the given type and return it.
|
||||
*
|
||||
* @param {string('ol'|'ul'|'checklist')} type the type of list to insert
|
||||
* @returns {Node}
|
||||
*/
|
||||
_createListElement: function (type) {
|
||||
var ul = this.document.createElement(type === "ol" ? "ol" : "ul");
|
||||
if (type === 'checklist') {
|
||||
ul.className = 'o_checklist';
|
||||
}
|
||||
return ul;
|
||||
},
|
||||
/**
|
||||
* Delete a list element's edges if necessary.
|
||||
*
|
||||
* @param {Node} ul
|
||||
*/
|
||||
_deleteListElementEdges: function (ul) {
|
||||
this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
|
||||
this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
|
||||
this.editable.normalize();
|
||||
},
|
||||
/**
|
||||
* Fill a list element with the nodes passed, wrapped in LIs.
|
||||
*
|
||||
* @param {Node} ul
|
||||
* @param {Node[]} nodes
|
||||
*/
|
||||
_fillListElementWith: function (ul, nodes) {
|
||||
var self = this;
|
||||
_.each(nodes, function (node) {
|
||||
var li = self.document.createElement('li');
|
||||
$(li).append(node);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Filter the editable format ancestors of the given nodes
|
||||
* and fill or wrap them if needed for range selection.
|
||||
*
|
||||
* @param {Node[]} nodes
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
_filterEditableFormatNodes: function (nodes) {
|
||||
var self = this;
|
||||
var formatNodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
|
||||
formatNodes = _.compact(_.map(formatNodes, function (node) {
|
||||
var ancestor = (!node.tagName || node.tagName === 'BR') && dom.ancestor(node, dom.isCell);
|
||||
if (ancestor && self.options.isEditableNode(ancestor)) {
|
||||
if (!ancestor.childNodes.length) {
|
||||
var br = self.document.createElement('br');
|
||||
ancestor.appendChild(br);
|
||||
}
|
||||
var p = self.document.createElement('p');
|
||||
$(p).append(ancestor.childNodes);
|
||||
ancestor.appendChild(p);
|
||||
return p;
|
||||
}
|
||||
return self.options.isEditableNode(node) && node || null;
|
||||
}));
|
||||
return formatNodes;
|
||||
},
|
||||
/**
|
||||
* Indent or outdent a format node.
|
||||
*
|
||||
* @param {bool} outdent true to outdent, false to indent
|
||||
* @returns {false|[]Node} indented nodes
|
||||
*/
|
||||
_indent: function (outdent) {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var nodes = [];
|
||||
var isWithinElem;
|
||||
var ancestor = range.commonAncestor();
|
||||
var $dom = $(ancestor);
|
||||
|
||||
if (!dom.isList(ancestor)) {
|
||||
// to indent a selection, we indent the child nodes of the common
|
||||
// ancestor that contains this selection
|
||||
$dom = $(ancestor.tagName ? ancestor : ancestor.parentNode).children();
|
||||
}
|
||||
|
||||
// if selection is inside indented contents and outdent is true, we can outdent this node
|
||||
var indentedContent = outdent && dom.ancestor(ancestor, function (node) {
|
||||
var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
|
||||
return node.tagName && !!parseFloat(node.style[style] || 0);
|
||||
});
|
||||
|
||||
if (indentedContent) {
|
||||
$dom = $(indentedContent);
|
||||
} else {
|
||||
// if selection is inside a list, we indent its list items
|
||||
$dom = $(dom.ancestor(ancestor, dom.isList));
|
||||
if (!$dom.length) {
|
||||
// if the selection is contained in a single HTML node, we indent
|
||||
// the first ancestor 'content block' (P, H1, PRE, ...) or TD
|
||||
$dom = $(range.sc).closest(this.options.styleTags.join(',') + ',td');
|
||||
}
|
||||
}
|
||||
|
||||
// if select tr, take the first td
|
||||
$dom = $dom.map(function () {
|
||||
return this.tagName === "TR" ? this.firstElementChild : this;
|
||||
});
|
||||
|
||||
$dom.each(function () {
|
||||
if (isWithinElem || $.contains(this, range.sc)) {
|
||||
if (dom.isList(this)) {
|
||||
if (outdent) {
|
||||
var type = this.tagName === 'OL' ? 'ol' : (this.className && this.className.indexOf('o_checklist') !== -1 ? 'checklist' : 'ul');
|
||||
isWithinElem = self._convertList(isWithinElem, nodes, range.getStartPoint(), range.getEndPoint(), type);
|
||||
} else {
|
||||
isWithinElem = self._indentUL(isWithinElem, nodes, this, range.sc, range.ec);
|
||||
}
|
||||
} else if (self.context.invoke('HelperPlugin.isFormatNode', this) || dom.ancestor(this, dom.isCell)) {
|
||||
isWithinElem = self._indentFormatNode(outdent, isWithinElem, nodes, this, range.sc, range.ec);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($dom.parent().length) {
|
||||
var $parent = $dom.parent();
|
||||
|
||||
// remove text nodes between lists
|
||||
var $ul = $parent.find('ul, ol');
|
||||
if (!$ul.length) {
|
||||
$ul = $(dom.ancestor(range.sc, dom.isList));
|
||||
}
|
||||
$ul.each(function () {
|
||||
var notWhitespace = /\S/;
|
||||
if (
|
||||
this.previousSibling &&
|
||||
this.previousSibling !== this.previousElementSibling &&
|
||||
!this.previousSibling.textContent.match(notWhitespace)
|
||||
) {
|
||||
this.parentNode.removeChild(this.previousSibling);
|
||||
}
|
||||
if (
|
||||
this.nextSibling &&
|
||||
this.nextSibling !== this.nextElementSibling &&
|
||||
!this.nextSibling.textContent.match(notWhitespace)
|
||||
) {
|
||||
this.parentNode.removeChild(this.nextSibling);
|
||||
}
|
||||
});
|
||||
|
||||
// merge same ul or ol
|
||||
$ul.prev('ul, ol').each(function () {
|
||||
self.context.invoke('HelperPlugin.deleteEdge', this, 'next');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
range.normalize().select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
|
||||
return !!nodes.length && nodes;
|
||||
},
|
||||
/**
|
||||
* Indent several LIs in a list.
|
||||
*
|
||||
* @param {bool} isWithinElem true if selection already inside the LI
|
||||
* @param {Node[]} nodes
|
||||
* @param {Node} UL
|
||||
* @param {Node} start
|
||||
* @param {Node} end
|
||||
* @returns {bool} isWithinElem
|
||||
*/
|
||||
_indentUL: function (isWithinElem, nodes, UL, start, end) {
|
||||
var next;
|
||||
var tagName = UL.tagName;
|
||||
var node = UL.firstChild;
|
||||
var ul = document.createElement(tagName);
|
||||
ul.className = UL.className;
|
||||
var flag;
|
||||
|
||||
if (isWithinElem) {
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// create and fill ul into a li
|
||||
while (node) {
|
||||
if (flag || node === start || $.contains(node, start)) {
|
||||
isWithinElem = true;
|
||||
node.parentNode.insertBefore(ul, node);
|
||||
}
|
||||
next = node.nextElementSibling;
|
||||
if (isWithinElem) {
|
||||
ul.appendChild(node);
|
||||
nodes.push(node);
|
||||
}
|
||||
if (node === end || $.contains(node, end)) {
|
||||
isWithinElem = false;
|
||||
break;
|
||||
}
|
||||
node = next;
|
||||
}
|
||||
|
||||
var temp;
|
||||
var prev = ul.previousElementSibling;
|
||||
if (
|
||||
prev && prev.tagName === "LI" &&
|
||||
(temp = prev.firstElementChild) && temp.tagName === tagName &&
|
||||
((prev.firstElementChild || prev.firstChild) !== ul)
|
||||
) {
|
||||
$(prev.firstElementChild || prev.firstChild).append($(ul).contents());
|
||||
$(ul).remove();
|
||||
ul = prev;
|
||||
ul.parentNode.removeChild(ul.nextElementSibling);
|
||||
}
|
||||
next = ul.nextElementSibling;
|
||||
if (
|
||||
next && next.tagName === "LI" &&
|
||||
(temp = next.firstElementChild) && temp.tagName === tagName &&
|
||||
(ul.firstElementChild !== next.firstElementChild)
|
||||
) {
|
||||
$(ul.firstElementChild).append($(next.firstElementChild).contents());
|
||||
$(next.firstElementChild).remove();
|
||||
ul.parentNode.removeChild(ul.nextElementSibling);
|
||||
}
|
||||
|
||||
// wrap in li
|
||||
var li = this.document.createElement('li');
|
||||
li.className = 'o_indent';
|
||||
$(ul).before(li);
|
||||
li.appendChild(ul);
|
||||
|
||||
return isWithinElem;
|
||||
},
|
||||
/**
|
||||
* Outdent a container node.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {float} margin
|
||||
*/
|
||||
_outdentContainer: function (node) {
|
||||
var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
|
||||
var margin = parseFloat(node.style[style] || 0) - 1.5;
|
||||
node.style[style] = margin > 0 ? margin + "em" : "";
|
||||
return margin;
|
||||
},
|
||||
/**
|
||||
* Indent a container node.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {float} margin
|
||||
*/
|
||||
_indentContainer: function (node) {
|
||||
var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
|
||||
var margin = parseFloat(node.style[style] || 0) + 1.5;
|
||||
node.style[style] = margin + "em";
|
||||
return margin;
|
||||
},
|
||||
/**
|
||||
* Indent/outdent a format node.
|
||||
*
|
||||
* @param {bool} outdent true to outdent, false to indent
|
||||
* @param {bool} isWithinElem true if selection already inside the element
|
||||
* @param {DOM[]} nodes
|
||||
* @param {DOM} p
|
||||
* @param {DOM} start
|
||||
* @param {DOM} end
|
||||
* @returns {bool} isWithinElem
|
||||
*/
|
||||
_indentFormatNode: function (outdent, isWithinElem, nodes, p, start, end) {
|
||||
if (p === start || $.contains(p, start) || $.contains(start, p)) {
|
||||
isWithinElem = true;
|
||||
}
|
||||
if (isWithinElem) {
|
||||
nodes.push(p);
|
||||
if (outdent) {
|
||||
this._outdentContainer(p);
|
||||
} else {
|
||||
this._indentContainer(p);
|
||||
}
|
||||
}
|
||||
if (p === end || $.contains(p, end) || $.contains(end, p)) {
|
||||
isWithinElem = false;
|
||||
}
|
||||
return isWithinElem;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {SummernoteEvent} se
|
||||
* @param {jQueryEvent} e
|
||||
*/
|
||||
_onMouseDown: function (se, e) {
|
||||
if (!dom.isLi(e.target) || !$(e.target).parent('ul.o_checklist').length || e.offsetX > 0) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
var checked = $(e.target).hasClass('o_checked');
|
||||
$(e.target).toggleClass('o_checked', !checked);
|
||||
var $sublevel = $(e.target).next('ul.o_checklist, li:has(> ul.o_checklist)').find('> li, ul.o_checklist > li');
|
||||
var $parents = $(e.target).parents('ul.o_checklist').map(function () {
|
||||
return this.parentNode.tagName === 'LI' ? this.parentNode : this;
|
||||
});
|
||||
if (checked) {
|
||||
$sublevel.removeClass('o_checked');
|
||||
$parents.prev('ul.o_checklist li').removeClass('o_checked');
|
||||
} else {
|
||||
$sublevel.addClass('o_checked');
|
||||
var $lis;
|
||||
do {
|
||||
$lis = $parents.not(':has(li:not(.o_checked))').prev('ul.o_checklist li:not(.o_checked)');
|
||||
$lis.addClass('o_checked');
|
||||
} while ($lis.length);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('BulletPlugin', BulletPlugin);
|
||||
|
||||
return BulletPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.buttons', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
Plugins.buttons.include({
|
||||
/**
|
||||
* Fix tooltip for 'option' buttons (summernote 0.8.9 bug)
|
||||
* Remove this once the library is updated to 0.8.10.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
addToolbarButtons: function () {
|
||||
this.options.codeview = this.lang.options.codeview;
|
||||
this.options.help = this.lang.options.help;
|
||||
this.options.fullscreen = this.lang.options.fullscreen;
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* Show current style (of start of selection) in magic wand dropdown.
|
||||
*
|
||||
* @param {JQuery Object} $container
|
||||
*/
|
||||
updateActiveStyleDropdown: function ($container) {
|
||||
var self = this;
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
var el = dom.ancestor(range.sc, function (n) {
|
||||
return n.tagName && self.options.styleTags.indexOf(n.tagName.toLowerCase()) !== -1;
|
||||
});
|
||||
if (el) {
|
||||
var tagName = el.tagName.toLowerCase();
|
||||
$container.find('.dropdown-style a').each(function (idx, item) {
|
||||
var $item = $(item);
|
||||
// always compare string to avoid creating another func.
|
||||
var isChecked = ($item.data('value') + '') === (tagName + '');
|
||||
$item.toggleClass('active', isChecked);
|
||||
});
|
||||
} else {
|
||||
var $item = $container.find('.dropdown-style a.active');
|
||||
$item.removeClass('active');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
updateCurrentStyle: function ($container) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.updateParaAlignIcon($container || this.$toolbar);
|
||||
this.updateActiveStyleDropdown($container || this.$toolbar);
|
||||
},
|
||||
/**
|
||||
* Show current paragraph alignment (of start of selection) on paragraph alignment dropdown.
|
||||
*
|
||||
* @param {JQuery Object} $container
|
||||
*/
|
||||
updateParaAlignIcon: function ($container) {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
var $paraIcon = $container.find('.note-para .dropdown-toggle i');
|
||||
var el = dom.isText(range.sc) ? range.sc.parentNode : range.sc;
|
||||
if (el) {
|
||||
$paraIcon.removeClass();
|
||||
switch ($(el).css('text-align')) {
|
||||
case 'left':
|
||||
$paraIcon.addClass('note-icon-align-left');
|
||||
break;
|
||||
case 'center':
|
||||
$paraIcon.addClass('note-icon-align-center');
|
||||
break;
|
||||
case 'right':
|
||||
$paraIcon.addClass('note-icon-align-right');
|
||||
break;
|
||||
case 'justify':
|
||||
$paraIcon.addClass('note-icon-align-justify');
|
||||
break;
|
||||
default:
|
||||
$paraIcon.addClass('note-icon-align-left');
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return Plugins.buttons;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.codeview', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var CodeviewPlugin = Plugins.codeview.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
activate: function () {
|
||||
this._super();
|
||||
if (this.$codable.height() === 0) {
|
||||
this.$codable.height(180);
|
||||
}
|
||||
this.context.invoke('editor.hidePopover');
|
||||
this.context.invoke('editor.clearTarget');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
deactivate: function () {
|
||||
if (
|
||||
this.context.invoke('HelperPlugin.hasJinja', this.context.invoke('code')) &&
|
||||
!this.isBeingDestroyed
|
||||
) {
|
||||
var message = _t("Your code contains JINJA conditions.\nRemove them to return to WYSIWYG HTML edition.");
|
||||
this.do_warn(_t("Cannot edit HTML"), message);
|
||||
this.$codable.focus();
|
||||
return;
|
||||
}
|
||||
this._super();
|
||||
this.$editable.css('height', '');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this.isBeingDestroyed = true;
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* Force activation of the code view.
|
||||
*/
|
||||
forceActivate: function () {
|
||||
if (!this.isActivated()) {
|
||||
this.activate();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('codeview', CodeviewPlugin);
|
||||
|
||||
return CodeviewPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.dropzone', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var _t = core._t;
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
var DropzonePlugin = Plugins.dropzone.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public summernote module API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Disable Summernote's handling of drop events.
|
||||
*/
|
||||
attachDragAndDropEvent: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$dropzone.off('drop');
|
||||
this.$dropzone.on('drop', this._onDrop.bind(this));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Clean up then drops HTML or plain text into the editor.
|
||||
*
|
||||
* @private
|
||||
* @param {String} html
|
||||
* @param {Boolean} textOnly true to allow only dropping plain text
|
||||
*/
|
||||
_dropHTML: function (html, textOnly) {
|
||||
this.context.invoke('editor.beforeCommand');
|
||||
|
||||
// Clean up
|
||||
var nodes = this.context.invoke('TextPlugin.prepareClipboardData', html);
|
||||
|
||||
// Delete selection
|
||||
this.context.invoke('HelperPlugin.deleteSelection');
|
||||
|
||||
// Insert the nodes
|
||||
this.context.invoke('TextPlugin.pasteNodes', nodes, textOnly);
|
||||
this.context.invoke('HelperPlugin.normalize');
|
||||
this.context.invoke('editor.saveRange');
|
||||
|
||||
this.context.invoke('editor.afterCommand');
|
||||
},
|
||||
/**
|
||||
* Drop images into the editor: save them as attachments.
|
||||
*
|
||||
* @private
|
||||
* @param {File[]]} files (images only)
|
||||
*/
|
||||
_dropImages: function (files) {
|
||||
var self = this;
|
||||
this.context.invoke('editor.beforeCommand');
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
|
||||
var spinners = [];
|
||||
var images = [];
|
||||
var defs = [];
|
||||
_.each(files, function (file) {
|
||||
// Add spinner
|
||||
var spinner = $('<span class="fa fa-spinner fa-spin">').attr('data-filename', file.name)[0];
|
||||
self.context.invoke('editor.hidePopover');
|
||||
if (range.sc.tagName) {
|
||||
if (range.so >= dom.nodeLength(range.sc)) {
|
||||
$(range.sc).append(spinner);
|
||||
} else {
|
||||
$(range.sc).before(range.sc.childNodes[range.so]);
|
||||
}
|
||||
} else {
|
||||
range.sc.splitText(range.so);
|
||||
$(range.sc).after(spinner);
|
||||
}
|
||||
spinners.push(spinner);
|
||||
|
||||
// save images as attachments
|
||||
var def = $.Deferred();
|
||||
defs.push(def);
|
||||
// Get image's Base64 string
|
||||
var reader = new FileReader();
|
||||
reader.addEventListener('load', function (e) {
|
||||
self._uploadImage(e.target.result, file.name).then(function (attachment) {
|
||||
// Make the HTML
|
||||
var image = self.document.createElement('img');
|
||||
image.setAttribute('style', 'width: 100%;');
|
||||
image.src = '/web/content/' + attachment.id + '/' + attachment.name;
|
||||
image.alt = attachment.name;
|
||||
$(spinner).replaceWith(image);
|
||||
images.push(image);
|
||||
def.resolve(image);
|
||||
$(image).trigger('dropped');
|
||||
});
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
this.trigger_up('drop_images', {
|
||||
spinners: spinners,
|
||||
promises: defs,
|
||||
});
|
||||
|
||||
$.when.apply($, defs).then(function () {
|
||||
var defs = [];
|
||||
$(images).each(function () {
|
||||
if (!this.height) {
|
||||
var def = $.Deferred();
|
||||
defs.push(def);
|
||||
$(this).one('load error abort', def.resolve.bind(def));
|
||||
}
|
||||
});
|
||||
$.when.apply($, defs).then(function () {
|
||||
if (images.length === 1) {
|
||||
range = self.context.invoke('editor.setRange', _.last(images), 0);
|
||||
range.select();
|
||||
self.context.invoke('editor.saveRange');
|
||||
self.context.invoke('editor.afterCommand');
|
||||
self.context.invoke('MediaPlugin.updatePopoverAfterEdit', images[0]);
|
||||
} else {
|
||||
self.context.invoke('editor.afterCommand');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Upload an image from its Base64 representation.
|
||||
*
|
||||
* @private
|
||||
* @param {String} imageBase64
|
||||
* @param {String} fileName
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_uploadImage: function (imageBase64, fileName) {
|
||||
var options = {};
|
||||
this.trigger_up('getRecordInfo', {
|
||||
recordInfo: options,
|
||||
type: 'media',
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(options, recordInfo);
|
||||
},
|
||||
});
|
||||
|
||||
return this._rpc({
|
||||
route: '/web_editor/add_image_base64',
|
||||
params: {
|
||||
res_model: options.res_model,
|
||||
res_id: options.res_id,
|
||||
image_base64: imageBase64.split(';base64,').pop(),
|
||||
filename: fileName,
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {JQueryEvent} e
|
||||
*/
|
||||
_onDrop: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.options.disableDragAndDrop) {
|
||||
return;
|
||||
}
|
||||
var dataTransfer = e.originalEvent.dataTransfer;
|
||||
|
||||
if (!this._canDropHere()) {
|
||||
this.context.invoke('HelperPlugin.notify', _t("Not a dropzone"), _t("Dropping is prohibited in this area."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataTransfer.getData('text/html')) {
|
||||
this._dropHTML(dataTransfer.getData('text/html'));
|
||||
return;
|
||||
}
|
||||
if (dataTransfer.files.length) {
|
||||
var images = [];
|
||||
_.each(dataTransfer.files, function (file) {
|
||||
if (file.type.indexOf('image') !== -1) {
|
||||
images.push(file);
|
||||
}
|
||||
});
|
||||
if (!images.length || images.length < dataTransfer.files.length) {
|
||||
this.context.invoke('HelperPlugin.notify', _t("Unsupported file type"), _t("Images are the only file types that can be dropped."));
|
||||
}
|
||||
if (images.length) {
|
||||
this._dropImages(images);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Return true if dropping is allowed at the current range.
|
||||
*
|
||||
* @private
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_canDropHere: function () {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
return this.options.isEditableNode(range.sc);
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('dropzone', DropzonePlugin);
|
||||
|
||||
return DropzonePlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.editor', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
var TablePlugin = require('web_editor.wysiwyg.plugin.table');
|
||||
|
||||
|
||||
var NewSummernoteEditor = Plugins.editor.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public summernote module API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
this.history = this.context.modules.HistoryPlugin;
|
||||
this.dropzone = this.context.modules.DropzonePlugin;
|
||||
|
||||
this.insertTable = this.wrapCommand(this._insertTable.bind(this));
|
||||
this.insertOrderedList = this.wrapCommand(this._insertOrderedList.bind(this));
|
||||
this.insertUnorderedList = this.wrapCommand(this._insertUnorderedList.bind(this));
|
||||
this.insertCheckList = this.wrapCommand(this._insertCheckList.bind(this));
|
||||
this.indent = this.wrapCommand(this._indent.bind(this));
|
||||
this.outdent = this.wrapCommand(this._outdent.bind(this));
|
||||
this.table = new TablePlugin(this.context);
|
||||
this.hasFocus = this._hasFocus.bind(this);
|
||||
|
||||
this._makeTextCommand('formatBlock');
|
||||
this._makeTextCommand('removeFormat');
|
||||
_.each('bold,italic,underline,strikethrough,superscript,subscript'.split(','), function (sCmd) {
|
||||
self._makeTextCommand('formatText', sCmd);
|
||||
});
|
||||
_.each('justifyLeft,justifyCenter,justifyRight,justifyFull'.split(','), function (sCmd) {
|
||||
self._makeTextCommand('formatBlockStyle', sCmd);
|
||||
});
|
||||
|
||||
this._super();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hide all popovers.
|
||||
*/
|
||||
hidePopover: function () {
|
||||
this.context.invoke('MediaPlugin.hidePopovers');
|
||||
this.context.invoke('LinkPopover.hide');
|
||||
},
|
||||
/*
|
||||
* Focus the editor.
|
||||
*/
|
||||
focus: function () {
|
||||
// [workaround] Screen will move when page is scolled in IE.
|
||||
// - do focus when not focused
|
||||
if (!this.hasFocus()) {
|
||||
var range = $.summernote.range.create();
|
||||
if (range) {
|
||||
$(range.sc).closest('[contenteditable]').focus();
|
||||
range.select();
|
||||
} else {
|
||||
this.$editable.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Fix double-undo (CTRL-Z) issue with Odoo integration.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
undo: function () {
|
||||
this.createRange();
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* Set the range at the given nodes and offsets.
|
||||
* If no `ec` is specified, the range is collapsed on start.
|
||||
* Note: Does NOT select the range.
|
||||
*
|
||||
* @param {Node} sc
|
||||
* @param {Number} so
|
||||
* @param {Node} [ec]
|
||||
* @param {Number} [eo]
|
||||
* @returns {Object} range
|
||||
*/
|
||||
setRange: function (sc, so, ec, eo) {
|
||||
var range = this.createRange();
|
||||
range.sc = sc;
|
||||
range.so = so;
|
||||
range.ec = ec || sc;
|
||||
range.eo = eo || so;
|
||||
return range;
|
||||
},
|
||||
/**
|
||||
* Remove a link and preserve its text contents.
|
||||
*/
|
||||
unlink: function () {
|
||||
this.beforeCommand();
|
||||
this.context.invoke('LinkPlugin.unlink');
|
||||
this.afterCommand();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns true if editable area has focus.
|
||||
*
|
||||
* @private
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasFocus: function () {
|
||||
return this.$editable.is(':focus') || !!this.$editable.find('[contenteditable]:focus').length;
|
||||
},
|
||||
/**
|
||||
* Indent a list or a format node.
|
||||
*
|
||||
* @private
|
||||
* @returns {false|Node[]} contents of list/indented item
|
||||
*/
|
||||
_indent: function () {
|
||||
return this.context.invoke('BulletPlugin.indent');
|
||||
},
|
||||
/**
|
||||
* Insert a checklist.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_insertCheckList: function () {
|
||||
this.context.invoke('BulletPlugin.insertList', 'checklist');
|
||||
},
|
||||
/**
|
||||
* Insert an ordered list.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_insertOrderedList: function () {
|
||||
this.context.invoke('BulletPlugin.insertList', 'ol');
|
||||
},
|
||||
/**
|
||||
* Insert table (respecting unbreakable node rules).
|
||||
*
|
||||
* @private
|
||||
* @param {string} dim (eg: 3x3)
|
||||
*/
|
||||
_insertTable: function (dim) {
|
||||
this.context.invoke('TablePlugin.insertTable', dim);
|
||||
},
|
||||
/**
|
||||
* Insert an ordered list.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_insertUnorderedList: function () {
|
||||
this.context.invoke('BulletPlugin.insertList', 'ul');
|
||||
},
|
||||
/**
|
||||
* Adds a TextPlugin command to the editor object.
|
||||
* The command will exist in the editor object under the name
|
||||
* arg if specified, or commandName otherwise.
|
||||
*
|
||||
* @param {String} commandName
|
||||
* @param {String} arg
|
||||
*/
|
||||
_makeTextCommand: function (commandName, arg) {
|
||||
var self = this;
|
||||
this[arg || commandName] = self.wrapCommand(function (value) {
|
||||
self.context.invoke('TextPlugin.' + commandName, arg || value, value);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Outdent a list or a format node.
|
||||
*
|
||||
* @private
|
||||
* @returns {false|Node[]} contents of list/outdented item
|
||||
*/
|
||||
_outdent: function () {
|
||||
return this.context.invoke('BulletPlugin.outdent');
|
||||
},
|
||||
});
|
||||
|
||||
// Override Summernote default editor
|
||||
registry.add('editor', NewSummernoteEditor);
|
||||
|
||||
return NewSummernoteEditor;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,559 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.font', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
|
||||
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
|
||||
var wysiwygOptions = require('web_editor.wysiwyg.options');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Font (colorpicker & font-size)
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
dom.isFont = function (node) {
|
||||
return node && node.tagName === "FONT" || dom.isIcon(node);
|
||||
};
|
||||
|
||||
var FontPlugin = AbstractPlugin.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates both ColorPicker buttons (fore- and backcolors) containing their palettes.
|
||||
*
|
||||
* @returns {jQuery} the buttons' div.btn-group container
|
||||
*/
|
||||
createColorPickerButtons: function () {
|
||||
var self = this;
|
||||
var $container = $('<div class="note-btn-group btn-group note-color"/>');
|
||||
|
||||
// create and append the buttons
|
||||
|
||||
this._createForeColorButton().appendTo($container);
|
||||
this._createBackColorButton().appendTo($container);
|
||||
|
||||
// add event
|
||||
|
||||
$container.on('mousedown', function () {
|
||||
self.context.invoke('editor.saveRange');
|
||||
});
|
||||
return $container;
|
||||
},
|
||||
/**
|
||||
* Takes the font size button created by summernote and override its
|
||||
* events so as to customize its behavior.
|
||||
*
|
||||
* @param {jQuery} $button the button, as created by summernote
|
||||
* @returns {jQuery} the overridden button
|
||||
*/
|
||||
overrideFontSizeButton: function ($button) {
|
||||
var self = this;
|
||||
$button.click(function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
$button.find('.dropdown-menu').off('click').on('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
self.context.invoke('editor.createRange');
|
||||
self.context.invoke('editor.beforeCommand');
|
||||
var $target = $(e.target);
|
||||
self.context.invoke('FontPlugin.changeFontSize', $target.closest('[data-value]').data('value'), $target);
|
||||
self.context.invoke('buttons.updateCurrentStyle');
|
||||
self.context.invoke('editor.saveRange');
|
||||
self.context.invoke('editor.afterCommand');
|
||||
});
|
||||
return $button;
|
||||
},
|
||||
/**
|
||||
* Creates a (fore- or back-) color palette.
|
||||
*
|
||||
* @param {string('foreColor'|'backColor')} eventName
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
createPalette: function (eventName) {
|
||||
var self = this;
|
||||
var colors = _.clone(this.options.colors);
|
||||
colors.splice(0, 1); // Ignore the summernote gray palette and use ours
|
||||
var $palette = $(QWeb.render('wysiwyg.plugin.font.colorPalette', {
|
||||
colors: colors,
|
||||
eventName: eventName,
|
||||
lang: this.lang,
|
||||
}));
|
||||
if (this.options.tooltip) {
|
||||
$palette.find('.note-color-btn').tooltip({
|
||||
container: this.options.container,
|
||||
trigger: 'hover',
|
||||
placement: 'bottom'
|
||||
});
|
||||
}
|
||||
|
||||
// custom structure for the color picker and custom colorsin XML template
|
||||
var $clpicker = $(QWeb.render('web_editor.colorpicker'));
|
||||
|
||||
var $buttons = $(_.map($clpicker.children(), function (group) {
|
||||
var $contents = $(group).contents();
|
||||
if (!$contents.length) {
|
||||
return '';
|
||||
}
|
||||
var $row = $("<div/>", {
|
||||
"class": "note-color-row mb8 clearfix",
|
||||
'data-group': $(group).data('name'),
|
||||
}).append($contents);
|
||||
var $after_breaks = $row.find(".o_small + :not(.o_small)");
|
||||
if ($after_breaks.length === 0) {
|
||||
$after_breaks = $row.find(":nth-child(8n+9)");
|
||||
}
|
||||
$after_breaks.addClass("o_clear");
|
||||
return $row[0].outerHTML;
|
||||
}).join(""));
|
||||
|
||||
$buttons.find('button').each(function () {
|
||||
var color = $(this).data('color');
|
||||
$(this).addClass('note-color-btn bg-' + color).attr('data-value', (eventName === 'backColor' ? 'bg-' : 'text-') + color);
|
||||
});
|
||||
|
||||
$palette.find('.o_theme_color_placeholder').prepend($buttons.filter('[data-group="theme"]'));
|
||||
$palette.find('.o_transparent_color_placeholder').prepend($buttons.filter('[data-group="transparent_grayscale"]'));
|
||||
$palette.find('.o_common_color_placeholder').prepend($buttons.filter('[data-group="common"]'));
|
||||
|
||||
$palette.off('click').on('mousedown', '.note-color-btn', function (e) {
|
||||
e.preventDefault();
|
||||
self.context.invoke('editor.createRange');
|
||||
self.context.invoke('editor.beforeCommand');
|
||||
var method = eventName === 'backColor' ? 'changeBgColor' : 'changeForeColor';
|
||||
var $target = $(e.target);
|
||||
self.context.invoke('FontPlugin.' + method, $target.closest('[data-value]').data('value'), $target);
|
||||
self.context.invoke('buttons.updateCurrentStyle');
|
||||
self.editable.normalize();
|
||||
self.context.invoke('editor.saveRange');
|
||||
self.context.invoke('editor.afterCommand');
|
||||
});
|
||||
$palette.on('mousedown', '.note-custom-color', this._onCustomColor.bind(this, eventName));
|
||||
|
||||
return $palette;
|
||||
},
|
||||
/**
|
||||
* Change the selection's fore color.
|
||||
*
|
||||
* @param {string} color (hexadecimal or class name)
|
||||
*/
|
||||
changeForeColor: function (color) {
|
||||
this._applyFont(color || 'text-undefined', null, null);
|
||||
},
|
||||
/**
|
||||
* Change the selection's background color.
|
||||
*
|
||||
* @param {string} color (hexadecimal or class name)
|
||||
*/
|
||||
changeBgColor: function (color) {
|
||||
this._applyFont(null, color || 'bg-undefined', null);
|
||||
},
|
||||
/**
|
||||
* Change the selection's font size.
|
||||
*
|
||||
* @param {integer} fontsize
|
||||
*/
|
||||
changeFontSize: function (fontsize) {
|
||||
this._applyFont(null, null, fontsize || 'inherit');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Applies the given styles (fore- or backcolor, font size)
|
||||
* to a given <font> node.
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node
|
||||
* @param {string} color (hexadecimal or class name)
|
||||
* @param {string} bgcolor (hexadecimal or class name)
|
||||
* @param {integer} size
|
||||
* @returns {Node} the <font> node
|
||||
*/
|
||||
_applyStylesToFontNode: function (node, color, bgcolor, size) {
|
||||
var className = node.className.split(this.context.invoke('HelperPlugin.getRegex', 'space'));
|
||||
var k;
|
||||
if (color) {
|
||||
for (k = 0; k < className.length; k++) {
|
||||
if (className[k].length && className[k].slice(0, 5) === "text-") {
|
||||
className.splice(k, 1);
|
||||
k--;
|
||||
}
|
||||
}
|
||||
if (color === 'text-undefined') {
|
||||
node.className = className.join(" ");
|
||||
node.style.color = "inherit";
|
||||
} else if (color.indexOf('text-') !== -1) {
|
||||
node.className = className.join(" ") + " " + color;
|
||||
node.style.color = "inherit";
|
||||
} else {
|
||||
node.className = className.join(" ");
|
||||
node.style.color = color;
|
||||
}
|
||||
}
|
||||
if (bgcolor) {
|
||||
for (k = 0; k < className.length; k++) {
|
||||
if (className[k].length && className[k].slice(0, 3) === "bg-") {
|
||||
className.splice(k, 1);
|
||||
k--;
|
||||
}
|
||||
}
|
||||
|
||||
if (bgcolor === 'bg-undefined') {
|
||||
node.className = className.join(" ");
|
||||
node.style.backgroundColor = "inherit";
|
||||
} else if (bgcolor.indexOf('bg-') !== -1) {
|
||||
node.className = className.join(" ") + " " + bgcolor;
|
||||
node.style.backgroundColor = "inherit";
|
||||
} else {
|
||||
node.className = className.join(" ");
|
||||
node.style.backgroundColor = bgcolor;
|
||||
}
|
||||
}
|
||||
if (size) {
|
||||
node.style.fontSize = "inherit";
|
||||
if (!isNaN(size) && Math.abs(parseInt(this.window.getComputedStyle(node).fontSize, 10) - size) / size > 0.05) {
|
||||
node.style.fontSize = size + "px";
|
||||
}
|
||||
}
|
||||
return node;
|
||||
},
|
||||
/**
|
||||
* Applies the given styles (fore- or backcolor, font size) to the selection.
|
||||
* If no text is selected, apply to the current text node, if any.
|
||||
*
|
||||
* @private
|
||||
* @param {string} color (hexadecimal or class name)
|
||||
* @param {string} bgcolor (hexadecimal or class name)
|
||||
* @param {integer} fontsize
|
||||
*/
|
||||
_applyFont: function (color, bgcolor, size) {
|
||||
var self = this;
|
||||
var r = this.context.invoke('editor.createRange');
|
||||
if (!r || !this.$editable.has(r.sc).length || !this.$editable.has(r.ec).length) {
|
||||
return;
|
||||
}
|
||||
var target;
|
||||
var font;
|
||||
if (r.isCollapsed()) {
|
||||
if (dom.isIcon(r.sc)) {
|
||||
target = dom.lastAncestor(r.sc, dom.isIcon);
|
||||
} else {
|
||||
target = this.context.invoke('editor.restoreTarget');
|
||||
if (target && dom.isIcon(target)) {
|
||||
r = this.context.invoke('editor.setRange', target, 0);
|
||||
} else if (dom.isText(r.sc)) {
|
||||
font = dom.create("font");
|
||||
font.appendChild(this.document.createTextNode('\u200B'));
|
||||
|
||||
var fontParent = dom.ancestor(r.sc, function (n) {
|
||||
return n.tagName === 'FONT';
|
||||
});
|
||||
var right;
|
||||
if (fontParent) {
|
||||
right = this.context.invoke('HelperPlugin.splitTree', fontParent, r.getStartPoint());
|
||||
} else {
|
||||
right = r.sc.splitText(r.so);
|
||||
}
|
||||
$(right).before(font);
|
||||
font = this._applyStylesToFontNode(font, color, bgcolor, size);
|
||||
r = this.context.invoke('editor.setRange', font, 1);
|
||||
r.select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startPoint = r.getStartPoint();
|
||||
var endPoint = r.getEndPoint();
|
||||
if (startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset]) {
|
||||
startPoint.node = startPoint.node.childNodes[startPoint.offset];
|
||||
startPoint.offset = 0;
|
||||
}
|
||||
if (endPoint.node.tagName && endPoint.node.childNodes[endPoint.offset]) {
|
||||
endPoint.node = endPoint.node.childNodes[endPoint.offset];
|
||||
endPoint.offset = 0;
|
||||
}
|
||||
// get first and last point
|
||||
var ancestor;
|
||||
var node;
|
||||
if (!r.isCollapsed()) {
|
||||
if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
|
||||
ancestor = dom.lastAncestor(endPoint.node, dom.isFont) || endPoint.node;
|
||||
dom.splitTree(ancestor, endPoint);
|
||||
}
|
||||
if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
|
||||
ancestor = dom.lastAncestor(startPoint.node, dom.isFont) || startPoint.node;
|
||||
node = dom.splitTree(ancestor, startPoint);
|
||||
if (endPoint.node === startPoint.node) {
|
||||
endPoint.node = node;
|
||||
endPoint.offset = dom.nodeLength(node);
|
||||
}
|
||||
startPoint.node = node;
|
||||
startPoint.offset = 0;
|
||||
}
|
||||
}
|
||||
// get list of nodes to change
|
||||
var nodes = [];
|
||||
dom.walkPoint(startPoint, endPoint, function (point) {
|
||||
var node = point.node;
|
||||
if (((dom.isText(node) && self.context.invoke('HelperPlugin.isVisibleText', node)) || dom.isIcon(node)) &&
|
||||
(node !== endPoint.node || endPoint.offset)) {
|
||||
nodes.push(point.node);
|
||||
}
|
||||
});
|
||||
nodes = _.unique(nodes);
|
||||
// if fontawesome
|
||||
if (r.isCollapsed()) {
|
||||
nodes.push(startPoint.node);
|
||||
}
|
||||
|
||||
// apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
|
||||
var $font;
|
||||
var fonts = [];
|
||||
var style;
|
||||
var className;
|
||||
var i;
|
||||
if (color || bgcolor || size) {
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
font = dom.lastAncestor(node, dom.isFont);
|
||||
if (!font) {
|
||||
if (node.textContent.match(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace'))) {
|
||||
node.textContent = node.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace', 'g'), '\u00A0');
|
||||
}
|
||||
font = dom.create("font");
|
||||
node.parentNode.insertBefore(font, node);
|
||||
font.appendChild(node);
|
||||
}
|
||||
fonts.push(font);
|
||||
this._applyStylesToFontNode(font, color, bgcolor, size);
|
||||
}
|
||||
}
|
||||
// remove empty values
|
||||
// we must remove the value in 2 steps (applay inherit then remove) because some
|
||||
// browser like chrome have some time an error for the rendering and/or keep inherit
|
||||
for (i = 0; i < fonts.length; i++) {
|
||||
font = fonts[i];
|
||||
if (font.style.color === "inherit") {
|
||||
font.style.color = "";
|
||||
}
|
||||
if (font.style.backgroundColor === "inherit") {
|
||||
font.style.backgroundColor = "";
|
||||
}
|
||||
if (font.style.fontSize === "inherit") {
|
||||
font.style.fontSize = "";
|
||||
}
|
||||
$font = $(font);
|
||||
if (font.style.color === '' && font.style.backgroundColor === '' && font.style.fontSize === '') {
|
||||
$font.removeAttr("style");
|
||||
}
|
||||
if (!font.className.length) {
|
||||
$font.removeAttr("class");
|
||||
}
|
||||
}
|
||||
|
||||
// target the deepest node
|
||||
if (startPoint.node.tagName && !startPoint.offset) {
|
||||
startPoint.node = this.context.invoke('HelperPlugin.firstLeaf', startPoint.node.childNodes[startPoint.offset] || startPoint.node);
|
||||
startPoint.offset = 0;
|
||||
}
|
||||
if (endPoint.node.tagName && !endPoint.offset) {
|
||||
endPoint.node = this.context.invoke('HelperPlugin.firstLeaf', endPoint.node.childNodes[endPoint.offset] || endPoint.node);
|
||||
endPoint.offset = 0;
|
||||
}
|
||||
|
||||
// select nodes to clean (to remove empty font and merge same nodes)
|
||||
nodes = [];
|
||||
dom.walkPoint(startPoint, endPoint, function (point) {
|
||||
nodes.push(point.node);
|
||||
});
|
||||
nodes = _.unique(nodes);
|
||||
// remove node without attributes (move content), and merge the same nodes
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
if (dom.isText(node) && !this.context.invoke('HelperPlugin.isVisibleText', node)) {
|
||||
continue;
|
||||
}
|
||||
font = dom.lastAncestor(node, dom.isFont);
|
||||
node = font || dom.ancestor(node, dom.isSpan);
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
$font = $(node);
|
||||
className = this.context.invoke('HelperPlugin.orderClass', node);
|
||||
style = this.context.invoke('HelperPlugin.orderStyle', node);
|
||||
if (!className && !style) {
|
||||
$(node).before($(node).contents());
|
||||
if (endPoint.node === node) {
|
||||
endPoint = dom.prevPointUntil(endPoint, function (point) {
|
||||
return point.node !== node;
|
||||
});
|
||||
}
|
||||
$(node).remove();
|
||||
|
||||
nodes.splice(i, 1);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
var prev = font && font.previousSibling;
|
||||
while (prev && !font.tagName && !this.context.invoke('HelperPlugin.isVisibleText', prev)) {
|
||||
prev = prev.previousSibling;
|
||||
}
|
||||
if (prev &&
|
||||
font.tagName === prev.tagName &&
|
||||
className === this.context.invoke('HelperPlugin.orderClass', prev) && style === this.context.invoke('HelperPlugin.orderStyle', prev)) {
|
||||
$(prev).append($(font).contents());
|
||||
if (endPoint.node === font) {
|
||||
endPoint = dom.prevPointUntil(endPoint, function (point) {
|
||||
return point.node !== font;
|
||||
});
|
||||
}
|
||||
$(font).remove();
|
||||
|
||||
nodes.splice(i, 1);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// restore selection
|
||||
r = this.context.invoke('editor.setRange', startPoint.node, startPoint.offset, endPoint.node, endPoint.offset);
|
||||
r.normalize().select();
|
||||
|
||||
if (target) {
|
||||
this.context.invoke('MediaPlugin.updatePopoverAfterEdit', target);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Creates the backcolor button containing its palette.
|
||||
*
|
||||
* @private
|
||||
* @returns {jQuery} the backcolor button
|
||||
*/
|
||||
_createBackColorButton: function () {
|
||||
var $bgPalette = this.createPalette('backColor');
|
||||
$bgPalette.find("button:not(.note-color-btn)")
|
||||
.addClass("note-color-btn")
|
||||
.attr('data-event', 'backColor')
|
||||
.each(function () {
|
||||
var $el = $(this);
|
||||
var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color');
|
||||
$el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className);
|
||||
});
|
||||
|
||||
var $bgContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
|
||||
className: 'note-bg-color',
|
||||
icon: this.options.icons.bg,
|
||||
}));
|
||||
$bgContainer.find('.dropdown-menu').append($bgPalette);
|
||||
return $bgContainer;
|
||||
},
|
||||
/**
|
||||
* Creates the forecolor button containing its palette.
|
||||
*
|
||||
* @private
|
||||
* @returns {jQuery} the forecolor button
|
||||
*/
|
||||
_createForeColorButton: function () {
|
||||
var $forePalette = this.createPalette('foreColor');
|
||||
$forePalette.find("button:not(.note-color-btn)")
|
||||
.addClass("note-color-btn")
|
||||
.attr('data-event', 'foreColor')
|
||||
.each(function () {
|
||||
var $el = $(this);
|
||||
var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color');
|
||||
$el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color'));
|
||||
});
|
||||
|
||||
var $foreContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
|
||||
className: 'note-fore-color',
|
||||
icon: this.options.icons.fore,
|
||||
}));
|
||||
$foreContainer.find('.dropdown-menu').append($forePalette);
|
||||
return $foreContainer;
|
||||
},
|
||||
/**
|
||||
* Method called on custom color button click :
|
||||
* opens the color picker dialog and saves the chosen color on save.
|
||||
*
|
||||
* @private
|
||||
* @param {string} targetColor
|
||||
* @param {jQuery Event} ev
|
||||
*/
|
||||
_onCustomColor: function (targetColor, ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
var self = this;
|
||||
var $button = $(ev.target).next('button');
|
||||
var target = this.context.invoke('editor.restoreTarget');
|
||||
var colorPickerDialog = new ColorpickerDialog(this, {});
|
||||
|
||||
this.context.invoke('editor.saveRange');
|
||||
colorPickerDialog.on('colorpicker:saved', this, this._wrapCommand(function (ev) {
|
||||
$button = $button.clone().appendTo($button.parent());
|
||||
$button.show();
|
||||
$button.css('background-color', ev.data.cssColor);
|
||||
$button.attr('data-value', ev.data.cssColor);
|
||||
$button.data('value', ev.data.cssColor);
|
||||
$button.attr('title', ev.data.cssColor);
|
||||
self.context.invoke('editor.saveTarget', target);
|
||||
self.context.invoke('editor.restoreRange');
|
||||
$button.mousedown();
|
||||
}));
|
||||
colorPickerDialog.open();
|
||||
this.context.invoke('MediaPlugin.hidePopovers');
|
||||
},
|
||||
});
|
||||
|
||||
_.extend(wysiwygOptions.icons, {
|
||||
fore: 'fa fa-font',
|
||||
bg: 'fa fa-paint-brush',
|
||||
});
|
||||
_.extend(wysiwygTranslation.color, {
|
||||
customColor: _t('Custom color'),
|
||||
fore: _t('Color'),
|
||||
bg: _t('Background color'),
|
||||
});
|
||||
|
||||
registry.add('FontPlugin', FontPlugin);
|
||||
|
||||
registry.addXmlDependency('/web_editor/static/src/xml/wysiwyg_colorpicker.xml');
|
||||
registry.addJob(function (wysiwyg) {
|
||||
if ('web_editor.colorpicker' in QWeb.templates) {
|
||||
return;
|
||||
}
|
||||
var options = {};
|
||||
wysiwyg.trigger_up('getRecordInfo', {
|
||||
recordInfo: options,
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(options, recordInfo);
|
||||
},
|
||||
});
|
||||
return wysiwyg._rpc({
|
||||
model: 'ir.ui.view',
|
||||
method: 'read_template',
|
||||
args: ['web_editor.colorpicker'],
|
||||
kwargs: {
|
||||
context: options.context,
|
||||
},
|
||||
}).then(function (template) {
|
||||
QWeb.add_template(template);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return FontPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.font.buttons', function (require) {
|
||||
'use strict';
|
||||
|
||||
var ButtonsPlugin = require('web_editor.wysiwyg.plugin.buttons');
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// override the ColorPicker button into the Toolbar and the font size button
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
ButtonsPlugin.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
addToolbarButtons: function () {
|
||||
var self = this;
|
||||
this._super();
|
||||
|
||||
this.context.memo('button.colorpicker', function () {
|
||||
return self.context.invoke('FontPlugin.createColorPickerButtons');
|
||||
});
|
||||
|
||||
var fontsizeFunction = this.context.memo('button.fontsize');
|
||||
this.context.memo('button.fontsize', function () {
|
||||
return self.context.invoke('FontPlugin.overrideFontSizeButton', fontsizeFunction());
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return ButtonsPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.HelpDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
*/
|
||||
var HelpDialog = Plugins.helpDialog.extend({
|
||||
/**
|
||||
* Restore the hidden close button.
|
||||
*/
|
||||
showHelpDialog: function () {
|
||||
var self = this;
|
||||
return this._super().then(function () {
|
||||
self.$dialog.find('button.close span').attr('aria-hidden', 'false');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('helpDialog', HelpDialog);
|
||||
|
||||
return {
|
||||
HelpDialog: HelpDialog,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.hint', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
|
||||
var HintPlugin = Plugins.hintPopover.extend({
|
||||
init: function (context) {
|
||||
context.options.hint = (context.options.hint || []).concat(this._hints());
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public summernote module API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Replace the current hint.
|
||||
*/
|
||||
replace: function () {
|
||||
var self = this;
|
||||
var $item = this.$content.find('.note-hint-item.active');
|
||||
if ($item.length) {
|
||||
this.lastWordRange.select();
|
||||
this.context.invoke('HelperPlugin.deleteSelection');
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
|
||||
this.nodeFromItem($item).each(function () {
|
||||
$(range.sc).after(this);
|
||||
range = self.context.invoke('editor.setRange', this, dom.nodeLength(this));
|
||||
});
|
||||
range.select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
this.lastWordRange = null;
|
||||
this.hide();
|
||||
this.context.triggerEvent('change', this.$editable.html(), this.$editable[0]);
|
||||
this.context.invoke('editor.focus');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {JQueryEvent} e
|
||||
*/
|
||||
handleKeyup: function (e) {
|
||||
var self = this;
|
||||
if ([13, 38, 40].indexOf(e.keyCode) === -1) { // enter, up, down
|
||||
var wordRange = this.context.invoke('editor.createRange');
|
||||
var keyword_1 = wordRange.sc.textContent.slice(0, wordRange.so);
|
||||
if (this.hints.length && keyword_1) {
|
||||
this.$content.empty();
|
||||
this.$popover.hide();
|
||||
this.lastWordRange = wordRange;
|
||||
var hasMatch = false;
|
||||
|
||||
// test all hints
|
||||
this.hints.forEach(function (hint, idx) {
|
||||
var match = keyword_1.match(hint.match);
|
||||
if (match) {
|
||||
hasMatch = true;
|
||||
wordRange.so = wordRange.eo - match[0].length;
|
||||
self.createGroup(idx, match[0]).appendTo(self.$content);
|
||||
}
|
||||
});
|
||||
if (!hasMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// select first .note-hint-item
|
||||
this.$content.find('.note-hint-item:first').addClass('active');
|
||||
|
||||
// set position for popover after group is created
|
||||
var rect = wordRange.getClientRects()[0];
|
||||
var bnd = {
|
||||
top: rect.top + $(this.document).scrollTop(),
|
||||
left: rect.left + $(this.document).scrollLeft(),
|
||||
};
|
||||
this.$popover.css({
|
||||
left: bnd.left,
|
||||
top: bnd.top + (this.direction === 'top' ? -this.$popover.outerHeight() : (rect.bottom - rect.top)) - 5,
|
||||
});
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get hint objects.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object[]} hints
|
||||
*/
|
||||
_hints: function () {
|
||||
var self = this;
|
||||
return [{
|
||||
className: 'o_hint_partner',
|
||||
match: /\B@(\w+(\s\w*)?)$/,
|
||||
search: function (keyword, callback) {
|
||||
self._rpc({
|
||||
model: 'res.partner',
|
||||
method: "search_read",
|
||||
fields: ['id', 'name', 'email'],
|
||||
domain: [
|
||||
['name', 'ilike', keyword],
|
||||
],
|
||||
limit: 10,
|
||||
}).then(callback);
|
||||
},
|
||||
template: function (partner) {
|
||||
return partner.name + (partner.email ? ' <i style="color: #999;">(' + partner.email + ')</i>' : '');
|
||||
},
|
||||
content: function (item) {
|
||||
return $(self.document.createTextNode('@' + item.name + '\u00A0'));
|
||||
},
|
||||
},
|
||||
{
|
||||
className: 'fa',
|
||||
match: /:([\-+\w]+)$/,
|
||||
search: function () {},
|
||||
template: function () {
|
||||
return '<span class="fa fa-star">\u200B</span>';
|
||||
},
|
||||
content: function () {}
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('hintPopover', null);
|
||||
|
||||
return HintPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.history', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
|
||||
var HistoryPlugin = Plugins.history.extend({
|
||||
/**
|
||||
* Apply a snapshot.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
applySnapshot: function () {
|
||||
this.context.invoke('MediaPlugin.hidePopovers');
|
||||
try {
|
||||
this._super.apply(this, arguments);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.context.invoke('editor.focus');
|
||||
},
|
||||
/**
|
||||
* Clear the history.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
clear: function () {
|
||||
this.stack = [];
|
||||
this.stackOffset = -1;
|
||||
this.recordUndo();
|
||||
},
|
||||
/**
|
||||
* Get the current history stack and the current offset.
|
||||
*
|
||||
* @returns {Object} {stack: Object, stackOffset: Integer}
|
||||
*/
|
||||
getHistoryStep: function () {
|
||||
return {
|
||||
stack: this.stack,
|
||||
stackOffset: this.stackOffset,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Prevent errors with first snapshot.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
makeSnapshot: function () {
|
||||
var rng = $.summernote.range.create(this.editable);
|
||||
var snapshot = this._super();
|
||||
if (rng.sc === this.editable || $(rng.sc).has(this.editable).length) {
|
||||
snapshot.bookmark.s.path = snapshot.bookmark.e.path = [0];
|
||||
snapshot.bookmark.s.offset = snapshot.bookmark.e.offset = 0;
|
||||
}
|
||||
return snapshot;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
recordUndo: function () {
|
||||
if (!this.stack[this.stackOffset] || this.$editable.html() !== this.stack[this.stackOffset].contents) {
|
||||
this._super();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
undo: function () {
|
||||
if (this.stackOffset > 0) {
|
||||
this.stackOffset--;
|
||||
}
|
||||
this.applySnapshot(this.stack[this.stackOffset]);
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('HistoryPlugin', HistoryPlugin);
|
||||
|
||||
return HistoryPlugin;
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.link', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var LinkDialog = require('wysiwyg.widgets.LinkDialog');
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var _t = core._t;
|
||||
var dom = $.summernote.dom;
|
||||
dom.isAnchor = function (node) {
|
||||
return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
|
||||
!$(node).hasClass('fa') && !$(node).hasClass('o_image');
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// link
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
var LinkPlugin = Plugins.linkDialog.extend({
|
||||
events: {
|
||||
'dblclick .note-editable': '_onDblclick',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public summernote module API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} linkInfo
|
||||
* @returns {Promise}
|
||||
*/
|
||||
showLinkDialog: function (linkInfo) {
|
||||
var self = this;
|
||||
this.context.invoke('editor.hidePopover');
|
||||
var media = this.context.invoke('editor.restoreTarget');
|
||||
|
||||
if (linkInfo.range) {
|
||||
var r = linkInfo.range.getPoints();
|
||||
|
||||
// move carret in icon, video...
|
||||
if (!media && (dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon))) {
|
||||
media = dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon);
|
||||
}
|
||||
// if select a text content in anchor then click on image then click on anchor button
|
||||
if (dom.isImg(media) || dom.isIcon(media)) {
|
||||
r.sc = media.parentNode;
|
||||
r.so = [].indexOf.call(media.parentNode.childNodes, media);
|
||||
r.ec = media;
|
||||
r.eo = dom.nodeLength(media);
|
||||
|
||||
linkInfo.range.sc = r.sc;
|
||||
linkInfo.range.so = r.so;
|
||||
linkInfo.range.ec = r.ec;
|
||||
linkInfo.range.eo = r.eo;
|
||||
}
|
||||
|
||||
linkInfo.isAnchor = linkInfo.range.isOnAnchor();
|
||||
linkInfo.className = linkInfo.isAnchor ? dom.ancestor(r.sc, dom.isAnchor).className : '';
|
||||
linkInfo.url = linkInfo.isAnchor ? ($(dom.ancestor(r.sc, dom.isAnchor)).attr('href') || '').replace(window.location.origin, '') : '';
|
||||
|
||||
var nodes = [];
|
||||
if (linkInfo.isAnchor) {
|
||||
nodes = dom.ancestor(r.sc, dom.isAnchor).childNodes;
|
||||
} else if (!linkInfo.range.isCollapsed()) {
|
||||
if (dom.isImg(media) || dom.isIcon(media)) {
|
||||
nodes.push(media);
|
||||
} else {
|
||||
if (r.sc.tagName) {
|
||||
r.sc = (r.so ? r.sc.childNodes[r.so] : r.sc).firstChild || r.sc;
|
||||
r.so = 0;
|
||||
} else if (r.so !== r.sc.textContent.length) {
|
||||
if (r.sc === r.ec) {
|
||||
r.ec = r.sc = r.sc.splitText(r.so);
|
||||
r.eo -= r.so;
|
||||
} else {
|
||||
r.sc = r.sc.splitText(r.so);
|
||||
}
|
||||
r.so = 0;
|
||||
}
|
||||
if (r.ec.tagName) {
|
||||
r.ec = (r.eo ? r.ec.childNodes[r.eo - 1] : r.ec).lastChild || r.ec;
|
||||
r.eo = r.ec.textContent.length;
|
||||
} else if (r.eo !== r.ec.textContent.length) {
|
||||
r.ec.splitText(r.eo);
|
||||
}
|
||||
|
||||
// browsers can't target a picture or void node
|
||||
if (dom.isVoid(r.sc) || dom.isImg(r.sc)) {
|
||||
r.so = dom.listPrev(r.sc).length - 1;
|
||||
r.sc = r.sc.parentNode;
|
||||
}
|
||||
if (dom.isBR(r.ec)) {
|
||||
r.eo = dom.listPrev(r.ec).length - 1;
|
||||
r.ec = r.ec.parentNode;
|
||||
} else if (dom.isVoid(r.ec) || dom.isImg(r.sc)) {
|
||||
r.eo = dom.listPrev(r.ec).length;
|
||||
r.ec = r.ec.parentNode;
|
||||
}
|
||||
linkInfo.range.sc = r.sc;
|
||||
linkInfo.range.so = r.so;
|
||||
linkInfo.range.ec = r.ec;
|
||||
linkInfo.range.eo = r.eo;
|
||||
linkInfo.range.select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
linkInfo.range = this.context.invoke('editor.createRange');
|
||||
|
||||
// search nodes to insert in the anchor
|
||||
|
||||
var startPoint = this.context.invoke('HelperPlugin.makePoint', r.sc, r.so);
|
||||
var endPoint = this.context.invoke('HelperPlugin.makePoint', r.ec, r.eo);
|
||||
dom.walkPoint(startPoint, endPoint, function (point) {
|
||||
var node = point.node.childNodes && point.node.childNodes[point.offset] || point.node;
|
||||
nodes.push(node);
|
||||
});
|
||||
|
||||
nodes = _.filter(_.uniq(nodes), function (node) {
|
||||
return nodes.indexOf(node.parentNode) === -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
var text = "";
|
||||
linkInfo.images = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (dom.ancestor(nodes[i], dom.isImg)) {
|
||||
text += dom.ancestor(nodes[i], dom.isImg).outerHTML;
|
||||
} else if (dom.ancestor(nodes[i], dom.isIcon)) {
|
||||
text += dom.ancestor(nodes[i], dom.isIcon).outerHTML;
|
||||
} else if (!linkInfo.isAnchor && nodes[i].nodeType === 1) {
|
||||
// just use text nodes from listBetween
|
||||
} else if (!linkInfo.isAnchor && i === 0) {
|
||||
text += nodes[i].textContent;
|
||||
} else if (!linkInfo.isAnchor && i === nodes.length - 1) {
|
||||
text += nodes[i].textContent;
|
||||
} else {
|
||||
text += nodes[i].textContent;
|
||||
}
|
||||
}
|
||||
linkInfo.text = text.replace(this.context.invoke('HelperPlugin.getRegex', 'space', 'g'), ' ');
|
||||
}
|
||||
|
||||
linkInfo.needLabel = !linkInfo.text.length;
|
||||
}
|
||||
|
||||
var def = $.Deferred();
|
||||
var linkDialog = new LinkDialog(this.options.parent, {
|
||||
onClose: function () {
|
||||
setTimeout(function () {
|
||||
self.context.invoke('editor.focus');
|
||||
});
|
||||
}
|
||||
},
|
||||
_.omit(linkInfo, 'range')
|
||||
);
|
||||
|
||||
linkDialog.on('save', this, this._wrapCommand(function (newLinkInfo) {
|
||||
var isCollapsed = linkInfo.range.isCollapsed();
|
||||
linkInfo.range.select();
|
||||
var $anchor;
|
||||
if (linkInfo.isAnchor) {
|
||||
$anchor = $(dom.ancestor(r.sc, dom.isAnchor));
|
||||
$anchor.css(newLinkInfo.style || {});
|
||||
if (newLinkInfo.isNewWindow) {
|
||||
$anchor.attr('target', '_blank');
|
||||
} else {
|
||||
$anchor.removeAttr('target');
|
||||
}
|
||||
} else {
|
||||
self.context.invoke('editor.saveRange');
|
||||
def.resolve(_.clone(newLinkInfo));
|
||||
var range = self.context.invoke('editor.createRange');
|
||||
var anchor = dom.ancestor(range.sc.childNodes[range.so] || range.sc, dom.isAnchor);
|
||||
$anchor = $(anchor);
|
||||
if (isCollapsed) {
|
||||
// move the range just after the link
|
||||
var point = dom.nextPoint({
|
||||
node: anchor,
|
||||
offset: dom.nodeLength(anchor),
|
||||
});
|
||||
range = self.context.invoke('editor.setRange', point.node, point.offset);
|
||||
range.select();
|
||||
} else {
|
||||
$anchor.selectContent();
|
||||
}
|
||||
}
|
||||
if ((dom.isImg(media) || dom.isIcon(media)) && !$anchor.find(media).length) {
|
||||
$(media).remove();
|
||||
}
|
||||
$anchor.attr('class', newLinkInfo.className);
|
||||
$anchor.attr('href', newLinkInfo.url);
|
||||
self.context.invoke('editor.saveRange');
|
||||
self.context.invoke('editor.saveTarget', $anchor[0]);
|
||||
self.context.triggerEvent('focusnode', $anchor[0]);
|
||||
}));
|
||||
linkDialog.on('closed', this, function () {
|
||||
def.reject();
|
||||
this.context.invoke('editor.restoreRange');
|
||||
this.context.invoke('LinkPopover.update');
|
||||
});
|
||||
|
||||
linkDialog.open();
|
||||
return def.promise();
|
||||
},
|
||||
/**
|
||||
* Remove the current link, keep its contents.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
unlink: function () {
|
||||
var rng = this.context.invoke('editor.createRange');
|
||||
var anchor = rng.sc;
|
||||
while (anchor && anchor.tagName !== 'A') {
|
||||
anchor = anchor.parentElement;
|
||||
}
|
||||
if (!anchor) {
|
||||
this.context.invoke('editor.hidePopover');
|
||||
return;
|
||||
}
|
||||
var startAndEndInvisible = this.context.invoke('HelperPlugin.getRegex', 'startAndEndInvisible', 'g');
|
||||
anchor.innerHTML = anchor.innerHTML.replace(startAndEndInvisible, '');
|
||||
var $contents = $(anchor).contents();
|
||||
$(anchor).before($contents).remove();
|
||||
|
||||
this.context.invoke('editor.hidePopover');
|
||||
|
||||
var start = $contents[0];
|
||||
var end = $contents.last()[0];
|
||||
rng = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
|
||||
rng.select();
|
||||
this.editable.normalize();
|
||||
this.context.invoke('editor.saveRange');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_addButtons: function () {
|
||||
var self = this;
|
||||
this._super();
|
||||
|
||||
this.context.memo('help.LinkPlugin.show', this.options.langInfo.help['linkDialog.show']);
|
||||
|
||||
this.context.memo('button.linkPlugin', function () {
|
||||
return self.context.invoke('buttons.button', {
|
||||
contents: self.ui.icon(self.options.icons.link),
|
||||
tooltip: self.lang.link.link + self.context.invoke('buttons.representShortcut', 'LinkPlugin.show'),
|
||||
click: self.context.createInvokeHandler('LinkPlugin.show')
|
||||
}).render();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {jQueryEvent} e
|
||||
*/
|
||||
_onDblclick: function (e) {
|
||||
if (dom.isAnchor(e.target)) {
|
||||
this.show();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var LinkPopover = Plugins.linkPopover.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Prevent links without text: replace empty text with the word "Label".
|
||||
*
|
||||
* @param {DOM} anchor
|
||||
* @param {bool} select
|
||||
*/
|
||||
fillEmptyLink: function (anchor, select) {
|
||||
if (dom.isAnchor(anchor)) {
|
||||
var text = _t('Label');
|
||||
if (this.context.invoke('HelperPlugin.getRegex', 'onlyEmptySpace').test(anchor.innerHTML)) {
|
||||
$(anchor).contents().remove();
|
||||
$(anchor).append(this.document.createTextNode(text));
|
||||
}
|
||||
var rng = this.context.invoke('editor.createRange');
|
||||
if (select && anchor.innerHTML === text) {
|
||||
this._cleanLastLink();
|
||||
rng.sc = rng.ec = anchor.firstChild;
|
||||
rng.so = 0;
|
||||
rng.eo = dom.nodeLength(rng.sc);
|
||||
rng.select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
}
|
||||
if (select) {
|
||||
this.lastAnchor = anchor;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
hide: function () {
|
||||
this._cleanLastLink();
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
update: function () {
|
||||
var rng = this.context.invoke('editor.createRange');
|
||||
var anchor = dom.ancestor(rng.sc, dom.isAnchor);
|
||||
if (anchor && anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
|
||||
anchor = dom.ancestor(rng.sc, dom.isAnchor);
|
||||
if (!$(anchor).is(':o_editable')) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
anchor = false;
|
||||
}
|
||||
|
||||
if ($(anchor).data('toggle') === 'tab') {
|
||||
anchor = false;
|
||||
}
|
||||
|
||||
if (!this.options.displayPopover(anchor)) {
|
||||
anchor = false;
|
||||
}
|
||||
|
||||
if (anchor !== this.lastAnchor) {
|
||||
this._cleanLastLink();
|
||||
}
|
||||
|
||||
if (!anchor) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dom.isAnchor(anchor)) {
|
||||
this.lastAnchor = anchor;
|
||||
}
|
||||
|
||||
|
||||
var $target = $(anchor);
|
||||
if (!$target.data('show_tooltip')) {
|
||||
$target.data('show_tooltip', true);
|
||||
setTimeout(function () {
|
||||
$target.tooltip({
|
||||
title: _t('Double-click to edit'),
|
||||
trigger: 'manuel',
|
||||
container: this.document.body,
|
||||
placement: 'top'
|
||||
}).tooltip('show');
|
||||
setTimeout(function () {
|
||||
$target.tooltip('dispose');
|
||||
}, 2000);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
var innerHTML = anchor.innerHTML.replace(' ', '\u00A0').replace('​', '\u200B');
|
||||
|
||||
// prevent links without text
|
||||
this.fillEmptyLink(anchor, true);
|
||||
|
||||
// add invisible char to prevent the carret to leave the link at the begin or end
|
||||
if (innerHTML[0] !== '\u200B') {
|
||||
var before = this.document.createTextNode('\u200B');
|
||||
$(anchor).prepend(before);
|
||||
}
|
||||
if (innerHTML[innerHTML.length - 1] !== '\u200B') {
|
||||
var after = this.document.createTextNode('\u200B');
|
||||
$(anchor).append(after);
|
||||
}
|
||||
|
||||
anchor.normalize();
|
||||
|
||||
var href = $(anchor).attr('href');
|
||||
this.$popover.find('a').attr('href', href).html(href);
|
||||
|
||||
var pos = $(anchor).offset();
|
||||
var posContainer = $(this.options.container).offset();
|
||||
pos.left = pos.left - posContainer.left + 10;
|
||||
pos.top = pos.top - posContainer.top + $(anchor).outerHeight();
|
||||
|
||||
this.$popover.css({
|
||||
display: 'block',
|
||||
left: pos.left,
|
||||
top: pos.top,
|
||||
});
|
||||
|
||||
this.context.layoutInfo.editor.after(this.$popover);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Clean the last link and prevent links without text.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_cleanLastLink: function () {
|
||||
if (this.lastAnchor) {
|
||||
this.fillEmptyLink(this.lastAnchor);
|
||||
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
var rangeChange;
|
||||
var innerHTML = this.lastAnchor.innerHTML.replace(' ', '\u00A0').replace('​', '\u200B');
|
||||
// prevent links without text
|
||||
if (
|
||||
this.$editable.has(this.lastAnchor).length &&
|
||||
this.context.invoke('HelperPlugin.getRegexBlank', {
|
||||
space: true,
|
||||
invisible: true,
|
||||
nbsp: true,
|
||||
}).test(innerHTML)
|
||||
) {
|
||||
$(this.lastAnchor).contents().remove();
|
||||
$(this.lastAnchor).append(this.document.createTextNode(_t('Label')));
|
||||
range.sc = range.ec = this.lastAnchor.firstChild;
|
||||
range.so = 0;
|
||||
range.eo = dom.nodeLength(range.sc);
|
||||
rangeChange = true;
|
||||
}
|
||||
var firstChild = this.context.invoke('HelperPlugin.firstLeaf', this.lastAnchor);
|
||||
if (
|
||||
!firstChild.tagName &&
|
||||
this.context.invoke('HelperPlugin.getRegex', 'startInvisible').test(firstChild.textContent)
|
||||
) {
|
||||
firstChild.textContent = firstChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startInvisible'), '');
|
||||
if (range.sc === firstChild && range.so) {
|
||||
range.so -= 1;
|
||||
rangeChange = true;
|
||||
}
|
||||
if (range.ec === firstChild && range.eo) {
|
||||
range.eo -= 1;
|
||||
rangeChange = true;
|
||||
}
|
||||
}
|
||||
var lastChild = this.context.invoke('HelperPlugin.lastLeaf', this.lastAnchor);
|
||||
if (
|
||||
lastChild.textContent.length > 1 &&
|
||||
!lastChild.tagName &&
|
||||
this.context.invoke('HelperPlugin.getRegex', 'endInvisible').test(lastChild.textContent)
|
||||
) {
|
||||
lastChild.textContent = lastChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'endInvisible'), '');
|
||||
if (range.sc === lastChild && range.so > dom.nodeLength(lastChild)) {
|
||||
range.so = dom.nodeLength(lastChild);
|
||||
rangeChange = true;
|
||||
}
|
||||
if (range.ec === lastChild && range.eo > dom.nodeLength(lastChild)) {
|
||||
range.eo = dom.nodeLength(lastChild);
|
||||
rangeChange = true;
|
||||
}
|
||||
}
|
||||
if (rangeChange) {
|
||||
range.select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
}
|
||||
this.lastAnchor = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('LinkPlugin', LinkPlugin)
|
||||
.add('LinkPopover', LinkPopover)
|
||||
.add('linkDialog', null)
|
||||
.add('linkPopover', null)
|
||||
.add('autoLink', null);
|
||||
|
||||
return {
|
||||
LinkPlugin: LinkPlugin,
|
||||
LinkPopover: LinkPopover,
|
||||
};
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
odoo.define('web_editor.wysiwyg.plugins', function (require) {
|
||||
'use strict';
|
||||
|
||||
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
var wysiwygOptions = require('web_editor.wysiwyg.options');
|
||||
|
||||
|
||||
var plugins = _.mapObject(wysiwygOptions.modules, function (Module, pluginName) {
|
||||
var prototype = {
|
||||
init: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
var events = _.clone(this.events);
|
||||
this.summernote.options.modules[pluginName].apply(this, arguments);
|
||||
_.each(events, function (value, key) {
|
||||
self.events[key] = value;
|
||||
});
|
||||
},
|
||||
};
|
||||
_.each(Module.prototype, function (prop, name) {
|
||||
if (typeof prop === 'function') {
|
||||
prototype[name] = function () {
|
||||
return this.summernote.options.modules[pluginName].prototype[name].apply(this, arguments);
|
||||
};
|
||||
} else {
|
||||
prototype[name] = prop;
|
||||
}
|
||||
});
|
||||
|
||||
var Plugin = AbstractPlugin.extend(prototype).extend({
|
||||
destroy: function () {
|
||||
if (this.shouldInitialize()) {
|
||||
this._super();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// override summernote default buttons
|
||||
registry.add(pluginName, Plugin);
|
||||
|
||||
return Plugin;
|
||||
});
|
||||
|
||||
// export table plugin to convert it in module (see editor)
|
||||
|
||||
var $textarea = $('<textarea>');
|
||||
$('body').append($textarea);
|
||||
$textarea.summernote();
|
||||
var summernote = $textarea.data('summernote');
|
||||
|
||||
_.each(['style', 'table', 'typing', 'bullet', 'history'], function (name) {
|
||||
var prototype = {};
|
||||
for (var k in summernote.modules.editor[name]) {
|
||||
prototype[k] = summernote.modules.editor[name][k];
|
||||
}
|
||||
plugins[name] = AbstractPlugin.extend(prototype);
|
||||
});
|
||||
|
||||
var History = summernote.modules.editor.history.constructor;
|
||||
plugins.history.include({
|
||||
init: function (context) {
|
||||
this._super(context);
|
||||
History.call(this, this.$editable);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
$textarea.summernote('destroy');
|
||||
} catch (e) {
|
||||
summernote.layoutInfo.editor.remove();
|
||||
}
|
||||
$textarea.remove();
|
||||
|
||||
|
||||
return plugins;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.table', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
|
||||
var TablePlugin = Plugins.table.extend({
|
||||
|
||||
initialize: function () {
|
||||
this._super.apply(this, arguments);
|
||||
var self = this;
|
||||
// We need setTimeout to make sure to initialize after HelperPlugin and HistoryPlugin
|
||||
setTimeout(function () {
|
||||
// contentEditable fail for image and font in table
|
||||
// user must use right arrow the number of space but without feedback
|
||||
self.$editable.find('td:has(img, span.fa)').each(function () {
|
||||
if (this.firstChild && !this.firstChild.tagName) {
|
||||
var startSpace = self.context.invoke('HelperPlugin.getRegex', 'startSpace');
|
||||
this.firstChild.textContent = this.firstChild.textContent.replace(startSpace, ' ');
|
||||
}
|
||||
if (this.lastChild && !this.lastChild.tagName) {
|
||||
var endSpace = self.context.invoke('HelperPlugin.getRegex', 'endSpace');
|
||||
this.lastChild.textContent = this.lastChild.textContent.replace(endSpace, ' ');
|
||||
}
|
||||
});
|
||||
self.context.invoke('HistoryPlugin.clear');
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a new col and
|
||||
* wrap contents of the new cells in p elements.
|
||||
*
|
||||
* @see summernote library
|
||||
*
|
||||
* @override
|
||||
* @param {WrappedRange} rng
|
||||
* @param {String('left'|'right')} position
|
||||
*/
|
||||
addCol: function (rng, position) {
|
||||
this._super.apply(this, arguments);
|
||||
var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
|
||||
var table = dom.ancestor(cell, function (n) {
|
||||
return n.tagName === 'TABLE';
|
||||
});
|
||||
var newColIndex = $(cell)[position === 'right' ? 'next' : 'prev']('td').index() + 1;
|
||||
$(table).find('td:nth-child(' + newColIndex + ')').contents().wrap('<p></p>');
|
||||
},
|
||||
/**
|
||||
* Add a new row and
|
||||
* wrap contents of the new cells in p elements.
|
||||
*
|
||||
* @see summernote library
|
||||
*
|
||||
* @override
|
||||
* @param {WrappedRange} rng
|
||||
* @param {String('top'|'bottom')} position
|
||||
*/
|
||||
addRow: function (rng, position) {
|
||||
this._super.apply(this, arguments);
|
||||
var row = dom.ancestor(rng.commonAncestor(), dom.isCell).parentElement;
|
||||
$(row)[position === 'bottom' ? 'next' : 'prev']('tr').find('td').contents().wrap('<p></p>');
|
||||
},
|
||||
/**
|
||||
* Create empty table element and
|
||||
* wrap the contents of all cells in p elements.
|
||||
*
|
||||
* @see summernote library
|
||||
*
|
||||
* @override
|
||||
* @param {Number} rowCount
|
||||
* @param {Number} colCount
|
||||
* @returns {Node} table
|
||||
*/
|
||||
createTable: function () {
|
||||
var table = this._super.apply(this, arguments);
|
||||
$(table).find('td').contents().wrap('<p></p>');
|
||||
return table;
|
||||
},
|
||||
/**
|
||||
* @see summernote library
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
deleteCol: function () {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
|
||||
// Delete the last remaining column === delete the table
|
||||
var cell = dom.ancestor(range.commonAncestor(), dom.isCell);
|
||||
if (cell && !cell.previousElementSibling && !cell.nextElementSibling) {
|
||||
return this.deleteTable();
|
||||
}
|
||||
var neighbor = cell.previousElementSibling || cell.nextElementSibling;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// Put the range back on the previous or next cell after deleting
|
||||
// to allow chain-removing
|
||||
range = this.context.invoke('editor.createRange');
|
||||
if (range.sc.tagName !== 'TD' && neighbor) {
|
||||
range = this.context.invoke('editor.setRange', neighbor, 0);
|
||||
range.normalize().select();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @see summernote library
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
deleteRow: function () {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
|
||||
// Delete the last remaining row === delete the table
|
||||
var row = dom.ancestor(range.commonAncestor(), function (n) {
|
||||
return n.tagName === 'TR';
|
||||
});
|
||||
if (row && !row.previousElementSibling && !row.nextElementSibling) {
|
||||
return this.deleteTable();
|
||||
}
|
||||
var neighbor = row.previousElementSibling || row.nextElementSibling;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// Put the range back on the previous or next row after deleting
|
||||
// to allow chain-removing
|
||||
range = this.context.invoke('editor.createRange');
|
||||
if (range.sc.tagName !== 'TR' && neighbor) {
|
||||
range = this.context.invoke('editor.setRange', neighbor, 0);
|
||||
range.normalize().select();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Delete the table in range.
|
||||
*/
|
||||
deleteTable: function () {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
var cell = dom.ancestor(range.commonAncestor(), dom.isCell);
|
||||
var table = $(cell).closest('table')[0];
|
||||
|
||||
var point = this.context.invoke('HelperPlugin.removeBlockNode', table);
|
||||
range = this.context.invoke('editor.setRange', point.node, point.offset);
|
||||
range.normalize().select();
|
||||
},
|
||||
/**
|
||||
* Insert a table.
|
||||
* Note: adds <p><br></p> before/after the table if the table
|
||||
* has nothing brefore/after it, so as to allow the carret to move there.
|
||||
*
|
||||
* @param {String} dim dimension of table (ex : "5x5")
|
||||
*/
|
||||
insertTable: function (dim) {
|
||||
var dimension = dim.split('x');
|
||||
var table = this.createTable(dimension[0], dimension[1], this.options);
|
||||
this.context.invoke('HelperPlugin.insertBlockNode', table);
|
||||
var p;
|
||||
if (!table.previousElementSibling) {
|
||||
p = this.document.createElement('p');
|
||||
$(p).append(this.document.createElement('br'));
|
||||
$(table).before(p);
|
||||
}
|
||||
if (!table.nextElementSibling) {
|
||||
p = this.document.createElement('p');
|
||||
$(p).append(this.document.createElement('br'));
|
||||
$(table).after(p);
|
||||
}
|
||||
var range = this.context.invoke('editor.setRange', $(table).find('td')[0], 0);
|
||||
range.normalize().select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
var TablePopover = Plugins.tablePopover.extend({
|
||||
events: _.defaults({
|
||||
'summernote.scroll': '_onScroll',
|
||||
}, Plugins.tablePopover.prototype.events),
|
||||
|
||||
/**
|
||||
* Update the table's popover and its position.
|
||||
*
|
||||
* @override
|
||||
* @param {Node} target
|
||||
* @returns {false|Node} the selected cell (on which to display the popover)
|
||||
*/
|
||||
update: function (target) {
|
||||
if (!target || this.context.isDisabled()) {
|
||||
return false;
|
||||
}
|
||||
var cell = dom.ancestor(target, dom.isCell);
|
||||
if (!!cell && this.options.isEditableNode(cell)) {
|
||||
var pos = $(cell).offset();
|
||||
var posContainer = $(this.options.container).offset();
|
||||
pos.left = pos.left - posContainer.left + 10;
|
||||
pos.top = pos.top - posContainer.top + $(cell).outerHeight() - 4;
|
||||
|
||||
this.lastPos = this.context.invoke('HelperPlugin.makePoint', target, $(target).offset());
|
||||
|
||||
this.$popover.css({
|
||||
display: 'block',
|
||||
left: pos.left,
|
||||
top: pos.top,
|
||||
});
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
return cell;
|
||||
},
|
||||
/**
|
||||
* Update the target table and its popover.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onScroll: function () {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
var target = dom.ancestor(range.sc, dom.isCell);
|
||||
if (!target || target === this.editable) {
|
||||
return;
|
||||
}
|
||||
if (this.lastPos && this.lastPos.target === target && $(target).offset()) {
|
||||
var newTop = $(target).offset().top;
|
||||
var movement = this.lastPos.offset.top - newTop;
|
||||
if (movement && this.mousePosition) {
|
||||
this.mousePosition.pageY -= movement;
|
||||
}
|
||||
}
|
||||
return this.update(target);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
registry.add('TablePlugin', TablePlugin);
|
||||
registry.add('TablePopover', TablePopover);
|
||||
registry.add('tablePopover', null);
|
||||
|
||||
return TablePlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,799 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.text', function (require) {
|
||||
'use strict';
|
||||
|
||||
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
dom.isAnchor = function (node) {
|
||||
return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
|
||||
!$(node).hasClass('fa') && !$(node).hasClass('o_image');
|
||||
};
|
||||
|
||||
|
||||
var TextPlugin = AbstractPlugin.extend({
|
||||
events: {
|
||||
'summernote.paste': '_onPaste',
|
||||
},
|
||||
|
||||
// See: https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#removeformat-candidate
|
||||
formatTags: [
|
||||
'abbr',
|
||||
'acronym',
|
||||
'b',
|
||||
'bdi',
|
||||
'bdo',
|
||||
'big',
|
||||
'blink',
|
||||
'cite',
|
||||
'code',
|
||||
'dfn',
|
||||
'em',
|
||||
'font',
|
||||
'i',
|
||||
'ins',
|
||||
'kbd',
|
||||
'mark',
|
||||
'nobr',
|
||||
'q',
|
||||
's',
|
||||
'samp',
|
||||
'small',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'sub',
|
||||
'sup',
|
||||
'tt',
|
||||
'u',
|
||||
'var',
|
||||
],
|
||||
tab: '\u00A0\u00A0\u00A0\u00A0',
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Insert a Horizontal Rule element (hr).
|
||||
*/
|
||||
insertHR: function () {
|
||||
var self = this;
|
||||
var hr = this.document.createElement('hr');
|
||||
this.context.invoke('HelperPlugin.insertBlockNode', hr);
|
||||
var point = this.context.invoke('HelperPlugin.makePoint', hr, 0);
|
||||
point = dom.nextPointUntil(point, function (pt) {
|
||||
return pt.node !== hr && !self.options.isUnbreakableNode(pt.node);
|
||||
}) || point;
|
||||
var range = $.summernote.range.create(point.node, point.offset);
|
||||
range.select();
|
||||
},
|
||||
/**
|
||||
* Insert a TAB (4 non-breakable spaces).
|
||||
*/
|
||||
insertTab: function () {
|
||||
this.context.invoke('HelperPlugin.insertTextInline', this.tab);
|
||||
},
|
||||
/**
|
||||
* Paste nodes or their text content into the editor.
|
||||
*
|
||||
* @param {Node[]} nodes
|
||||
* @param {Boolean} textOnly true to allow only dropping plain text
|
||||
*/
|
||||
pasteNodes: function (nodes, textOnly) {
|
||||
if (!nodes.length) {
|
||||
return;
|
||||
}
|
||||
nodes = textOnly ? this.document.createTextNode($(nodes).text()) : nodes;
|
||||
nodes = this._mergeAdjacentULs(nodes);
|
||||
|
||||
var point = this._getPastePoint();
|
||||
// Prevent pasting HTML within a link:
|
||||
point = textOnly ? point : dom.nextPointUntil(point, this._isPointInAnchor.bind(this));
|
||||
|
||||
this._insertNodesAt(nodes, point);
|
||||
|
||||
var start = nodes[nodes.length - 1];
|
||||
this.context.invoke('editor.setRange', start, dom.nodeLength(start)).normalize().select();
|
||||
},
|
||||
/**
|
||||
* Prepare clipboard data for safe pasting into the editor.
|
||||
*
|
||||
* @see clipboardWhitelist
|
||||
* @see clipboardBlacklist
|
||||
*
|
||||
* @param {DOMString} clipboardData
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
prepareClipboardData: function (clipboardData) {
|
||||
var $clipboardData = this._removeIllegalClipboardElements($(clipboardData));
|
||||
|
||||
var $all = $clipboardData.find('*').addBack();
|
||||
$all.filter('table').addClass('table table-bordered');
|
||||
this._wrapTDContents($all.filter('td'));
|
||||
this._fillEmptyBlocks($all);
|
||||
this._removeIllegalClipboardAttributes($all);
|
||||
$all.filter('a').removeClass();
|
||||
$all.filter('img').css('max-width', '100%');
|
||||
|
||||
return $clipboardData.toArray();
|
||||
},
|
||||
/**
|
||||
* Format a 'format' block: change its tagName (eg: p -> h1).
|
||||
*
|
||||
* @param {string} tagName
|
||||
* P, H1, H2, H3, H4, H5, H6, BLOCKQUOTE, PRE
|
||||
*/
|
||||
formatBlock: function (tagName) {
|
||||
var self = this;
|
||||
var r = this.context.invoke('editor.createRange');
|
||||
if (
|
||||
!r ||
|
||||
!this.$editable.has(r.sc).length ||
|
||||
!this.$editable.has(r.ec).length ||
|
||||
this.options.isUnbreakableNode(r.sc)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
|
||||
nodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
|
||||
if (!nodes.length) {
|
||||
var node = this.context.invoke('editor.createRange').sc;
|
||||
if (node.tagName === 'BR' || dom.isText(node)) {
|
||||
node = node.parentNode;
|
||||
}
|
||||
nodes = [node];
|
||||
}
|
||||
var changedNodes = [];
|
||||
_.each(nodes, function (node) {
|
||||
var newNode = self.document.createElement(tagName);
|
||||
$(newNode).append($(node).contents());
|
||||
var attributes = $(node).prop("attributes");
|
||||
_.each(attributes, function (attr) {
|
||||
$(newNode).attr(attr.name, attr.value);
|
||||
});
|
||||
$(node).replaceWith(newNode);
|
||||
changedNodes.push(newNode);
|
||||
});
|
||||
|
||||
// Select all formatted nodes
|
||||
if (changedNodes.length) {
|
||||
var lastNode = changedNodes[changedNodes.length - 1];
|
||||
var startNode = changedNodes[0].firstChild || changedNodes[0];
|
||||
var endNode = lastNode.lastChild || lastNode;
|
||||
var range = this.context.invoke('editor.setRange', startNode, 0, endNode, dom.nodeLength(endNode));
|
||||
range.select();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Change the paragraph alignment of a 'format' block.
|
||||
*
|
||||
* @param {string} style
|
||||
* justifyLeft, justifyCenter, justifyRight, justifyFull
|
||||
*/
|
||||
formatBlockStyle: function (style) {
|
||||
var self = this;
|
||||
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
|
||||
nodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
|
||||
var align = style === 'justifyLeft' ? 'left' :
|
||||
style === 'justifyCenter' ? 'center' :
|
||||
style === 'justifyRight' ? 'right' : 'justify';
|
||||
_.each(nodes, function (node) {
|
||||
if (dom.isText(node)) {
|
||||
return;
|
||||
}
|
||||
var textAlign = self.window.getComputedStyle(node).textAlign;
|
||||
if (align !== textAlign) {
|
||||
if (align !== self.window.getComputedStyle(node.parentNode).textAlign) {
|
||||
$(node).css('text-align', align);
|
||||
} else {
|
||||
$(node).css('text-align', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
this.editable.normalize();
|
||||
},
|
||||
/**
|
||||
* (Un-)format text: make it bold, italic, ...
|
||||
*
|
||||
* @param {string} tagName
|
||||
* bold, italic, underline, strikethrough, superscript, subscript
|
||||
*/
|
||||
formatText: function (tagName) {
|
||||
var self = this;
|
||||
var tag = {
|
||||
bold: 'B',
|
||||
italic: 'I',
|
||||
underline: 'U',
|
||||
strikethrough: 'S',
|
||||
superscript: 'SUP',
|
||||
subscript: 'SUB',
|
||||
} [tagName];
|
||||
if (!tag) {
|
||||
throw new Error(tagName);
|
||||
}
|
||||
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
if (!range || !this.$editable.has(range.sc).length || !this.$editable.has(range.ec).length) {
|
||||
return;
|
||||
}
|
||||
if (range.isCollapsed()) {
|
||||
var br;
|
||||
if (range.sc.tagName === 'BR') {
|
||||
br = range.sc;
|
||||
} else if (range.sc.firstChild && range.sc.firstChild.tagName === 'BR') {
|
||||
br = range.sc.firstChild;
|
||||
}
|
||||
if (br) {
|
||||
var emptyText = this.document.createTextNode('\u200B');
|
||||
$(br).before(emptyText).remove();
|
||||
range = this.context.invoke('editor.setRange', emptyText, 0, emptyText, 1);
|
||||
} else {
|
||||
this.context.invoke('HelperPlugin.insertTextInline', '\u200B');
|
||||
range.eo += 1;
|
||||
}
|
||||
range.select();
|
||||
}
|
||||
|
||||
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
|
||||
var texts = this.context.invoke('HelperPlugin.filterLeafChildren', nodes);
|
||||
var formatted = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
|
||||
|
||||
var start = this.context.invoke('HelperPlugin.firstLeaf', nodes[0]);
|
||||
var end = this.context.invoke('HelperPlugin.lastLeaf', nodes[nodes.length - 1]);
|
||||
|
||||
function containsOnlySelectedText(node) {
|
||||
return _.all(node.childNodes, function (n) {
|
||||
return _.any(texts, function (t) {
|
||||
return n === t && !(dom.isText(n) && n.textContent === '');
|
||||
}) && containsOnlySelectedText(n);
|
||||
});
|
||||
}
|
||||
|
||||
function containsAllSelectedText(node) {
|
||||
return _.all(texts, function (t) {
|
||||
return _.any(node.childNodes, function (n) {
|
||||
return n === t && !(dom.isText(n) && n.textContent === '') || containsAllSelectedText(n);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var nodeAlreadyStyled = [];
|
||||
var toStyled = [];
|
||||
var notStyled = _.filter(texts, function (text, index) {
|
||||
if (toStyled.indexOf(text) !== -1 || nodeAlreadyStyled.indexOf(text) !== -1) {
|
||||
return;
|
||||
}
|
||||
nodeAlreadyStyled.push(text);
|
||||
|
||||
end = text;
|
||||
|
||||
var styled = dom.ancestor(text, function (node) {
|
||||
return node.tagName === tag;
|
||||
});
|
||||
if (styled) {
|
||||
if (
|
||||
!/^\u200B$/.test(text.textContent) &&
|
||||
containsAllSelectedText(styled) &&
|
||||
containsOnlySelectedText(styled)
|
||||
) {
|
||||
// Unwrap all contents
|
||||
nodes = $(styled).contents();
|
||||
$(styled).before(nodes).remove();
|
||||
nodeAlreadyStyled.push.apply(nodeAlreadyStyled, nodes);
|
||||
end = _.last(nodeAlreadyStyled);
|
||||
} else {
|
||||
var options = {
|
||||
isSkipPaddingBlankHTML: true,
|
||||
isNotSplitEdgePoint: true,
|
||||
};
|
||||
|
||||
if (
|
||||
nodeAlreadyStyled.indexOf(text.nextSibling) === -1 &&
|
||||
!dom.isRightEdgeOf(text, styled)
|
||||
) {
|
||||
options.nextText = false;
|
||||
var point = self.context.invoke('HelperPlugin.makePoint', text, dom.nodeLength(text));
|
||||
if (dom.isMedia(text)) {
|
||||
point = dom.nextPoint(point);
|
||||
}
|
||||
var next = self.context.invoke('HelperPlugin.splitTree', styled, point, options);
|
||||
nodeAlreadyStyled.push(next);
|
||||
}
|
||||
if (
|
||||
nodeAlreadyStyled.indexOf(text.previousSibling) === -1 &&
|
||||
!dom.isLeftEdgeOf(text, styled)
|
||||
) {
|
||||
options.nextText = true;
|
||||
var textPoint = self.context.invoke('HelperPlugin.makePoint', text, 0);
|
||||
text = self.context.invoke('HelperPlugin.splitTree', styled, textPoint, options);
|
||||
nodeAlreadyStyled.push(text);
|
||||
if (index === 0) {
|
||||
start = text;
|
||||
}
|
||||
end = text;
|
||||
}
|
||||
|
||||
var toRemove = dom.ancestor(text, function (n) {
|
||||
return n.tagName === tag;
|
||||
});
|
||||
if (toRemove) {
|
||||
// Remove generated empty elements
|
||||
if (
|
||||
toRemove.nextSibling &&
|
||||
self.context.invoke('HelperPlugin.isBlankNode', toRemove.nextSibling)
|
||||
) {
|
||||
$(toRemove.nextSibling).remove();
|
||||
}
|
||||
if (
|
||||
toRemove.previousSibling &&
|
||||
self.context.invoke('HelperPlugin.isBlankNode', toRemove.previousSibling)
|
||||
) {
|
||||
$(toRemove.previousSibling).remove();
|
||||
}
|
||||
|
||||
// Unwrap the element
|
||||
nodes = $(toRemove).contents();
|
||||
$(toRemove).before(nodes).remove();
|
||||
nodeAlreadyStyled.push.apply(nodeAlreadyStyled, nodes);
|
||||
end = _.last(nodeAlreadyStyled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dom.ancestor(text, function (node) {
|
||||
return toStyled.indexOf(node) !== -1;
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
var node = text;
|
||||
while (
|
||||
node && node.parentNode &&
|
||||
formatted.indexOf(node) === -1 &&
|
||||
formatted.indexOf(node.parentNode) === -1
|
||||
) {
|
||||
node = node.parentNode;
|
||||
}
|
||||
if (node !== text) {
|
||||
if (containsAllSelectedText(node)) {
|
||||
toStyled.push.apply(toStyled, texts);
|
||||
node = text;
|
||||
} else if (!containsOnlySelectedText(node)) {
|
||||
|
||||
node = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (toStyled.indexOf(node) === -1) {
|
||||
toStyled.push(node);
|
||||
}
|
||||
return !styled;
|
||||
});
|
||||
|
||||
toStyled = _.uniq(toStyled);
|
||||
|
||||
if (notStyled.length) {
|
||||
nodes = [];
|
||||
var toMerge = [];
|
||||
_.each(toStyled, function (node) {
|
||||
var next = true;
|
||||
if (node.nextSibling && node.nextSibling.tagName === tag) {
|
||||
$(node.nextSibling).prepend(node);
|
||||
next = false;
|
||||
}
|
||||
if (node.previousSibling && node.previousSibling.tagName === tag) {
|
||||
$(node.previousSibling).append(node);
|
||||
}
|
||||
if (node.parentNode && node.parentNode.tagName !== tag) {
|
||||
var styled = self.document.createElement(tag);
|
||||
if (node.tagName) {
|
||||
$(styled).append(node.childNodes);
|
||||
$(node).append(styled);
|
||||
} else {
|
||||
$(node).before(styled);
|
||||
styled.appendChild(node);
|
||||
}
|
||||
}
|
||||
// Add adjacent nodes with same tagName to list of nodes to merge
|
||||
if (
|
||||
node.parentNode && node.parentNode[next ? 'nextSibling' : 'previousSibling'] &&
|
||||
node.parentNode.tagName === node.parentNode[next ? 'nextSibling' : 'previousSibling'].tagName
|
||||
) {
|
||||
toMerge.push(next ? node.parentNode : node.parentNode.previousSibling);
|
||||
}
|
||||
});
|
||||
// Merge what needs merging
|
||||
while (toMerge.length) {
|
||||
this.context.invoke('HelperPlugin.deleteEdge', toMerge.pop(), 'next');
|
||||
}
|
||||
}
|
||||
|
||||
range = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
|
||||
|
||||
if (range.sc === range.ec && range.sc.textContent === '\u200B') {
|
||||
range.so = range.eo = 1;
|
||||
}
|
||||
|
||||
range.select();
|
||||
this.editable.normalize();
|
||||
},
|
||||
/**
|
||||
* Remove format on the current range.
|
||||
*
|
||||
* @see _isParentRemoveFormatCandidate
|
||||
*/
|
||||
removeFormat: function () {
|
||||
this._selectCurrentIfCollapsed();
|
||||
var selectedText = this.context.invoke('HelperPlugin.getSelectedText');
|
||||
var selectedIcons = this.context.invoke('HelperPlugin.getSelectedNodes');
|
||||
selectedIcons = _.filter(selectedIcons, dom.isIcon);
|
||||
if (!selectedText.length && !selectedIcons.length) {
|
||||
return;
|
||||
}
|
||||
_.each(selectedIcons, this._removeIconFormat.bind(this));
|
||||
_.each(selectedText, this._removeTextFormat.bind(this));
|
||||
var startNode = selectedText[0];
|
||||
var endNode = selectedText[selectedText.length - 1];
|
||||
this.context.invoke('editor.setRange', startNode, 0, endNode, dom.nodeLength(endNode)).select();
|
||||
this.editable.normalize();
|
||||
this.context.invoke('editor.saveRange');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Remove the non-whitelisted or blacklisted
|
||||
* top level elements from clipboard data.
|
||||
*
|
||||
* @see clipboardWhitelist
|
||||
* @see clipboardBlacklist
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $clipboardData
|
||||
* @returns {Object} {$clipboardData: JQuery, didRemoveNodes: Boolean}
|
||||
*/
|
||||
_cleanClipboardRoot: function ($clipboardData) {
|
||||
var didRemoveNodes = false;
|
||||
var whiteList = this._clipboardWhitelist();
|
||||
var blackList = this._clipboardBlacklist();
|
||||
var $fakeParent = $(this.document.createElement('div'));
|
||||
_.each($clipboardData, function (node) {
|
||||
var isWhitelisted = dom.isText(node) || $(node).filter(whiteList.join(',')).length;
|
||||
var isBlacklisted = $(node).filter(blackList.join(',')).length;
|
||||
if (!isWhitelisted || isBlacklisted) {
|
||||
$fakeParent.append(node.childNodes);
|
||||
didRemoveNodes = true;
|
||||
} else {
|
||||
$fakeParent.append(node);
|
||||
}
|
||||
});
|
||||
return {
|
||||
$clipboardData: $fakeParent.contents(),
|
||||
didRemoveNodes: didRemoveNodes,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Return a list of jQuery selectors for prohibited nodes on paste.
|
||||
*
|
||||
* @private
|
||||
* @returns {String[]}
|
||||
*/
|
||||
_clipboardBlacklist: function () {
|
||||
return ['.Apple-interchange-newline'];
|
||||
},
|
||||
/**
|
||||
* Return a list of jQuery selectors for exclusively authorized nodes on paste.
|
||||
*
|
||||
* @private
|
||||
* @returns {String[]}
|
||||
*/
|
||||
_clipboardWhitelist: function () {
|
||||
var listSels = ['ul', 'ol', 'li'];
|
||||
var styleSels = ['i', 'b', 'u', 'em', 'strong'];
|
||||
var tableSels = ['table', 'th', 'tbody', 'tr', 'td'];
|
||||
var miscSels = ['img', 'br', 'a', '.fa'];
|
||||
return this.options.styleTags.concat(listSels, styleSels, tableSels, miscSels);
|
||||
},
|
||||
/**
|
||||
* Return a list of attribute names that are exclusively authorized on paste.
|
||||
*
|
||||
* @private
|
||||
* @returns {String[]}
|
||||
*/
|
||||
_clipboardWhitelistAttr: function () {
|
||||
return ['class', 'href', 'src'];
|
||||
},
|
||||
/**
|
||||
* Fill up empty block elements with BR elements so the carret can enter them.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $els
|
||||
*/
|
||||
_fillEmptyBlocks: function ($els) {
|
||||
var self = this;
|
||||
|
||||
$els.filter(function (i, n) {
|
||||
return self.context.invoke('HelperPlugin.isNodeBlockType', n) && !n.childNodes;
|
||||
}).append(this.document.createElement('br'));
|
||||
},
|
||||
/**
|
||||
* Get all non-whitelisted or blacklisted elements from clipboard data.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $clipboardData
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
_filterIllegalClipboardElements: function ($clipboardData) {
|
||||
return $clipboardData.find('*').addBack()
|
||||
.not(this._clipboardWhitelist().join(','))
|
||||
.addBack(this._clipboardBlacklist().join(','))
|
||||
.filter(function () {
|
||||
return !dom.isText(this);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get a legal point to paste at, from the current range's start point.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} {node: Node, offset: Number}
|
||||
*/
|
||||
_getPastePoint: function () {
|
||||
var point = this.context.invoke('editor.createRange').getStartPoint();
|
||||
var offsetChild = point.node.childNodes[point.offset];
|
||||
point = offsetChild ? this.context.invoke('HelperPlugin.makePoint', offsetChild, 0) : point;
|
||||
return dom.nextPointUntil(point, this._isPastePointLegal.bind(this));
|
||||
},
|
||||
/**
|
||||
* Insert nodes at a point. Insert them inline if the first node is inline
|
||||
* and pasting inline is legal at that point.
|
||||
*
|
||||
* @private
|
||||
* @param {Node[]} nodes
|
||||
* @param {Object} point {node: Node, offset: Number}
|
||||
*/
|
||||
_insertNodesAt: function (nodes, point) {
|
||||
var canInsertInline = dom.isText(point.node) || point.node.tagName === 'BR' || dom.isMedia(point.node);
|
||||
var $fakeParent = $(this.document.createElement('div'));
|
||||
$fakeParent.append(nodes);
|
||||
if (dom.isInline(nodes[0]) && canInsertInline) {
|
||||
point.node = point.node.tagName ? point.node : point.node.splitText(point.offset);
|
||||
$(point.node).before($fakeParent.contents());
|
||||
} else {
|
||||
this.context.invoke('HelperPlugin.insertBlockNode', $fakeParent[0]);
|
||||
}
|
||||
$fakeParent.contents().unwrap();
|
||||
},
|
||||
/**
|
||||
* Return true if the parent of the given node is a removeFormat candidate:
|
||||
* - It is a removeFormat candidate as defined by W3C
|
||||
* - It is contained within the editable area
|
||||
* - It is not unbreakable
|
||||
*
|
||||
* @see formatTags the list of removeFormat candidates as defined by W3C
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node
|
||||
*/
|
||||
_isParentRemoveFormatCandidate: function (node) {
|
||||
var parent = node.parentNode;
|
||||
if (!parent) {
|
||||
return false;
|
||||
}
|
||||
var isEditableOrAbove = parent && (parent === this.editable || $.contains(parent, this.editable));
|
||||
var isUnbreakable = parent && this.options.isUnbreakableNode(parent);
|
||||
var isRemoveFormatCandidate = parent && parent.tagName && this.formatTags.indexOf(parent.tagName.toLowerCase()) !== -1;
|
||||
return parent && !isEditableOrAbove && !isUnbreakable && isRemoveFormatCandidate;
|
||||
},
|
||||
/**
|
||||
* Return true if it's legal to paste nodes at the given point:
|
||||
* if the point is not within a void node and the point is not unbreakable.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} point {node: Node, offset: Number}
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isPastePointLegal: function (point) {
|
||||
var node = point.node;
|
||||
var isWithinVoid = false;
|
||||
if (node.parentNode) {
|
||||
isWithinVoid = dom.isVoid(node.parentNode) || $(node.parentNode).filter('.fa').length;
|
||||
}
|
||||
return !isWithinVoid && !this.options.isUnbreakableNode(point.node);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} point {node: Node, offset: Number}
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isPointInAnchor: function (point) {
|
||||
var ancestor = dom.ancestor(point.node, dom.isAnchor);
|
||||
return !ancestor || ancestor === this.editable;
|
||||
},
|
||||
/**
|
||||
* Check a list of nodes and merges all adjacent ULs together:
|
||||
* [ul, ul, p, ul, ul] will return [ul, p, ul], with the li's of
|
||||
* nodes[1] and nodes[4] appended to nodes[0] and nodes[3].
|
||||
*
|
||||
* @private
|
||||
* @param {Node[]} nodes
|
||||
* @return {Node[]} the remaining, merged nodes
|
||||
*/
|
||||
_mergeAdjacentULs: function (nodes) {
|
||||
var res = [];
|
||||
var prevNode;
|
||||
_.each(nodes, function (node) {
|
||||
prevNode = res[res.length - 1];
|
||||
if (prevNode && node.tagName === 'UL' && prevNode.tagName === 'UL') {
|
||||
$(prevNode).append(node.childNodes);
|
||||
} else {
|
||||
res.push(node);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
},
|
||||
/**
|
||||
* Remove an icon's format (colors, font size).
|
||||
*
|
||||
* @private
|
||||
* @param {Node} icon
|
||||
*/
|
||||
_removeIconFormat: function (icon) {
|
||||
$(icon).css({
|
||||
color: '',
|
||||
backgroundColor: '',
|
||||
fontSize: '',
|
||||
});
|
||||
var reColorClasses = /(^|\s+)(bg|text)-\S*|/g;
|
||||
icon.className = icon.className.replace(reColorClasses, '').trim();
|
||||
},
|
||||
/**
|
||||
* Remove non-whitelisted attributes from clipboard.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $els
|
||||
*/
|
||||
_removeIllegalClipboardAttributes: function ($els) {
|
||||
var self = this;
|
||||
$els.each(function () {
|
||||
var $node = $(this);
|
||||
_.each(_.pluck(this.attributes, 'name'), function (attribute) {
|
||||
if (self._clipboardWhitelistAttr().indexOf(attribute) === -1) {
|
||||
$node.removeAttr(attribute);
|
||||
}
|
||||
});
|
||||
}).removeClass('o_editable o_not_editable');
|
||||
},
|
||||
/**
|
||||
* Remove non-whitelisted and blacklisted elements from clipboard data.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $clipboardData
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
_removeIllegalClipboardElements: function ($clipboardData) {
|
||||
var root = true;
|
||||
$clipboardData = $clipboardData.not('meta').not('style').not('script');
|
||||
var $badNodes = this._filterIllegalClipboardElements($clipboardData);
|
||||
|
||||
do {
|
||||
if (root) {
|
||||
root = false;
|
||||
var cleanData = this._cleanClipboardRoot($clipboardData);
|
||||
$clipboardData = cleanData.$clipboardData;
|
||||
root = cleanData.didRemoveNodes;
|
||||
} else {
|
||||
this._removeNodesPreserveContents($badNodes);
|
||||
}
|
||||
|
||||
$badNodes = this._filterIllegalClipboardElements($clipboardData);
|
||||
} while ($badNodes.length);
|
||||
return $clipboardData;
|
||||
},
|
||||
/**
|
||||
* Remove nodes from the DOM while preserving their contents if any.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $nodes
|
||||
*/
|
||||
_removeNodesPreserveContents: function ($nodes) {
|
||||
var $contents = $nodes.contents();
|
||||
if ($contents.length) {
|
||||
$contents.unwrap();
|
||||
} else {
|
||||
$nodes.remove();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Remove a text node's format: remove its style parents (b, i, u, ...).
|
||||
*
|
||||
* @private
|
||||
* @param {Node} textNode
|
||||
*/
|
||||
_removeTextFormat: function (textNode) {
|
||||
var node = textNode;
|
||||
while (this._isParentRemoveFormatCandidate(node)) {
|
||||
this.context.invoke('HelperPlugin.splitAtNodeEnds', node);
|
||||
$(node.parentNode).before(node).remove();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Select the whole current node if the range is collapsed
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_selectCurrentIfCollapsed: function () {
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
if (!range.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
this.context.invoke('editor.setRange', range.sc, 0, range.sc, dom.nodeLength(range.sc)).select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
},
|
||||
/**
|
||||
* Prevent inline nodes directly in TDs by wrapping them in P elements.
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $tds
|
||||
*/
|
||||
_wrapTDContents: function ($tds) {
|
||||
var self = this;
|
||||
var $inlinesInTD = $tds.contents().filter(function () {
|
||||
return !self.context.invoke('HelperPlugin.isNodeBlockType', this);
|
||||
});
|
||||
var parentsOfInlinesInTD = [];
|
||||
_.each($inlinesInTD, function (n) {
|
||||
parentsOfInlinesInTD.push(self.context.invoke('HelperPlugin.firstBlockAncestor', n));
|
||||
});
|
||||
$($.unique(parentsOfInlinesInTD)).wrapInner(this.document.createElement('p'));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle paste events to permit cleaning/sorting of the data before pasting.
|
||||
*
|
||||
* @private
|
||||
* @param {SummernoteEvent} se
|
||||
* @param {jQueryEvent} e
|
||||
*/
|
||||
_onPaste: function (se, e) {
|
||||
se.preventDefault();
|
||||
se.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
this.context.invoke('editor.beforeCommand');
|
||||
|
||||
// Clean up
|
||||
var clipboardData = e.originalEvent.clipboardData.getData('text/html');
|
||||
if (clipboardData) {
|
||||
clipboardData = this.prepareClipboardData(clipboardData);
|
||||
} else {
|
||||
clipboardData = e.originalEvent.clipboardData.getData('text/plain');
|
||||
// get that text as an array of text nodes separated by <br> where needed
|
||||
var allNewlines = /\n/g;
|
||||
clipboardData = $('<p>' + clipboardData.replace(allNewlines, '<br>') + '</p>').contents().toArray();
|
||||
}
|
||||
|
||||
// Delete selection
|
||||
this.context.invoke('HelperPlugin.deleteSelection');
|
||||
|
||||
// Insert the nodes
|
||||
this.pasteNodes(clipboardData);
|
||||
this.context.invoke('HelperPlugin.normalize');
|
||||
this.context.invoke('editor.saveRange');
|
||||
|
||||
this.context.invoke('editor.afterCommand');
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('TextPlugin', TextPlugin);
|
||||
|
||||
return TextPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.toolbar', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Plugins = require('web_editor.wysiwyg.plugins');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
|
||||
var ToolbarPlugin = Plugins.toolbar.extend({
|
||||
events: {
|
||||
'summernote.mouseup': 'update',
|
||||
'summernote.keyup': 'update',
|
||||
'summernote.change': 'update',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
initialize: function () {
|
||||
this._super();
|
||||
this.update();
|
||||
},
|
||||
/**
|
||||
* Update the toolbar (enabled/disabled).
|
||||
*/
|
||||
update: function () {
|
||||
var $btn = this.$toolbar.children().not('.note-history, .note-view').find('button');
|
||||
var target = this.context.invoke('editor.restoreTarget') || this.context.invoke('editor.createRange').sc;
|
||||
|
||||
if (!target || !$.contains(this.editable, target) || !this.options.isEditableNode(target)) {
|
||||
$btn.addClass('o_disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.removeClass('o_disabled');
|
||||
|
||||
if (!target || !this.options.displayPopover(target)) {
|
||||
$btn.addClass('o_disabled');
|
||||
}
|
||||
|
||||
if (dom.ancestor(target, dom.isMedia)) {
|
||||
this.$toolbar.children('.note-style, .note-font, .note-para, .note-table').addClass('o_disabled');
|
||||
|
||||
$btn.find('.fa-file-image-o').parent().removeClass('o_disabled');
|
||||
if (dom.ancestor(target, dom.isFont)) {
|
||||
this.$toolbar.children('.note-color').removeClass('o_disabled');
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('toolbar', ToolbarPlugin);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.transform', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
|
||||
var wysiwygOptions = require('web_editor.wysiwyg.options');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var TransformPlugin = AbstractPlugin.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Manages transformations on a media.
|
||||
*/
|
||||
transform: function () {
|
||||
var $image = $(this.context.invoke('editor.restoreTarget'));
|
||||
|
||||
if ($image.data('transfo-destroy')) {
|
||||
$image.removeData('transfo-destroy');
|
||||
return;
|
||||
}
|
||||
|
||||
$image.transfo(); // see web_editor/static/lib/jQuery.transfo.js
|
||||
|
||||
var mouseup = function () {
|
||||
$('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]'));
|
||||
};
|
||||
$(document).on('mouseup', mouseup);
|
||||
|
||||
var mousedown = this._wrapCommand(function (event) {
|
||||
if (!$(event.target).closest('.transfo-container').length) {
|
||||
$image.transfo('destroy');
|
||||
$(document).off('mousedown', mousedown).off('mouseup', mouseup);
|
||||
}
|
||||
if ($(event.target).closest('.note-popover').length) {
|
||||
var transformStyles = self.context.invoke('HelperPlugin.getRegex', '', 'g', '[^;]*transform[\\w:]*;?');
|
||||
$image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(transformStyles, ''));
|
||||
}
|
||||
});
|
||||
$(document).on('mousedown', mousedown);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adds the transform buttons.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_addButtons: function () {
|
||||
var self = this;
|
||||
this._super();
|
||||
|
||||
this.context.memo('button.transform', function () {
|
||||
return self.context.invoke('buttons.button', {
|
||||
contents: self.ui.icon(self.options.icons.transform),
|
||||
tooltip: self.lang.image.transform,
|
||||
click: self.context.createInvokeHandler('TransformPlugin.transform'),
|
||||
}).render();
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
_.extend(wysiwygOptions.icons, {
|
||||
transform: 'fa fa-object-ungroup',
|
||||
});
|
||||
_.extend(wysiwygTranslation.image, {
|
||||
transform: _t('Transform the picture (click twice to reset transformation)'),
|
||||
});
|
||||
|
||||
registry.add('TransformPlugin', TransformPlugin);
|
||||
|
||||
return TransformPlugin;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.unbreakable', function (require) {
|
||||
'use strict';
|
||||
|
||||
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
|
||||
var registry = require('web_editor.wysiwyg.plugin.registry');
|
||||
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// unbreakable node preventing editing
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
var Unbreakable = AbstractPlugin.extend({
|
||||
events: {
|
||||
'wysiwyg.range .note-editable': '_onRange',
|
||||
'summernote.mouseup': '_onMouseUp',
|
||||
'summernote.keyup': '_onKeyup',
|
||||
'summernote.keydown': '_onKeydown',
|
||||
// 'summernote.focusnode': '_onFocusnode', => add this event to summernote.
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
setTimeout(function () {
|
||||
self.secureArea();
|
||||
self.context.invoke('HistoryPlugin.clear');
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Change the selection if it breaks an unbreakable node.
|
||||
*
|
||||
<unbreakable id="a">
|
||||
content_1
|
||||
<unbreakable id="b">content_2</unbreakable>
|
||||
<allow id="c">
|
||||
content_3
|
||||
<unbreakable id="d">content_4</unbreakable>
|
||||
<unbreakable id="e">
|
||||
content_5
|
||||
<allow id="f">content_6</allow>
|
||||
content_7
|
||||
</unbreakable>
|
||||
content_8
|
||||
</allow>
|
||||
<unbreakable id="f">content_9</unbreakable>
|
||||
<allow id="g">
|
||||
content_10
|
||||
<unbreakable id="h">content_11</unbreakable>
|
||||
content_12
|
||||
</allow>
|
||||
</unbreakable>
|
||||
|
||||
START END RESIZE START RESIZE END
|
||||
|
||||
content_1 content_1 content_3 content_3 (find the first allowed node)
|
||||
content_1 content_2 content_3 content_3
|
||||
content_1 content_3 content_3 -
|
||||
content_3 content_3 - - (nothing to do)
|
||||
content_3 content_8 - - (can remove unbreakable node)
|
||||
content_3 content_4 - content_3
|
||||
content_3 content_5 - #d (can select the entire unbreakable node)
|
||||
content_5 content_8 content_6 content_6
|
||||
content_5 content_7 #e #e (select the entire unbreakable node)
|
||||
content_6 content_8 - content_6
|
||||
content_7 content_8 - content_8
|
||||
content_9 content_12 content_10 -
|
||||
*
|
||||
* @returns {WrappedRange}
|
||||
*/
|
||||
secureRange: function () {
|
||||
var self = this;
|
||||
var range = this.context.invoke('editor.createRange');
|
||||
var isCollapsed = range.isCollapsed();
|
||||
var needReselect = false;
|
||||
var startPoint = range.getStartPoint();
|
||||
var endPoint = range.getEndPoint();
|
||||
|
||||
// don't change the selection if the carret is just after a media in editable area
|
||||
var prev;
|
||||
if (
|
||||
isCollapsed && startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset] &&
|
||||
(prev = dom.prevPoint(startPoint)) && dom.isMedia(prev.node) &&
|
||||
this.options.isEditableNode(prev.node.parentNode)
|
||||
) {
|
||||
return range;
|
||||
}
|
||||
|
||||
// move the start selection to an allowed node
|
||||
var target = startPoint.node.childNodes[startPoint.offset] || startPoint.node;
|
||||
if (startPoint.offset && startPoint.offset === dom.nodeLength(startPoint.node)) {
|
||||
startPoint.node = this.context.invoke('HelperPlugin.lastLeaf', startPoint.node);
|
||||
startPoint.offset = dom.nodeLength(startPoint.node);
|
||||
}
|
||||
if (!dom.isMedia(target) || !this.options.isEditableNode(target)) {
|
||||
var afterEnd = false;
|
||||
startPoint = dom.nextPointUntil(startPoint, function (point) {
|
||||
if (point.node === endPoint.node && point.offset === endPoint.offset) {
|
||||
afterEnd = true;
|
||||
}
|
||||
return self.options.isEditableNode(point.node) && dom.isVisiblePoint(point) || !point.node;
|
||||
});
|
||||
if (!startPoint || !startPoint.node) { // no allowed node, search the other way
|
||||
afterEnd = false;
|
||||
startPoint = dom.prevPointUntil(range.getStartPoint(), function (point) {
|
||||
return self.options.isEditableNode(point.node) && dom.isVisiblePoint(point) || !point.node;
|
||||
});
|
||||
}
|
||||
if (startPoint && !startPoint.node) {
|
||||
startPoint = null;
|
||||
}
|
||||
if (afterEnd) {
|
||||
isCollapsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (startPoint && (startPoint.node !== range.sc || startPoint.offset !== range.so)) {
|
||||
needReselect = true;
|
||||
range.sc = startPoint.node;
|
||||
range.so = startPoint.offset;
|
||||
if (isCollapsed) {
|
||||
range.ec = range.sc;
|
||||
range.eo = range.so;
|
||||
}
|
||||
}
|
||||
|
||||
if (startPoint && !isCollapsed) { // mouse selection or key selection with shiftKey
|
||||
var point = endPoint;
|
||||
endPoint = false;
|
||||
|
||||
// if the start point was moved after the end point
|
||||
var toCollapse = !dom.prevPointUntil(point, function (point) {
|
||||
return point.node === range.sc && point.offset === range.so;
|
||||
});
|
||||
|
||||
if (!toCollapse) {
|
||||
// find the first allowed ancestor
|
||||
var commonUnbreakableParent = dom.ancestor(range.sc, function (node) {
|
||||
return !dom.isMedia(node) && self.options.isUnbreakableNode(node);
|
||||
});
|
||||
if (!commonUnbreakableParent) {
|
||||
commonUnbreakableParent = this.editable;
|
||||
}
|
||||
|
||||
var lastCheckedNode;
|
||||
if (point.offset === dom.nodeLength(point.node)) {
|
||||
point = dom.nextPoint(point);
|
||||
}
|
||||
|
||||
// move the end selection to an allowed node in the first allowed ancestor
|
||||
endPoint = dom.prevPointUntil(point, function (point) {
|
||||
if (point.node === range.sc && point.offset === range.so) {
|
||||
return true;
|
||||
}
|
||||
if (lastCheckedNode === point.node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// select the entirety of the unbreakable node
|
||||
if (
|
||||
point.node.tagName && point.offset &&
|
||||
$.contains(commonUnbreakableParent, point.node) &&
|
||||
self.options.isUnbreakableNode(point.node)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var unbreakableParent = dom.ancestor(point.node, function (node) {
|
||||
return !dom.isMedia(node) && self.options.isUnbreakableNode(node);
|
||||
});
|
||||
if (!unbreakableParent) {
|
||||
unbreakableParent = self.editable;
|
||||
}
|
||||
|
||||
if (commonUnbreakableParent !== unbreakableParent) {
|
||||
lastCheckedNode = point.node;
|
||||
return false;
|
||||
}
|
||||
lastCheckedNode = point.node;
|
||||
if (!self.options.isEditableNode(point.node)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(/\S|\u200B|\u00A0/.test(point.node.textContent) ||
|
||||
dom.isMedia(point.node)) &&
|
||||
dom.isVisiblePoint(point)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (dom.isText(point.node)) {
|
||||
lastCheckedNode = point.node;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (!endPoint) {
|
||||
endPoint = range.getStartPoint();
|
||||
}
|
||||
|
||||
if (endPoint.node !== range.ec || endPoint.offset !== range.eo) {
|
||||
needReselect = true;
|
||||
range.ec = endPoint.node;
|
||||
range.eo = endPoint.offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (needReselect) {
|
||||
range = range.select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
}
|
||||
return range;
|
||||
},
|
||||
/**
|
||||
* Apply contentEditable false on all media.
|
||||
*
|
||||
* @param {DOM} [node] default is editable area
|
||||
*/
|
||||
secureArea: function (node) {
|
||||
this.$editable.find('o_not_editable').attr('contentEditable', 'false');
|
||||
|
||||
var medias = (function findMedia(node) {
|
||||
var medias = [];
|
||||
if (node.tagName !== 'IMG' && dom.isMedia(node)) {
|
||||
medias.push(node);
|
||||
} else {
|
||||
$(node.childNodes).each(function () {
|
||||
if (this.tagName) {
|
||||
medias.push.apply(medias, findMedia(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
return medias;
|
||||
})(node || this.editable);
|
||||
$(medias).addClass('o_fake_not_editable').attr('contentEditable', 'false');
|
||||
|
||||
$(medias).each(function () {
|
||||
if (dom.isVideo(this) && !$(this).children('.o_fake_editable').length) {
|
||||
// allow char insertion
|
||||
$(this).prepend('<div class="o_fake_editable o_wysiwyg_to_remove" style="position: absolute;" contentEditable="true"/>');
|
||||
$(this).append('<div class="o_fake_editable o_wysiwyg_to_remove" style="position: absolute;" contentEditable="true"/>');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Trigger a focusnode event when the focus enters another node.
|
||||
*
|
||||
* @param {DOM} node
|
||||
*/
|
||||
_focusNode: function (node) {
|
||||
if (!node.tagName) {
|
||||
node = node.parentNode;
|
||||
}
|
||||
if (this._focusedNode !== node) {
|
||||
this._focusedNode = node;
|
||||
this.context.triggerEvent('focusnode', node);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method called on wysiwyg.range event on the editable: secures the range, refocuses.
|
||||
*/
|
||||
_onRange: function () {
|
||||
var range = this.secureRange();
|
||||
this._focusNode(range.sc);
|
||||
},
|
||||
/**
|
||||
* Method called on mouseup event: secures the range, refocuses.
|
||||
*/
|
||||
_onMouseUp: function () {
|
||||
var range = this.secureRange();
|
||||
this._focusNode(range.ec);
|
||||
},
|
||||
/**
|
||||
* Method called on keydown event: prevents changes to unbreakable nodes.
|
||||
*
|
||||
* @param {SummernoteEvent} se
|
||||
* @param {jQueryEvent} e
|
||||
*/
|
||||
_onKeydown: function (se, e) {
|
||||
if (!e.key || (e.key.length !== 1 && e.keyCode !== 8 && e.keyCode !== 46)) {
|
||||
return;
|
||||
}
|
||||
var range;
|
||||
// for test tour, to trigger Keydown on target (instead of use Wysiwyg.setRange)
|
||||
if (
|
||||
e.target !== this._focusedNode &&
|
||||
(this.editable === e.target || $.contains(this.editable, e.target))
|
||||
) {
|
||||
range = this.context.invoke('editor.createRange');
|
||||
if (!$.contains(e.target, range.sc) && !$.contains(e.target, range.ec)) {
|
||||
range = this.context.invoke('editor.setRange', e.target, 0);
|
||||
range = range.normalize().select();
|
||||
this.context.invoke('editor.saveRange');
|
||||
this._focusNode(range.ec);
|
||||
}
|
||||
}
|
||||
|
||||
// rerange to prevent some edition.
|
||||
// eg: if the user select with arraw and shifKey and keypress an other char
|
||||
range = this.secureRange();
|
||||
var target = range.getStartPoint();
|
||||
|
||||
if (e.keyCode === 8) { // backspace
|
||||
if (!target || this.options.isUnbreakableNode(target.node)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.keyCode === 46) { // delete
|
||||
target = dom.nextPointUntil(dom.nextPoint(target), dom.isVisiblePoint);
|
||||
if (!target || this.options.isUnbreakableNode(target.node)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method called on keyup event: prevents selection of unbreakable nodes.
|
||||
*
|
||||
* @param {SummernoteEvent} se
|
||||
* @param {jQueryEvent} se
|
||||
*/
|
||||
_onKeyup: function (se, e) {
|
||||
if (e.keyCode < 37 || e.keyCode > 40) {
|
||||
return;
|
||||
}
|
||||
var range;
|
||||
if (e.keyCode === 37) { // left
|
||||
range = this.secureRange();
|
||||
this._focusNode(range.sc);
|
||||
} else if (e.keyCode === 39) { // right
|
||||
range = this.secureRange();
|
||||
this._focusNode(range.ec);
|
||||
} else if (e.keyCode === 38) { // up
|
||||
range = this.secureRange();
|
||||
this._focusNode(range.sc);
|
||||
} else { // down
|
||||
range = this.secureRange();
|
||||
this._focusNode(range.ec);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registry.add('UnbreakablePlugin', Unbreakable);
|
||||
|
||||
return Unbreakable;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
odoo.define('web_editor.wysiwyg.plugin.registry', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
|
||||
var WysiwygRegistry = Registry.extend({
|
||||
init: function () {
|
||||
this._super();
|
||||
this._jobs = [];
|
||||
this._xmlDependencies = [];
|
||||
this._def = $.when();
|
||||
},
|
||||
start: function (wysiwyg) {
|
||||
var defs = [this._def];
|
||||
var fn;
|
||||
while ((fn = this._jobs.shift())) {
|
||||
defs.push(fn(wysiwyg));
|
||||
}
|
||||
var xmlPath;
|
||||
while ((xmlPath = this._xmlDependencies.shift())) {
|
||||
defs.push(ajax.loadXML(xmlPath, core.qweb));
|
||||
}
|
||||
if (defs.length !== 1) {
|
||||
this._def = $.when.apply($, defs);
|
||||
}
|
||||
return this._def.state() === 'resolved' ? $.when() : this._def;
|
||||
},
|
||||
addJob: function (job) {
|
||||
this._jobs.push(job);
|
||||
},
|
||||
addXmlDependency: function (xmlDependency) {
|
||||
this._xmlDependencies.push(xmlDependency);
|
||||
},
|
||||
plugins: function () {
|
||||
return _.clone(this.map);
|
||||
},
|
||||
});
|
||||
|
||||
return new WysiwygRegistry();
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
odoo.define('web_editor.wysiwyg.translation', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
$.summernote.lang.odoo = {
|
||||
font: {
|
||||
bold: _t('Bold'),
|
||||
italic: _t('Italic'),
|
||||
underline: _t('Underline'),
|
||||
clear: _t('Remove Font Style'),
|
||||
height: _t('Line Height'),
|
||||
name: _t('Font Family'),
|
||||
strikethrough: _t('Strikethrough'),
|
||||
subscript: _t('Subscript'),
|
||||
superscript: _t('Superscript'),
|
||||
size: _t('Font Size')
|
||||
},
|
||||
image: {
|
||||
image: _t('Picture'),
|
||||
insert: _t('Insert Image'),
|
||||
resizeFull: _t('Resize Full'),
|
||||
resizeHalf: _t('Resize Half'),
|
||||
resizeQuarter: _t('Resize Quarter'),
|
||||
floatLeft: _t('Float Left'),
|
||||
floatRight: _t('Float Right'),
|
||||
floatNone: _t('Float None'),
|
||||
shapeRounded: _t('Shape: Rounded'),
|
||||
shapeCircle: _t('Shape: Circle'),
|
||||
shapeThumbnail: _t('Shape: Thumbnail'),
|
||||
shapeNone: _t('Shape: None'),
|
||||
dragImageHere: _t('Drag image or text here'),
|
||||
dropImage: _t('Drop image or Text'),
|
||||
selectFromFiles: _t('Select from files'),
|
||||
maximumFileSize: _t('Maximum file size'),
|
||||
maximumFileSizeError: _t('Maximum file size exceeded.'),
|
||||
url: _t('Image URL'),
|
||||
remove: _t('Remove Image'),
|
||||
original: _t('Original')
|
||||
},
|
||||
video: {
|
||||
video: _t('Video'),
|
||||
videoLink: _t('Video Link'),
|
||||
insert: _t('Insert Video'),
|
||||
url: _t('Video URL'),
|
||||
providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
|
||||
},
|
||||
link: {
|
||||
link: _t('Link'),
|
||||
insert: _t('Insert Link'),
|
||||
unlink: _t('Unlink'),
|
||||
edit: _t('Edit'),
|
||||
textToDisplay: _t('Text to display'),
|
||||
url: _t('To what URL should this link go?'),
|
||||
openInNewWindow: _t('Open in new window')
|
||||
},
|
||||
table: {
|
||||
table: _t('Table'),
|
||||
addRowAbove: _t('Add row above'),
|
||||
addRowBelow: _t('Add row below'),
|
||||
addColLeft: _t('Add column left'),
|
||||
addColRight: _t('Add column right'),
|
||||
delRow: _t('Delete row'),
|
||||
delCol: _t('Delete column'),
|
||||
delTable: _t('Delete table')
|
||||
},
|
||||
hr: {
|
||||
insert: _t('Insert Horizontal Rule')
|
||||
},
|
||||
style: {
|
||||
style: _t('Style'),
|
||||
p: _t('Normal'),
|
||||
blockquote: _t('Quote'),
|
||||
pre: _t('Code'),
|
||||
h1: _t('Header 1'),
|
||||
h2: _t('Header 2'),
|
||||
h3: _t('Header 3'),
|
||||
h4: _t('Header 4'),
|
||||
h5: _t('Header 5'),
|
||||
h6: _t('Header 6')
|
||||
},
|
||||
lists: {
|
||||
unordered: _t('Unordered list'),
|
||||
ordered: _t('Ordered list')
|
||||
},
|
||||
options: {
|
||||
help: _t('Help'),
|
||||
fullscreen: _t('Full Screen'),
|
||||
codeview: _t('Code View')
|
||||
},
|
||||
paragraph: {
|
||||
paragraph: _t('Paragraph'),
|
||||
outdent: _t('Outdent'),
|
||||
indent: _t('Indent'),
|
||||
left: _t('Align left'),
|
||||
center: _t('Align center'),
|
||||
right: _t('Align right'),
|
||||
justify: _t('Justify full')
|
||||
},
|
||||
color: {
|
||||
recent: _t('Recent Color'),
|
||||
more: _t('More Color'),
|
||||
background: _t('Background Color'),
|
||||
foreground: _t('Foreground Color'),
|
||||
transparent: _t('Transparent'),
|
||||
setTransparent: _t('Set transparent'),
|
||||
reset: _t('Reset'),
|
||||
resetToDefault: _t('Reset to default')
|
||||
},
|
||||
shortcut: {
|
||||
shortcuts: _t('Keyboard shortcuts'),
|
||||
close: _t('Close'),
|
||||
textFormatting: _t('Text formatting'),
|
||||
action: _t('Action'),
|
||||
paragraphFormatting: _t('Paragraph formatting'),
|
||||
documentStyle: _t('Document Style'),
|
||||
extraKeys: _t('Extra keys')
|
||||
},
|
||||
help: {
|
||||
insertParagraph: _t('Insert Paragraph'),
|
||||
undo: _t('Undoes the last command'),
|
||||
redo: _t('Redoes the last command'),
|
||||
tab: _t('Tab'),
|
||||
untab: _t('Outdent (when at the start of a line)'),
|
||||
bold: _t('Set a bold style'),
|
||||
italic: _t('Set a italic style'),
|
||||
underline: _t('Set a underline style'),
|
||||
strikethrough: _t('Set a strikethrough style'),
|
||||
removeFormat: _t('Clean a style'),
|
||||
justifyLeft: _t('Set left align'),
|
||||
justifyCenter: _t('Set center align'),
|
||||
justifyRight: _t('Set right align'),
|
||||
justifyFull: _t('Set full align'),
|
||||
insertUnorderedList: _t('Toggle unordered list'),
|
||||
insertOrderedList: _t('Toggle ordered list'),
|
||||
outdent: _t('Outdent current paragraph'),
|
||||
indent: _t('Indent current paragraph'),
|
||||
formatPara: _t('Change current block\'s format as a paragraph(P tag)'),
|
||||
formatH1: _t('Change current block\'s format as H1'),
|
||||
formatH2: _t('Change current block\'s format as H2'),
|
||||
formatH3: _t('Change current block\'s format as H3'),
|
||||
formatH4: _t('Change current block\'s format as H4'),
|
||||
formatH5: _t('Change current block\'s format as H5'),
|
||||
formatH6: _t('Change current block\'s format as H6'),
|
||||
insertHorizontalRule: _t('Insert horizontal rule'),
|
||||
'linkDialog.show': _t('Show Link Dialog')
|
||||
},
|
||||
history: {
|
||||
undo: _t('Undo'),
|
||||
redo: _t('Redo')
|
||||
},
|
||||
specialChar: {
|
||||
specialChar: _t('SPECIAL CHARACTERS'),
|
||||
select: _t('Select Special characters')
|
||||
}
|
||||
};
|
||||
|
||||
return $.summernote.lang.odoo;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
odoo.define('wysiwyg.widgets.AltDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Dialog = require('wysiwyg.widgets.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* Let users change the alt & title of a media.
|
||||
*/
|
||||
var AltDialog = Dialog.extend({
|
||||
template: 'wysiwyg.widgets.alt',
|
||||
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
|
||||
['/web_editor/static/src/xml/editor.xml']
|
||||
),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, media) {
|
||||
options = options || {};
|
||||
this._super(parent, _.extend({}, {
|
||||
title: _t("Change media description and tooltip")
|
||||
}, options));
|
||||
|
||||
this.trigger_up('getRecordInfo', {
|
||||
recordInfo: options,
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(options, recordInfo);
|
||||
},
|
||||
});
|
||||
|
||||
this.media = media;
|
||||
var allEscQuots = /"/g;
|
||||
this.alt = ($(this.media).attr('alt') || "").replace(allEscQuots, '"');
|
||||
var title = $(this.media).attr('title') || $(this.media).data('original-title') || "";
|
||||
this.tag_title = (title).replace(allEscQuots, '"');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
var alt = this.$('#alt').val();
|
||||
var title = this.$('#title').val();
|
||||
var allNonEscQuots = /"/g;
|
||||
$(this.media).attr('alt', alt ? alt.replace(allNonEscQuots, """) : null)
|
||||
.attr('title', title ? title.replace(allNonEscQuots, """) : null);
|
||||
|
||||
this.trigger('saved', {
|
||||
media: this.media,
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return AltDialog;
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
odoo.define('web.colorpicker', function (require) {
|
||||
odoo.define('wysiwyg.widgets.ColorpickerDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
@@ -7,11 +7,11 @@ var Dialog = require('web.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var Colorpicker = Dialog.extend({
|
||||
var ColorpickerDialog = Dialog.extend({
|
||||
xmlDependencies: (Dialog.prototype.xmlDependencies || [])
|
||||
.concat(['/web/static/src/xml/colorpicker.xml']),
|
||||
.concat(['/web_editor/static/src/xml/wysiwyg_colorpicker.xml']),
|
||||
|
||||
template: 'web.colorpicker',
|
||||
template: 'wysiwyg.widgets.ColorpickerDialog',
|
||||
events: _.extend({}, Dialog.prototype.events || {}, {
|
||||
'mousedown .o_color_pick_area': '_onMouseDownPicker',
|
||||
'mousedown .o_color_slider': '_onMouseDownSlider',
|
||||
@@ -37,6 +37,13 @@ var Colorpicker = Dialog.extend({
|
||||
],
|
||||
}, options));
|
||||
|
||||
this.trigger_up('getRecordInfo', {
|
||||
recordInfo: options,
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(options, recordInfo);
|
||||
},
|
||||
});
|
||||
|
||||
this.pickerFlag = false;
|
||||
this.sliderFlag = false;
|
||||
this.opacitySliderFlag = false;
|
||||
@@ -69,15 +76,9 @@ var Colorpicker = Dialog.extend({
|
||||
this.$opacitySliderPointer = this.$('.o_opacity_pointer');
|
||||
|
||||
var defaultColor = this.options.defaultColor || '#FF0000';
|
||||
var rgba = defaultColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
|
||||
var rgba = ColorpickerDialog.convertColorToRgba(defaultColor);
|
||||
if (rgba) {
|
||||
if (rgba[4] === undefined) {
|
||||
rgba[4] = 1;
|
||||
}
|
||||
this._updateRgba(parseInt(rgba[1]), parseInt(rgba[2]), parseInt(rgba[3]), Math.round(parseFloat(rgba[4] * 100)));
|
||||
} else {
|
||||
this._updateHex(defaultColor);
|
||||
this._updateOpacity(100);
|
||||
this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity);
|
||||
}
|
||||
this.opened().then(this._updateUI.bind(this));
|
||||
|
||||
@@ -140,14 +141,14 @@ var Colorpicker = Dialog.extend({
|
||||
* @param {string} hex - hexadecimal code
|
||||
*/
|
||||
_updateHex: function (hex) {
|
||||
var rgb = Colorpicker.prototype.convertHexToRgb(hex);
|
||||
var rgb = ColorpickerDialog.convertHexToRgba(hex);
|
||||
if (!rgb) {
|
||||
return;
|
||||
}
|
||||
_.extend(this.colorComponents,
|
||||
{hex: hex},
|
||||
rgb,
|
||||
Colorpicker.prototype.convertRgbToHsl(rgb.red, rgb.green, rgb.blue)
|
||||
ColorpickerDialog.convertRgbToHsl(rgb.red, rgb.green, rgb.blue)
|
||||
);
|
||||
this._updateCssColor();
|
||||
},
|
||||
@@ -161,7 +162,7 @@ var Colorpicker = Dialog.extend({
|
||||
* @param {integer} [a]
|
||||
*/
|
||||
_updateRgba: function (r, g, b, a) {
|
||||
var hex = Colorpicker.prototype.convertRgbToHex(r, g, b);
|
||||
var hex = ColorpickerDialog.convertRgbToHex(r, g, b);
|
||||
if (!hex) {
|
||||
return;
|
||||
}
|
||||
@@ -169,7 +170,7 @@ var Colorpicker = Dialog.extend({
|
||||
{red: r, green: g, blue: b},
|
||||
a === undefined ? {} : {opacity: a},
|
||||
hex,
|
||||
Colorpicker.prototype.convertRgbToHsl(r, g, b)
|
||||
ColorpickerDialog.convertRgbToHsl(r, g, b)
|
||||
);
|
||||
this._updateCssColor();
|
||||
},
|
||||
@@ -182,14 +183,14 @@ var Colorpicker = Dialog.extend({
|
||||
* @param {integer} l
|
||||
*/
|
||||
_updateHsl: function (h, s, l) {
|
||||
var rgb = Colorpicker.prototype.convertHslToRgb(h, s, l);
|
||||
var rgb = ColorpickerDialog.convertHslToRgb(h, s, l);
|
||||
if (!rgb) {
|
||||
return;
|
||||
}
|
||||
_.extend(this.colorComponents,
|
||||
{hue: h, saturation: s, lightness: l},
|
||||
rgb,
|
||||
Colorpicker.prototype.convertRgbToHex(rgb.red, rgb.green, rgb.blue)
|
||||
ColorpickerDialog.convertRgbToHex(rgb.red, rgb.green, rgb.blue)
|
||||
);
|
||||
this._updateCssColor();
|
||||
},
|
||||
@@ -214,17 +215,8 @@ var Colorpicker = Dialog.extend({
|
||||
* @private
|
||||
*/
|
||||
_updateCssColor: function () {
|
||||
var cssColor = this.colorComponents.hex;
|
||||
if (this.colorComponents.opacity !== 100) {
|
||||
cssColor = _.str.sprintf('rgba(%s, %s, %s, %s)',
|
||||
this.colorComponents.red,
|
||||
this.colorComponents.green,
|
||||
this.colorComponents.blue,
|
||||
this.colorComponents.opacity / 100
|
||||
);
|
||||
}
|
||||
_.extend(this.colorComponents,
|
||||
{cssColor: cssColor}
|
||||
{cssColor: ColorpickerDialog.formatColor(this.colorComponents)}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -358,140 +350,214 @@ var Colorpicker = Dialog.extend({
|
||||
_onFinalPick: function () {
|
||||
this.trigger_up('colorpicker:saved', this.colorComponents);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Static
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Converts Hexadecimal code to RGB.
|
||||
*
|
||||
* @static
|
||||
* @param {string} hex - hexadecimal code
|
||||
* @returns {Object|false} contains red, green and blue
|
||||
*/
|
||||
convertHexToRgb: function (hex) {
|
||||
if (!/^#[0-9A-F]{6}$/i.test(hex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
red: parseInt(hex.substr(1, 2), 16),
|
||||
green: parseInt(hex.substr(3, 2), 16),
|
||||
blue: parseInt(hex.substr(5, 2), 16),
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Converts RGB color to HSL.
|
||||
*
|
||||
* @static
|
||||
* @param {integer} r
|
||||
* @param {integer} g
|
||||
* @param {integer} b
|
||||
* @returns {Object|false} contains hue, saturation and lightness
|
||||
*/
|
||||
convertRgbToHsl: function (r, g, b) {
|
||||
if (typeof(r) !== 'number' || isNaN(r) || r < 0 || r > 255 ||
|
||||
typeof(g) !== 'number' || isNaN(g) || g < 0 || g > 255 ||
|
||||
typeof(b) !== 'number' || isNaN(b) || b < 0 || b > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var red = r / 255;
|
||||
var green = g / 255;
|
||||
var blue = b / 255;
|
||||
var maxColor = Math.max(red, green, blue);
|
||||
var minColor = Math.min(red, green, blue);
|
||||
var delta = maxColor - minColor;
|
||||
var hue = 0;
|
||||
var saturation = 0;
|
||||
var lightness = (maxColor + minColor) / 2;
|
||||
if (delta) {
|
||||
if (maxColor === red) {
|
||||
hue = (green - blue) / delta;
|
||||
}
|
||||
if (maxColor === green) {
|
||||
hue = 2 + (blue - red) / delta;
|
||||
}
|
||||
if (maxColor === blue) {
|
||||
hue = 4 + (red - green) / delta;
|
||||
}
|
||||
if (maxColor) {
|
||||
saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
||||
}
|
||||
}
|
||||
hue = 60 * hue | 0;
|
||||
return {
|
||||
hue: hue < 0 ? hue += 360 : hue,
|
||||
saturation: (saturation * 100) | 0,
|
||||
lightness: (lightness * 100) | 0,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Converts HSL color to RGB.
|
||||
*
|
||||
* @static
|
||||
* @param {integer} h
|
||||
* @param {integer} s
|
||||
* @param {integer} l
|
||||
* @returns {Object|false} contains red, green and blue
|
||||
*/
|
||||
convertHslToRgb: function (h, s, l) {
|
||||
if (typeof(h) !== 'number' || isNaN(h) || h < 0 || h > 360 ||
|
||||
typeof(s) !== 'number' || isNaN(s) || s < 0 || s > 100 ||
|
||||
typeof(l) !== 'number' || isNaN(l) || l < 0 || l > 100) {
|
||||
return false;
|
||||
}
|
||||
var huePrime = h / 60;
|
||||
var saturation = s / 100;
|
||||
var lightness = l / 100;
|
||||
var chroma = saturation * (1 - Math.abs(2 * lightness - 1));
|
||||
var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1));
|
||||
var lightnessAdjustment = lightness - chroma/2;
|
||||
var precision = 255;
|
||||
chroma = (chroma + lightnessAdjustment) * precision | 0;
|
||||
secondComponent = (secondComponent + lightnessAdjustment) * precision | 0;
|
||||
lightnessAdjustment = lightnessAdjustment * precision | 0;
|
||||
if (huePrime >= 0 && huePrime < 1) {
|
||||
return {red: chroma, green: secondComponent, blue: lightnessAdjustment};
|
||||
}
|
||||
if (huePrime >= 1 && huePrime < 2) {
|
||||
return {red: secondComponent, green: chroma, blue: lightnessAdjustment};
|
||||
}
|
||||
if (huePrime >= 2 && huePrime < 3) {
|
||||
return {red: lightnessAdjustment, green: chroma, blue: secondComponent};
|
||||
}
|
||||
if (huePrime >= 3 && huePrime < 4) {
|
||||
return {red: lightnessAdjustment, green: secondComponent, blue: chroma};
|
||||
}
|
||||
if (huePrime >= 4 && huePrime < 5) {
|
||||
return {red: secondComponent, green: lightnessAdjustment, blue: chroma};
|
||||
}
|
||||
if (huePrime >= 5 && huePrime <= 6) {
|
||||
return {red: chroma, green: lightnessAdjustment, blue: secondComponent};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Converts RGB color to Hexadecimal code.
|
||||
*
|
||||
* @static
|
||||
* @param {integer} r
|
||||
* @param {integer} g
|
||||
* @param {integer} b
|
||||
* @returns {Object|false} contains hexadecimal code
|
||||
*/
|
||||
convertRgbToHex: function (r, g, b) {
|
||||
if (typeof(r) !== 'number' || isNaN(r) || r < 0 || r > 255 ||
|
||||
typeof(g) !== 'number' || isNaN(g) || g < 0 || g > 255 ||
|
||||
typeof(b) !== 'number' || isNaN(b) || b < 0 || b > 255) {
|
||||
return false;
|
||||
}
|
||||
var red = r < 16 ? '0' + r.toString(16) : r.toString(16);
|
||||
var green = g < 16 ? '0' + g.toString(16) : g.toString(16);
|
||||
var blue = b < 16 ? '0' + b.toString(16) : b.toString(16);
|
||||
return {hex: _.str.sprintf('#%s%s%s', red, green, blue)};
|
||||
},
|
||||
});
|
||||
|
||||
return Colorpicker;
|
||||
//--------------------------------------------------------------------------
|
||||
// Static
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Converts any color to string RGBA.
|
||||
*
|
||||
* @static
|
||||
* @param {Object|string} color
|
||||
* @returns {string} rgba (red, green, blue, opacity)
|
||||
*/
|
||||
ColorpickerDialog.formatColor = function (color) {
|
||||
if (typeof color === 'string') {
|
||||
color = ColorpickerDialog.convertColorToRgba(color);
|
||||
}
|
||||
if (!color) {
|
||||
return '';
|
||||
}
|
||||
if (color.opacity === 100) {
|
||||
return ColorpickerDialog.convertRgbToHex(color.red, color.green, color.blue).hex;
|
||||
}
|
||||
return _.str.sprintf('rgba(%s, %s, %s, %s)',
|
||||
color.red,
|
||||
color.green,
|
||||
color.blue,
|
||||
color.opacity / 100
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Converts any color to code to RGBA.
|
||||
*
|
||||
* @static
|
||||
* @param {string} color
|
||||
* @returns {Object|false} contains red, green, blue, opacity
|
||||
*/
|
||||
ColorpickerDialog.convertColorToRgba = function (color) {
|
||||
var rgba = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
|
||||
if (rgba) {
|
||||
if (rgba[4] === undefined) {
|
||||
rgba[4] = 100;
|
||||
}
|
||||
return {
|
||||
red: parseInt(rgba[1]),
|
||||
green: parseInt(rgba[2]),
|
||||
blue: parseInt(rgba[3]),
|
||||
opacity: parseInt(rgba[4]),
|
||||
};
|
||||
} else {
|
||||
return ColorpickerDialog.convertHexToRgba(color);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Converts Hexadecimal code to RGB.
|
||||
*
|
||||
* @static
|
||||
* @param {string} hex - hexadecimal code
|
||||
* @returns {Object|false} contains red, green and blue
|
||||
*/
|
||||
ColorpickerDialog.convertHexToRgba = function (hex) {
|
||||
if (!/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(hex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
red: parseInt(hex.substr(1, 2), 16),
|
||||
green: parseInt(hex.substr(3, 2), 16),
|
||||
blue: parseInt(hex.substr(5, 2), 16),
|
||||
opacity: hex.length === 9 ? parseInt(hex.substr(7, 2), 16) : 100,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Converts RGB color to HSL.
|
||||
*
|
||||
* @static
|
||||
* @param {integer} r
|
||||
* @param {integer} g
|
||||
* @param {integer} b
|
||||
* @returns {Object|false} contains hue, saturation and lightness
|
||||
*/
|
||||
ColorpickerDialog.convertRgbToHsl = function (r, g, b) {
|
||||
if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 ||
|
||||
typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 ||
|
||||
typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var red = r / 255;
|
||||
var green = g / 255;
|
||||
var blue = b / 255;
|
||||
var maxColor = Math.max(red, green, blue);
|
||||
var minColor = Math.min(red, green, blue);
|
||||
var delta = maxColor - minColor;
|
||||
var hue = 0;
|
||||
var saturation = 0;
|
||||
var lightness = (maxColor + minColor) / 2;
|
||||
if (delta) {
|
||||
if (maxColor === red) {
|
||||
hue = (green - blue) / delta;
|
||||
}
|
||||
if (maxColor === green) {
|
||||
hue = 2 + (blue - red) / delta;
|
||||
}
|
||||
if (maxColor === blue) {
|
||||
hue = 4 + (red - green) / delta;
|
||||
}
|
||||
if (maxColor) {
|
||||
saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
||||
}
|
||||
}
|
||||
hue = 60 * hue | 0;
|
||||
return {
|
||||
hue: hue < 0 ? hue += 360 : hue,
|
||||
saturation: (saturation * 100) | 0,
|
||||
lightness: (lightness * 100) | 0,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Converts HSL color to RGB.
|
||||
*
|
||||
* @static
|
||||
* @param {integer} h
|
||||
* @param {integer} s
|
||||
* @param {integer} l
|
||||
* @returns {Object|false} contains red, green and blue
|
||||
*/
|
||||
ColorpickerDialog.convertHslToRgb = function (h, s, l) {
|
||||
if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 ||
|
||||
typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 ||
|
||||
typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) {
|
||||
return false;
|
||||
}
|
||||
var huePrime = h / 60;
|
||||
var saturation = s / 100;
|
||||
var lightness = l / 100;
|
||||
var chroma = saturation * (1 - Math.abs(2 * lightness - 1));
|
||||
var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1));
|
||||
var lightnessAdjustment = lightness - chroma / 2;
|
||||
var precision = 255;
|
||||
chroma = (chroma + lightnessAdjustment) * precision | 0;
|
||||
secondComponent = (secondComponent + lightnessAdjustment) * precision | 0;
|
||||
lightnessAdjustment = lightnessAdjustment * precision | 0;
|
||||
if (huePrime >= 0 && huePrime < 1) {
|
||||
return {
|
||||
red: chroma,
|
||||
green: secondComponent,
|
||||
blue: lightnessAdjustment,
|
||||
};
|
||||
}
|
||||
if (huePrime >= 1 && huePrime < 2) {
|
||||
return {
|
||||
red: secondComponent,
|
||||
green: chroma,
|
||||
blue: lightnessAdjustment,
|
||||
};
|
||||
}
|
||||
if (huePrime >= 2 && huePrime < 3) {
|
||||
return {
|
||||
red: lightnessAdjustment,
|
||||
green: chroma,
|
||||
blue: secondComponent,
|
||||
};
|
||||
}
|
||||
if (huePrime >= 3 && huePrime < 4) {
|
||||
return {
|
||||
red: lightnessAdjustment,
|
||||
green: secondComponent,
|
||||
blue: chroma,
|
||||
};
|
||||
}
|
||||
if (huePrime >= 4 && huePrime < 5) {
|
||||
return {
|
||||
red: secondComponent,
|
||||
green: lightnessAdjustment,
|
||||
blue: chroma,
|
||||
};
|
||||
}
|
||||
if (huePrime >= 5 && huePrime <= 6) {
|
||||
return {
|
||||
red: chroma,
|
||||
green: lightnessAdjustment,
|
||||
blue: secondComponent,
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Converts RGB color to Hexadecimal code.
|
||||
*
|
||||
* @static
|
||||
* @param {integer} r
|
||||
* @param {integer} g
|
||||
* @param {integer} b
|
||||
* @returns {Object|false} contains hexadecimal code
|
||||
*/
|
||||
ColorpickerDialog.convertRgbToHex = function (r, g, b) {
|
||||
if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 ||
|
||||
typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 ||
|
||||
typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) {
|
||||
return false;
|
||||
}
|
||||
var red = r < 16 ? '0' + r.toString(16) : r.toString(16);
|
||||
var green = g < 16 ? '0' + g.toString(16) : g.toString(16);
|
||||
var blue = b < 16 ? '0' + b.toString(16) : b.toString(16);
|
||||
return {
|
||||
hex: _.str.sprintf('#%s%s%s', red, green, blue)
|
||||
};
|
||||
};
|
||||
|
||||
return ColorpickerDialog;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
odoo.define('wysiwyg.widgets.CropImageDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Dialog = require('wysiwyg.widgets.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* CropImageDialog widget. Let users crop an image.
|
||||
*/
|
||||
var CropImageDialog = Dialog.extend({
|
||||
template: 'wysiwyg.widgets.crop_image',
|
||||
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
|
||||
['/web_editor/static/src/xml/editor.xml']
|
||||
),
|
||||
jsLibs: [
|
||||
'/web_editor/static/lib/cropper/js/cropper.js',
|
||||
],
|
||||
cssLibs: [
|
||||
'/web_editor/static/lib/cropper/css/cropper.css',
|
||||
],
|
||||
events: _.extend({}, Dialog.prototype.events, {
|
||||
'click .o_crop_options [data-event]': '_onCropOptionClick',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, media) {
|
||||
var self = this;
|
||||
this.media = media;
|
||||
this.$media = $(this.media);
|
||||
var src = this.$media.attr('src').split('?')[0];
|
||||
this.aspectRatioList = [
|
||||
[_t("Free"), '0/0', 0],
|
||||
["16:9", '16/9', 16 / 9],
|
||||
["4:3", '4/3', 4 / 3],
|
||||
["1:1", '1/1', 1],
|
||||
["2:3", '2/3', 2 / 3],
|
||||
];
|
||||
this.imageData = {
|
||||
imageSrc: src,
|
||||
originalSrc: this.$media.data('crop:originalSrc') || src, // the original src for cropped DB images will be fetched later
|
||||
mimetype: this.$media.data('crop:mimetype') || (_.str.endsWith(src, '.png') ? 'image/png' : 'image/jpeg'), // the mimetype for DB images will be fetched later
|
||||
aspectRatio: this.$media.data('aspectRatio') || this.aspectRatioList[0][1],
|
||||
isExternalImage: src.substr(0, 5) !== 'data:' && src[0] !== '/' && src.indexOf(window.location.host) < 0,
|
||||
};
|
||||
this.options = _.extend({
|
||||
title: _t("Crop Image"),
|
||||
buttons: this.imageData.isExternalImage ? [{
|
||||
text: _t("Close"),
|
||||
close: true,
|
||||
}] : [{
|
||||
text: _t("Save"),
|
||||
classes: 'btn-primary',
|
||||
click: this.save,
|
||||
}, {
|
||||
text: _t("Discard"),
|
||||
close: true,
|
||||
}],
|
||||
}, options || {});
|
||||
this._super(parent, this.options);
|
||||
this.trigger_up('getRecordInfo', _.extend(this.options, {
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(self.options, recordInfo);
|
||||
},
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
var def = this._super.apply(this, arguments);
|
||||
if (this.imageData.isExternalImage) {
|
||||
return def;
|
||||
}
|
||||
|
||||
var defs = [def];
|
||||
var params = {};
|
||||
var isDBImage = false;
|
||||
var matchImageID = this.imageData.imageSrc.match(/^\/web\/image\/(\d+)/);
|
||||
if (matchImageID) {
|
||||
params.image_id = parseInt(matchImageID[1]);
|
||||
isDBImage = true;
|
||||
} else {
|
||||
var matchXmlID = this.imageData.imageSrc.match(/^\/web\/image\/([^/?]+)/);
|
||||
if (matchXmlID) {
|
||||
params.xml_id = matchXmlID[1];
|
||||
isDBImage = true;
|
||||
}
|
||||
}
|
||||
if (isDBImage) {
|
||||
defs.push(this._rpc({
|
||||
route: '/web_editor/get_image_info',
|
||||
params: params,
|
||||
}).then(function (res) {
|
||||
_.extend(self.imageData, res);
|
||||
}));
|
||||
}
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$cropperImage = this.$('.o_cropper_image');
|
||||
if (this.$cropperImage.length) {
|
||||
var data = this.$media.data();
|
||||
var ratio = 0;
|
||||
for (var i = 0; i < this.aspectRatioList.length; i++) {
|
||||
if (this.aspectRatioList[i][1] === data.aspectRatio) {
|
||||
ratio = this.aspectRatioList[i][2];
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.$cropperImage.cropper({
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
aspectRatio: ratio,
|
||||
data: _.pick(data, 'x', 'y', 'width', 'height', 'rotate', 'scaleX', 'scaleY')
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this.$cropperImage.length) {
|
||||
this.$cropperImage.cropper('destroy');
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Updates the DOM image with cropped data and associates required
|
||||
* information for a potential future save (where required cropped data
|
||||
* attachments will be created).
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
var self = this;
|
||||
var cropperData = this.$cropperImage.cropper('getData');
|
||||
|
||||
// Mark the media for later creation of required cropped attachments...
|
||||
this.$media.addClass('o_cropped_img_to_save');
|
||||
|
||||
// ... and attach required data
|
||||
this.$media.data('crop:resModel', this.options.res_model);
|
||||
this.$media.data('crop:resID', this.options.res_id);
|
||||
this.$media.data('crop:id', this.imageData.id);
|
||||
this.$media.data('crop:mimetype', this.imageData.mimetype);
|
||||
this.$media.data('crop:originalSrc', this.imageData.originalSrc);
|
||||
|
||||
// Mark the media with the cropping information which is required for
|
||||
// a future crop edition
|
||||
this.$media
|
||||
.attr('data-aspect-ratio', this.imageData.aspectRatio)
|
||||
.data('aspectRatio', this.imageData.aspectRatio);
|
||||
_.each(cropperData, function (value, key) {
|
||||
key = _.str.dasherize(key);
|
||||
self.$media.attr('data-' + key, value);
|
||||
self.$media.data(key, value);
|
||||
});
|
||||
|
||||
// Update the media with base64 source for preview before saving
|
||||
var canvas = this.$cropperImage.cropper('getCroppedCanvas', {
|
||||
width: cropperData.width,
|
||||
height: cropperData.height,
|
||||
});
|
||||
this.$media.attr('src', canvas.toDataURL(this.imageData.mimetype));
|
||||
|
||||
this.trigger('saved', {
|
||||
media: this.$media[0],
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when a crop option is clicked -> change the crop area accordingly.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onCropOptionClick: function (ev) {
|
||||
var $option = $(ev.currentTarget);
|
||||
var opt = $option.data('event');
|
||||
var value = $option.data('value');
|
||||
switch (opt) {
|
||||
case 'ratio':
|
||||
this.$cropperImage.cropper('reset');
|
||||
this.imageData.aspectRatio = $option.data('label');
|
||||
this.$cropperImage.cropper('setAspectRatio', value);
|
||||
break;
|
||||
case 'zoom':
|
||||
case 'rotate':
|
||||
case 'reset':
|
||||
this.$cropperImage.cropper(opt, value);
|
||||
break;
|
||||
case 'flip':
|
||||
var direction = value === 'horizontal' ? 'x' : 'y';
|
||||
var scaleAngle = -$option.data(direction);
|
||||
$option.data(direction, scaleAngle);
|
||||
this.$cropperImage.cropper('scale' + direction.toUpperCase(), scaleAngle);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return CropImageDialog;
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
odoo.define('wysiwyg.widgets.Dialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* Extend Dialog class to handle save/cancel of edition components.
|
||||
*/
|
||||
var SummernoteDialog = Dialog.extend({
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
this.options = options || {};
|
||||
this._super(parent, _.extend({}, {
|
||||
buttons: [{
|
||||
text: this.options.save_text || _t("Save"),
|
||||
classes: 'btn-primary',
|
||||
click: this.save,
|
||||
},
|
||||
{
|
||||
text: _t("Discard"),
|
||||
close: true,
|
||||
}
|
||||
]
|
||||
}, this.options));
|
||||
|
||||
this.destroyAction = 'cancel';
|
||||
|
||||
var self = this;
|
||||
this.opened(function () {
|
||||
self.$('input:first').focus();
|
||||
self.$el.closest('.modal').on('hidden.bs.modal', self.options.onClose);
|
||||
});
|
||||
this.on('closed', this, function () {
|
||||
this.trigger(this.destroyAction, this.final_data || null);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the dialog is saved. Set the destroy action type to "save"
|
||||
* and should set the final_data variable correctly before closing.
|
||||
*/
|
||||
save: function () {
|
||||
this.destroyAction = "save";
|
||||
this.close();
|
||||
},
|
||||
});
|
||||
|
||||
return SummernoteDialog;
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
odoo.define('wysiwyg.widgets.LinkDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Dialog = require('wysiwyg.widgets.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
*/
|
||||
var LinkDialog = Dialog.extend({
|
||||
template: 'wysiwyg.widgets.link',
|
||||
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
|
||||
['/web_editor/static/src/xml/editor.xml']
|
||||
),
|
||||
events: _.extend({}, Dialog.prototype.events || {}, {
|
||||
'input': '_onAnyChange',
|
||||
'change': '_onAnyChange',
|
||||
'input input[name="url"]': '_onURLInput',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, linkInfo) {
|
||||
var self = this;
|
||||
this.options = options || {};
|
||||
|
||||
this._super(parent, _.extend({
|
||||
title: _t("Link to"),
|
||||
}, this.options));
|
||||
|
||||
this.trigger_up('getRecordInfo', {
|
||||
recordInfo: this.options,
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(self.options, recordInfo);
|
||||
},
|
||||
});
|
||||
|
||||
this.data = linkInfo || {};
|
||||
this.needLabel = linkInfo.needLabel;
|
||||
this.data.iniClassName = linkInfo.className;
|
||||
var allBtnClassSuffixes = /(^|\s+)btn(-[a-z0-9_-]*)?/gi;
|
||||
var allBtnShapes = /\s*(rounded-circle|flat)\s*/gi;
|
||||
this.data.className = linkInfo.className
|
||||
.replace(allBtnClassSuffixes, ' ')
|
||||
.replace(allBtnShapes, ' ');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this.$('input.link-style').prop('checked', false).first().prop('checked', true);
|
||||
if (this.data.iniClassName) {
|
||||
this.$('input[name="link_style_color"], select[name="link_style_size"] > option, select[name="link_style_shape"] > option').each(function () {
|
||||
var $option = $(this);
|
||||
if ($option.val() && self.data.iniClassName.match(new RegExp('(^|btn-| |btn-outline-)' + $option.val()))) {
|
||||
if ($option.is("input")) {
|
||||
$option.prop("checked", true);
|
||||
} else {
|
||||
$option.parent().find('option').removeAttr('selected').removeProp('selected');
|
||||
$option.parent().val($option.val());
|
||||
$option.attr('selected', 'selected').prop('selected', 'selected');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.data.url) {
|
||||
var match = /mailto:(.+)/.exec(this.data.url);
|
||||
this.$('input[name="url"]').val(match ? match[1] : this.data.url);
|
||||
}
|
||||
|
||||
// Hide the duplicate color buttons (most of the times, primary = alpha
|
||||
// and secondary = beta for example but this may depend on the theme)
|
||||
this.opened().then(function () {
|
||||
var colors = [];
|
||||
_.each(self.$('.o_link_dialog_color .o_btn_preview'), function (btn) {
|
||||
var $btn = $(btn);
|
||||
var color = $btn.css('background-color');
|
||||
if (_.contains(colors, color)) {
|
||||
$btn.hide(); // Not remove to be able to edit buttons with those styles
|
||||
} else {
|
||||
colors.push(color);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._adaptPreview();
|
||||
|
||||
this.$('input:visible:first').focus();
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
var data = this._getData();
|
||||
if (data === null) {
|
||||
var $url = this.$('input[name="url"]');
|
||||
$url.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
|
||||
$url.focus();
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
this.data.text = data.label;
|
||||
this.data.url = data.url;
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
var allBtnTypes = /(^|[ ])(btn-secondary|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi;
|
||||
this.data.className = data.classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '');
|
||||
if (data.classes.replace(allBtnTypes, ' ')) {
|
||||
this.data.style = {
|
||||
'background-color': '',
|
||||
'color': '',
|
||||
};
|
||||
}
|
||||
this.data.isNewWindow = data.isNewWindow;
|
||||
this.final_data = this.data;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adapt the link preview to changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_adaptPreview: function () {
|
||||
var $preview = this.$("#link-preview");
|
||||
var data = this._getData();
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
var floatClass = /float-\w+/;
|
||||
$preview.attr({
|
||||
target: data.isNewWindow ? '_blank' : '',
|
||||
href: data.url && data.url.length ? data.url : '#',
|
||||
class: data.classes.replace(floatClass, '') + ' o_btn_preview',
|
||||
}).html((data.label && data.label.length) ? data.label : data.url);
|
||||
},
|
||||
/**
|
||||
* Get the link's data (url, label and styles).
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} {label: String, url: String, classes: String, isNewWindow: Boolean}
|
||||
*/
|
||||
_getData: function () {
|
||||
var $url = this.$('input[name="url"]');
|
||||
var url = $url.val();
|
||||
var label = this.$('input[name="label"]').val() || url;
|
||||
|
||||
if (label && this.data.images) {
|
||||
for (var i = 0; i < this.data.images.length; i++) {
|
||||
label = label.replace('<', "<").replace('>', ">").replace(/\[IMG\]/, this.data.images[i].outerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
if ($url.prop('required') && (!url || !$url[0].checkValidity())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var style = this.$('input[name="link_style_color"]:checked').val() || '';
|
||||
var shape = this.$('select[name="link_style_shape"] option:selected').val() || '';
|
||||
var size = this.$('select[name="link_style_size"] option:selected').val() || '';
|
||||
var shapes = shape.split(',');
|
||||
var outline = shapes[0] === 'outline';
|
||||
shape = shapes.slice(outline ? 1 : 0).join(' ');
|
||||
var classes = (this.data.className || '') +
|
||||
(style ? (' btn btn-' + (outline ? 'outline-' : '') + style) : '') +
|
||||
(shape ? (' ' + shape) : '') +
|
||||
(size ? (' btn-' + size) : '');
|
||||
var isNewWindow = this.$('input[name="is_new_window"]').prop('checked');
|
||||
|
||||
if (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0 && !url.match(/^http[s]?/i)) {
|
||||
url = ('mailto:' + url);
|
||||
}
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
return {
|
||||
label: label,
|
||||
url: url,
|
||||
classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, ''),
|
||||
isNewWindow: isNewWindow,
|
||||
};
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAnyChange: function () {
|
||||
this._adaptPreview();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onURLInput: function (ev) {
|
||||
$(ev.currentTarget).closest('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
|
||||
var isLink = $(ev.currentTarget).val().indexOf('@') < 0;
|
||||
this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('d-none', !isLink);
|
||||
},
|
||||
});
|
||||
|
||||
return LinkDialog;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
odoo.define('wysiwyg.widgets.MediaDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var MediaModules = require('wysiwyg.widgets.media');
|
||||
var Dialog = require('wysiwyg.widgets.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* MediaDialog widget. Lets users change a media, including uploading a
|
||||
* new image, font awsome or video and can change a media into an other
|
||||
* media.
|
||||
*/
|
||||
var MediaDialog = Dialog.extend({
|
||||
template: 'wysiwyg.widgets.media',
|
||||
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
|
||||
['/web_editor/static/src/xml/editor.xml']
|
||||
),
|
||||
events: _.extend({}, Dialog.prototype.events, {
|
||||
'click a[data-toggle="tab"]': '_onTabChange',
|
||||
}),
|
||||
custom_events: _.extend({}, Dialog.prototype.custom_events || {}, {
|
||||
save_request: '_onSaveRequest',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, media) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
||||
this._super(parent, _.extend({}, {
|
||||
title: _t("Select a Media"),
|
||||
save_text: _t("Add"),
|
||||
}, options));
|
||||
|
||||
this.trigger_up('getRecordInfo', {
|
||||
recordInfo: options,
|
||||
type: 'media',
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(options, recordInfo);
|
||||
},
|
||||
});
|
||||
|
||||
this.media = media;
|
||||
this.$media = $(media);
|
||||
|
||||
this.multiImages = options.multiImages;
|
||||
var onlyImages = options.onlyImages || this.multiImages || (this.media && (this.$media.parent().data('oeField') === 'image' || this.$media.parent().data('oeType') === 'image'));
|
||||
this.noImages = options.noImages;
|
||||
this.noDocuments = onlyImages || options.noDocuments;
|
||||
this.noIcons = onlyImages || options.noIcons;
|
||||
this.noVideos = onlyImages || options.noVideos;
|
||||
|
||||
if (!this.noDocuments) {
|
||||
this.documentDialog = new MediaModules.ImageWidget(this, this.media, _.extend({}, options, {
|
||||
document: true,
|
||||
}));
|
||||
this.documentDialog.tabToShow = 'document';
|
||||
}
|
||||
if (!this.noIcons) {
|
||||
this.iconDialog = new MediaModules.IconWidget(this, this.media, options);
|
||||
this.iconDialog.tabToShow = 'icon';
|
||||
}
|
||||
if (!this.noVideos) {
|
||||
this.videoDialog = new MediaModules.VideoWidget(this, this.media, options);
|
||||
this.videoDialog.tabToShow = 'video';
|
||||
}
|
||||
if (!this.noImages) {
|
||||
this.imageDialog = new MediaModules.ImageWidget(this, this.media, options);
|
||||
this.imageDialog.tabToShow = 'image';
|
||||
}
|
||||
|
||||
this.active = this.imageDialog || this.documentDialog || this.iconDialog || this.videoDialog;
|
||||
if (this.imageDialog && this.$media.is('img')) {
|
||||
this.active = this.imageDialog;
|
||||
} else if (this.documentDialog && this.$media.is('a.o_image')) {
|
||||
this.active = this.documentDialog;
|
||||
} else if (this.videoDialog && this.$media.hasClass('media_iframe_video')) {
|
||||
this.active = this.videoDialog;
|
||||
} else if (this.videoDialog && this.$media.parent().hasClass('media_iframe_video')) {
|
||||
this.$media = this.$media.parent();
|
||||
this.media = this.$media[0];
|
||||
this.active = this.videoDialog;
|
||||
} else if (this.iconDialog && this.$media.is('span, i')) {
|
||||
this.active = this.iconDialog;
|
||||
}
|
||||
|
||||
this.opened(function () {
|
||||
self.$('[href="#editor-media-' + self.active.tabToShow + '"]').tab('show');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
this.$modal.addClass('note-image-dialog');
|
||||
this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
|
||||
|
||||
if (this.imageDialog) {
|
||||
this.imageDialog.clear();
|
||||
}
|
||||
if (this.documentDialog) {
|
||||
this.documentDialog.clear();
|
||||
}
|
||||
if (this.iconDialog) {
|
||||
this.iconDialog.clear();
|
||||
}
|
||||
if (this.videoDialog) {
|
||||
this.videoDialog.clear();
|
||||
}
|
||||
|
||||
if (this.imageDialog) {
|
||||
defs.push(this.imageDialog.appendTo(this.$("#editor-media-image")));
|
||||
}
|
||||
if (this.documentDialog) {
|
||||
defs.push(this.documentDialog.appendTo(this.$("#editor-media-document")));
|
||||
}
|
||||
if (this.iconDialog) {
|
||||
defs.push(this.iconDialog.appendTo(this.$("#editor-media-icon")));
|
||||
}
|
||||
if (this.videoDialog) {
|
||||
defs.push(this.videoDialog.appendTo(this.$("#editor-media-video")));
|
||||
}
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
self._setActive(self.active);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
var _super = this._super;
|
||||
if (this.multiImages) {
|
||||
// In the case of multi images selection we suppose this was not to
|
||||
// replace an old media, so we only retrieve the images and save.
|
||||
return $.when(this.active.save()).then(function (data) {
|
||||
self.final_data = data;
|
||||
return _super.apply(self, args);
|
||||
});
|
||||
}
|
||||
|
||||
return $.when(this.active.save()).then(function (media) {
|
||||
self.trigger('saved', {
|
||||
attachments: self.active.images,
|
||||
media: media,
|
||||
});
|
||||
return _super.apply(self, args);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} widget
|
||||
*/
|
||||
_setActive: function (widget) {
|
||||
this.active = widget;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onSaveRequest: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.save();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {JQueryEvent} ev
|
||||
*/
|
||||
_onTabChange: function (ev) {
|
||||
var $target = $(ev.target);
|
||||
if ($target.is('[href="#editor-media-image"]')) {
|
||||
this._setActive(this.imageDialog);
|
||||
} else if ($target.is('[href="#editor-media-document"]')) {
|
||||
this._setActive(this.documentDialog);
|
||||
} else if ($target.is('[href="#editor-media-icon"]')) {
|
||||
this._setActive(this.active = this.iconDialog);
|
||||
} else if ($target.is('[href="#editor-media-video"]')) {
|
||||
this._setActive(this.active = this.videoDialog);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return MediaDialog;
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
odoo.define('wysiwyg.widgets', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Dialog = require('wysiwyg.widgets.Dialog');
|
||||
var AltDialog = require('wysiwyg.widgets.AltDialog');
|
||||
var MediaDialog = require('wysiwyg.widgets.MediaDialog');
|
||||
var LinkDialog = require('wysiwyg.widgets.LinkDialog');
|
||||
var CropImageDialog = require('wysiwyg.widgets.CropImageDialog');
|
||||
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
|
||||
|
||||
var media = require('wysiwyg.widgets.media');
|
||||
|
||||
return {
|
||||
Dialog: Dialog,
|
||||
AltDialog: AltDialog,
|
||||
MediaDialog: MediaDialog,
|
||||
LinkDialog: LinkDialog,
|
||||
CropImageDialog: CropImageDialog,
|
||||
ColorpickerDialog: ColorpickerDialog,
|
||||
|
||||
MediaWidget: media.MediaWidget,
|
||||
ImageWidget: media.ImageWidget,
|
||||
IconWidget: media.IconWidget,
|
||||
VideoWidget: media.VideoWidget,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,821 @@
|
||||
odoo.define('web_editor.wysiwyg', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Widget = require('web.Widget');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var modulesRegistry = require('web_editor.wysiwyg.plugin.registry');
|
||||
var wysiwygOptions = require('web_editor.wysiwyg.options');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var Wysiwyg = Widget.extend({
|
||||
xmlDependencies: [
|
||||
'/web_editor/static/src/xml/wysiwyg.xml',
|
||||
],
|
||||
custom_events: {
|
||||
getRecordInfo: '_onGetRecordInfo',
|
||||
wysiwyg_blur: '_onWysiwygBlur',
|
||||
},
|
||||
defaultOptions: {
|
||||
codeview: config.debug
|
||||
},
|
||||
|
||||
/**
|
||||
* @params {Object} params
|
||||
* @params {Object} params.recordInfo
|
||||
* @params {Object} params.recordInfo.context
|
||||
* @params {String} [params.recordInfo.context]
|
||||
* @params {integer} [params.recordInfo.res_id]
|
||||
* @params {String} [params.recordInfo.data_res_model]
|
||||
* @params {integer} [params.recordInfo.data_res_id]
|
||||
* @see _onGetRecordInfo
|
||||
* @see _getAttachmentsDomain in /wysiwyg/widgets/media.js
|
||||
* @params {Object} params.attachments
|
||||
* @see _onGetRecordInfo
|
||||
* @see _getAttachmentsDomain in /wysiwyg/widgets/media.js (for attachmentIDs)
|
||||
* @params {function} params.generateOptions
|
||||
* called with the summernote configuration object used before sending to summernote
|
||||
* @see _editorOptions
|
||||
**/
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.options = _.extend({}, this.defaultOptions, params);
|
||||
this.attachments = this.options.attachments || [];
|
||||
this.hints = [];
|
||||
this.$el = null;
|
||||
this._dirty = false;
|
||||
this.id = _.uniqueId('wysiwyg_');
|
||||
},
|
||||
/**
|
||||
* Load assets and color picker template then call summernote API
|
||||
* and replace $el by the summernote editable node.
|
||||
*
|
||||
* @override
|
||||
**/
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
this.$target = this.$el;
|
||||
this.$el = null; // temporary null to avoid hidden error, setElement when start
|
||||
return this._super()
|
||||
.then(function () {
|
||||
return modulesRegistry.start(self).then(function () {
|
||||
return self._loadInstance();
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var value = this._summernote.code();
|
||||
this._value = value;
|
||||
if (this._summernote.invoke('HelperPlugin.hasJinja', value)) {
|
||||
this._summernote.invoke('codeview.forceActivate');
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this._summernote) {
|
||||
// prevents the replacement of the target by the content of summernote
|
||||
// (in order to be able to cancel)
|
||||
var removeLayout = $.summernote.ui.removeLayout;
|
||||
$.summernote.ui.removeLayout = function ($note, layoutInfo) {
|
||||
layoutInfo.editor.remove();
|
||||
$note.show();
|
||||
};
|
||||
this._summernote.destroy();
|
||||
$.summernote.ui.removeLayout = removeLayout;
|
||||
}
|
||||
this.$target.removeAttr('data-wysiwyg-id');
|
||||
this.$target.removeData('wysiwyg');
|
||||
$(document).off('.' + this.id);
|
||||
this._super();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a step (undo) in editor.
|
||||
*/
|
||||
addHistoryStep: function () {
|
||||
var editor = this._summernote.modules.editor;
|
||||
editor.createRange();
|
||||
editor.history.recordUndo();
|
||||
},
|
||||
/**
|
||||
* Return the editable area.
|
||||
*
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
getEditable: function () {
|
||||
if (this._summernote.invoke('HelperPlugin.hasJinja', this._summernote.code())) {
|
||||
return this._summernote.layoutInfo.codable;
|
||||
} else if (this._summernote.invoke('codeview.isActivated')) {
|
||||
this._summernote.invoke('codeview.deactivate');
|
||||
}
|
||||
return this._summernote.layoutInfo.editable;
|
||||
},
|
||||
/**
|
||||
* Perform undo or redo in the editor.
|
||||
*
|
||||
* @param {integer} step
|
||||
*/
|
||||
history: function (step) {
|
||||
if (step < 0) {
|
||||
while (step) {
|
||||
this._summernote.modules.editor.history.rewind();
|
||||
step++;
|
||||
}
|
||||
} else if (step > 0) {
|
||||
while (step) {
|
||||
this._summernote.modules.editor.history.redo();
|
||||
step--;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Return true if the content has changed.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isDirty: function () {
|
||||
if (!this._dirty && this._value !== this._summernote.code()) {
|
||||
console.warn("not dirty flag ? Please fix it.");
|
||||
}
|
||||
return this._value !== this._summernote.code();
|
||||
},
|
||||
/**
|
||||
* Return true if the current node is unbreakable.
|
||||
* An unbreakable node can be removed or added but can't by split into
|
||||
* different nodes (for keypress and selection).
|
||||
* An unbreakable node can contain nodes that can be edited.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isUnbreakableNode: function (node) {
|
||||
return ["TD", "TR", "TBODY", "TFOOT", "THEAD", "TABLE"].indexOf(node.tagName) !== -1 || $(node).is(this.getEditable()) ||
|
||||
!this.isEditableNode(node.parentNode) || !this.isEditableNode(node) || $.summernote.dom.isMedia(node);
|
||||
},
|
||||
/**
|
||||
* Return true if the current node is editable (for keypress and selection).
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isEditableNode: function (node) {
|
||||
return $(node).is(':o_editable') && !$(node).is('table, thead, tbody, tfoot, tr');
|
||||
},
|
||||
/**
|
||||
* Set the focus on the element.
|
||||
*/
|
||||
focus: function () {
|
||||
this.$el.mousedown();
|
||||
},
|
||||
/**
|
||||
* Get the value of the editable element.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {Boolean} [options.keepPopover]
|
||||
* @returns {String}
|
||||
*/
|
||||
getValue: function (options) {
|
||||
if (!options || !options.keepPopover) {
|
||||
this._summernote.invoke('editor.hidePopover');
|
||||
}
|
||||
var $editable = this.getEditable().clone();
|
||||
$editable.find('.o_wysiwyg_to_remove').remove();
|
||||
$editable.find('[contenteditable]').removeAttr('contenteditable');
|
||||
$editable.find('.o_fake_not_editable').removeClass('o_fake_not_editable');
|
||||
$editable.find('.o_fake_editable').removeClass('o_fake_editable');
|
||||
$editable.find('[class=""]').removeAttr('class');
|
||||
$editable.find('[style=""]').removeAttr('style');
|
||||
$editable.find('[title=""]').removeAttr('title');
|
||||
$editable.find('[alt=""]').removeAttr('alt');
|
||||
$editable.find('a.o_image, span.fa, i.fa').html('');
|
||||
$editable.find('[aria-describedby]').removeAttr('aria-describedby').removeAttr('data-original-title');
|
||||
|
||||
return $editable.html() || $editable.val();
|
||||
},
|
||||
/**
|
||||
* Save the content in the target
|
||||
* - in init option beforeSave
|
||||
* - receive editable jQuery DOM as attribute
|
||||
* - called after deactivate codeview if needed
|
||||
* @returns {$.Promise}
|
||||
* - resolve with true if the content was dirty
|
||||
*/
|
||||
save: function () {
|
||||
var isDirty = this.isDirty();
|
||||
var html = this.getValue();
|
||||
if (this.$target.is('textarea')) {
|
||||
this.$target.val(html);
|
||||
} else {
|
||||
this.$target.html(html);
|
||||
}
|
||||
return $.when(isDirty, html);
|
||||
},
|
||||
/**
|
||||
* @param {String} value
|
||||
* @param {Object} options
|
||||
* @param {Boolean} [options.notifyChange]
|
||||
* @returns {String}
|
||||
*/
|
||||
setValue: function (value, options) {
|
||||
if (this._summernote.invoke('HelperPlugin.hasJinja', value)) {
|
||||
this._summernote.invoke('codeview.forceActivate');
|
||||
}
|
||||
this._dirty = true;
|
||||
this._summernote.invoke('HistoryPlugin.clear');
|
||||
this._summernote.invoke('editor.hidePopover');
|
||||
this._summernote.invoke('editor.clearTarget');
|
||||
var $editable = this.getEditable().html(value + '');
|
||||
this._summernote.invoke('UnbreakablePlugin.secureArea');
|
||||
if (!options || options.notifyChange !== false) {
|
||||
$editable.change();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object} the summernote configuration
|
||||
*/
|
||||
_editorOptions: function () {
|
||||
var self = this;
|
||||
var allowAttachment = !this.options.noAttachment;
|
||||
|
||||
var options = JSON.parse(JSON.stringify(wysiwygOptions));
|
||||
|
||||
options.parent = this;
|
||||
options.lang = "odoo";
|
||||
|
||||
options.focus = false;
|
||||
options.disableDragAndDrop = !allowAttachment;
|
||||
options.styleTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
|
||||
options.fontSizes = [_t('Default'), '8', '9', '10', '11', '12', '13', '14', '18', '24', '36', '48', '62'];
|
||||
options.minHeight = 180;
|
||||
|
||||
options.keyMap.pc['CTRL+K'] = 'LinkPlugin.show';
|
||||
options.keyMap.mac['CMD+K'] = 'LinkPlugin.show';
|
||||
delete options.keyMap.pc['CTRL+LEFTBRACKET'];
|
||||
delete options.keyMap.mac['CMD+LEFTBRACKET'];
|
||||
delete options.keyMap.pc['CTRL+RIGHTBRACKET'];
|
||||
delete options.keyMap.mac['CMD+RIGHTBRACKET'];
|
||||
|
||||
options.toolbar = [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'italic', 'underline', 'clear']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['colorpicker']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', allowAttachment ? ['linkPlugin', 'mediaPlugin'] : ['linkPlugin']],
|
||||
['history', ['undo', 'redo']],
|
||||
['view', this.options.codeview ? ['fullscreen', 'codeview', 'help'] : ['fullscreen', 'help']]
|
||||
];
|
||||
options.popover = {
|
||||
image: [
|
||||
['padding'],
|
||||
['imagesize', ['imageSizeAuto', 'imageSize100', 'imageSize50', 'imageSize25']],
|
||||
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
|
||||
['imageShape'],
|
||||
['cropImage'],
|
||||
['media', ['mediaPlugin', 'removePluginMedia']],
|
||||
['alt']
|
||||
],
|
||||
video: [
|
||||
['padding'],
|
||||
['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
|
||||
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
|
||||
['media', ['mediaPlugin', 'removePluginMedia']]
|
||||
],
|
||||
icon: [
|
||||
['padding'],
|
||||
['faSize'],
|
||||
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
|
||||
['faSpin'],
|
||||
['media', ['mediaPlugin', 'removePluginMedia']]
|
||||
],
|
||||
document: [
|
||||
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
|
||||
['media', ['mediaPlugin', 'removePluginMedia']]
|
||||
],
|
||||
link: [
|
||||
['link', ['linkPlugin', 'unlink']]
|
||||
],
|
||||
table: [
|
||||
['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
|
||||
['delete', ['deleteRow', 'deleteCol', 'deleteTable']]
|
||||
],
|
||||
};
|
||||
|
||||
options.callbacks = {
|
||||
onBlur: this._onBlurEditable.bind(this),
|
||||
onFocus: this._onFocusEditable.bind(this),
|
||||
onChange: this._onChange.bind(this),
|
||||
onImageUpload: this._onImageUpload.bind(this),
|
||||
onFocusnode: this._onFocusnode.bind(this),
|
||||
};
|
||||
|
||||
options.isUnbreakableNode = function (node) {
|
||||
node = node && (node.tagName ? node : node.parentNode);
|
||||
if (!node) {
|
||||
return true;
|
||||
}
|
||||
return self.isUnbreakableNode(node);
|
||||
};
|
||||
options.isEditableNode = function (node) {
|
||||
node = node && (node.tagName ? node : node.parentNode);
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
return self.isEditableNode(node);
|
||||
};
|
||||
options.displayPopover = this._isDisplayingPopover.bind(this);
|
||||
options.hasFocus = function () {
|
||||
return self._isFocused;
|
||||
};
|
||||
|
||||
if (this.options.generateOptions) {
|
||||
this.options.generateOptions(options);
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object} modules list to load
|
||||
*/
|
||||
_getPlugins: function () {
|
||||
return _.extend({}, wysiwygOptions.modules, modulesRegistry.plugins());
|
||||
},
|
||||
/**
|
||||
* Return an object describing the linked record.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options
|
||||
* @returns {Object} {res_id, res_model, xpath}
|
||||
*/
|
||||
_getRecordInfo: function (options) {
|
||||
var data = this.options.recordInfo || {};
|
||||
if (typeof data === 'function') {
|
||||
data = data(options);
|
||||
}
|
||||
if (!data.context) {
|
||||
throw new Error("Context is missing");
|
||||
}
|
||||
return data;
|
||||
},
|
||||
/**
|
||||
* Return true if the editor is displaying the popover.
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isDisplayingPopover: function (node) {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Return true if the given node is in the editor.
|
||||
* Note: a button in the MediaDialog returns true.
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isEditorContent: function (node) {
|
||||
if (this.el === node) {
|
||||
return true;
|
||||
}
|
||||
if ($.contains(this.el, node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var children = this.getChildren();
|
||||
var allChildren = [];
|
||||
var child;
|
||||
while ((child = children.pop())) {
|
||||
allChildren.push(child);
|
||||
children = children.concat(child.getChildren());
|
||||
}
|
||||
|
||||
var childrenDom = _.filter(_.unique(_.flatten(_.map(allChildren, function (obj) {
|
||||
return _.map(obj, function (value) {
|
||||
return value instanceof $ ? value.get() : value;
|
||||
});
|
||||
}))), function (node) {
|
||||
return node && node.DOCUMENT_NODE && node.tagName && node.tagName !== 'BODY' && node.tagName !== 'HTML';
|
||||
});
|
||||
return !!$(node).closest(childrenDom).length;
|
||||
},
|
||||
/**
|
||||
* Create an instance with the API lib.
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_loadInstance: function () {
|
||||
var defaultOptions = this._editorOptions();
|
||||
var summernoteOptions = _.extend({
|
||||
id: this.id,
|
||||
}, defaultOptions, _.omit(this.options, 'isEditableNode', 'isUnbreakableNode'));
|
||||
|
||||
_.extend(summernoteOptions.callbacks, defaultOptions.callbacks, this.options.callbacks);
|
||||
if (this.options.keyMap) {
|
||||
_.defaults(summernoteOptions.keyMap.pc, defaultOptions.keyMap.pc);
|
||||
_.each(summernoteOptions.keyMap.pc, function (v, k, o) {
|
||||
if (!v) {
|
||||
delete o[k];
|
||||
}
|
||||
});
|
||||
_.defaults(summernoteOptions.keyMap.mac, defaultOptions.keyMap.mac);
|
||||
_.each(summernoteOptions.keyMap.mac, function (v, k, o) {
|
||||
if (!v) {
|
||||
delete o[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var plugins = _.extend(this._getPlugins(), this.options.plugins);
|
||||
summernoteOptions.modules = _.omit(plugins, function (v) {
|
||||
return !v;
|
||||
});
|
||||
|
||||
if (this.$target.parent().length) {
|
||||
summernoteOptions.container = this.$target.parent().css('position', 'relative')[0];
|
||||
} else {
|
||||
summernoteOptions.container = this.$target[0].ownerDocument.body;
|
||||
}
|
||||
|
||||
this.$target.summernote(summernoteOptions);
|
||||
|
||||
this._summernote = this.$target.data('summernote');
|
||||
this._summernote.layoutInfo.editable.data('wysiwyg', this);
|
||||
this.$target.attr('data-wysiwyg-id', this.id).data('wysiwyg', this);
|
||||
$('.note-editor, .note-popover').not('[data-wysiwyg-id]').attr('data-wysiwyg-id', this.id);
|
||||
|
||||
this.setElement(this._summernote.layoutInfo.editor);
|
||||
$(document).on('mousedown.' + this.id, this._onMouseDown.bind(this));
|
||||
$(document).on('mouseenter.' + this.id, '*', this._onMouseEnter.bind(this));
|
||||
$(document).on('mouseleave.' + this.id, '*', this._onMouseLeave.bind(this));
|
||||
$(document).on('mousemove.' + this.id, this._onMouseMove.bind(this));
|
||||
|
||||
this.$el.removeClass('card');
|
||||
|
||||
return $.when();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* trigger_up 'wysiwyg_focus'.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
_onFocus: function (options) {
|
||||
this.trigger_up('wysiwyg_focus', options);
|
||||
},
|
||||
/**
|
||||
* trigger_up 'wysiwyg_blur'.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
_onBlur: function (options) {
|
||||
this.trigger_up('wysiwyg_blur', options);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onMouseEnter: function (ev) {
|
||||
if (this._isFocused && !this._mouseInEditor && this._isEditorContent(ev.target)) {
|
||||
this._mouseInEditor = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onMouseLeave: function (ev) {
|
||||
if (this._isFocused && this._mouseInEditor) {
|
||||
this._mouseInEditor = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onMouseMove: function (ev) {
|
||||
if (this._mouseInEditor === null) {
|
||||
this._mouseInEditor = !!this._isEditorContent(ev.target);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onMouseDown: function (ev) {
|
||||
var self = this;
|
||||
if (this._isEditorContent(ev.target)) {
|
||||
setTimeout(function () {
|
||||
if (!self._editableHasFocus && !self._isEditorContent(document.activeElement)) {
|
||||
self._summernote.layoutInfo.editable.focus();
|
||||
}
|
||||
if (!self._isFocused) {
|
||||
self._isFocused = true;
|
||||
self._onFocus();
|
||||
}
|
||||
});
|
||||
} else if (this._isFocused) {
|
||||
this._isFocused = false;
|
||||
this._onBlur();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onBlurEditable: function (ev) {
|
||||
var self = this;
|
||||
this._editableHasFocus = false;
|
||||
if (!this._isFocused) {
|
||||
return;
|
||||
}
|
||||
if (!this._justFocused && !this._mouseInEditor) {
|
||||
if (this._isFocused) {
|
||||
this._isFocused = false;
|
||||
this._onBlur();
|
||||
}
|
||||
} else if (!this._forceEditableFocus) {
|
||||
this._forceEditableFocus = true;
|
||||
setTimeout(function () {
|
||||
if (!self._isEditorContent(document.activeElement)) {
|
||||
self._summernote.layoutInfo.editable.focus();
|
||||
}
|
||||
self._forceEditableFocus = false; // prevent stack size exceeded.
|
||||
});
|
||||
} else {
|
||||
this._mouseInEditor = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onWysiwygBlur: function (ev) {
|
||||
if (ev.target === this) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this._isFocused = false;
|
||||
this._forceEditableFocus = false;
|
||||
this._mouseInEditor = false;
|
||||
this._summernote.disable();
|
||||
this.$target.focus();
|
||||
setTimeout(this._summernote.enable.bind(this._summernote));
|
||||
this._onBlur(ev.data);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onFocusEditable: function (ev) {
|
||||
var self = this;
|
||||
this._editableHasFocus = true;
|
||||
this._justFocused = true;
|
||||
setTimeout(function () {
|
||||
self._justFocused = true;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* trigger_up 'wysiwyg_change'
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onChange: function () {
|
||||
var html = this._summernote.code();
|
||||
if (this.hints.length) {
|
||||
var hints = [];
|
||||
_.each(this.hints, function (hint) {
|
||||
if (html.indexOf('@' + hint.name) !== -1) {
|
||||
hints.push(hint);
|
||||
}
|
||||
});
|
||||
this.hints = hints;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
this.trigger_up('wysiwyg_change', {
|
||||
html: html,
|
||||
hints: this.hints,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* trigger_up 'wysiwyg_attachment' when add an image found in the view.
|
||||
*
|
||||
* This method is called when an image is uploaded by the media dialog and returns the
|
||||
* object attachment as recorded in the "ir.attachment" model, via a wysiwyg_attachment event.
|
||||
*
|
||||
* For e.g. when sending email, this allows people to add attachments with the content
|
||||
* editor interface and that they appear in the attachment list.
|
||||
* The new documents being attached to the email, they will not be erased by the CRON
|
||||
* when closing the wizard.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onImageUpload: function (attachments) {
|
||||
var self = this;
|
||||
attachments = _.filter(attachments, function (attachment) {
|
||||
return !_.findWhere(self.attachments, {
|
||||
id: attachment.id,
|
||||
});
|
||||
});
|
||||
if (!attachments.length) {
|
||||
return;
|
||||
}
|
||||
this.attachments = this.attachments.concat(attachments);
|
||||
|
||||
// todo remove image not in the view
|
||||
|
||||
this.trigger_up.bind(this, 'wysiwyg_attachment', this.attachments);
|
||||
},
|
||||
/**
|
||||
* Called when the carret focuses on another node (focus event, mouse event, or key arrow event)
|
||||
* from Unbreakable
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node
|
||||
*/
|
||||
_onFocusnode: function (node) {},
|
||||
/**
|
||||
* Do not override.
|
||||
*
|
||||
* @see _getRecordInfo
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {Object} ev.data
|
||||
* @param {Object} ev.data.recordInfo
|
||||
* @param {Function(recordInfo)} ev.data.callback
|
||||
*/
|
||||
_onGetRecordInfo: function (ev) {
|
||||
var data = this._getRecordInfo(ev.data);
|
||||
data.attachmentIDs = _.pluck(this.attachments, 'id');
|
||||
data.user_id = session.uid || session.user_id;
|
||||
ev.data.callback(data);
|
||||
},
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public helper
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load wysiwyg assets if needed.
|
||||
*
|
||||
* @see Wysiwyg.createReadyFunction
|
||||
* @param {Widget} parent
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
Wysiwyg.prepare = (function () {
|
||||
var assetsLoaded = false;
|
||||
var def;
|
||||
return function prepare(parent) {
|
||||
if (assetsLoaded) {
|
||||
return $.when();
|
||||
}
|
||||
if (def) {
|
||||
return def;
|
||||
}
|
||||
def = $.Deferred();
|
||||
var timeout = setTimeout(function () {
|
||||
throw _t("Can't load assets of the wysiwyg editor");
|
||||
}, 10000);
|
||||
var wysiwyg = new Wysiwyg(parent, {
|
||||
recordInfo: {
|
||||
context: {},
|
||||
}
|
||||
});
|
||||
wysiwyg.attachTo($('<textarea>')).then(function () {
|
||||
assetsLoaded = true;
|
||||
clearTimeout(timeout);
|
||||
wysiwyg.destroy();
|
||||
def.resolve();
|
||||
});
|
||||
return def;
|
||||
};
|
||||
})();
|
||||
/**
|
||||
* @param {Node} node (editable or node inside)
|
||||
* @returns {Object}
|
||||
* @returns {Node} sc - start container
|
||||
* @returns {Number} so - start offset
|
||||
* @returns {Node} ec - end container
|
||||
* @returns {Number} eo - end offset
|
||||
*/
|
||||
Wysiwyg.getRange = function (node) {
|
||||
var range = $.summernote.range.create();
|
||||
return range && {
|
||||
sc: range.sc,
|
||||
so: range.so,
|
||||
ec: range.ec,
|
||||
eo: range.eo,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @param {Node} startNode
|
||||
* @param {Number} startOffset
|
||||
* @param {Node} endNode
|
||||
* @param {Number} endOffset
|
||||
*/
|
||||
Wysiwyg.setRange = function (startNode, startOffset, endNode, endOffset) {
|
||||
$(startNode).focus();
|
||||
if (endNode) {
|
||||
$.summernote.range.create(startNode, startOffset, endNode, endOffset).select();
|
||||
} else {
|
||||
$.summernote.range.create(startNode, startOffset).select();
|
||||
}
|
||||
// trigger for Unbreakable
|
||||
$(startNode.tagName ? startNode : startNode.parentNode).trigger('wysiwyg.range');
|
||||
};
|
||||
/**
|
||||
* @param {Node} node - dom node
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} options.begin move the range to the beginning of the first node.
|
||||
* @param {Boolean} options.end move the range to the end of the last node.
|
||||
*/
|
||||
Wysiwyg.setRangeFromNode = function (node, options) {
|
||||
var last = node;
|
||||
while (last.lastChild) {
|
||||
last = last.lastChild;
|
||||
}
|
||||
var first = node;
|
||||
while (first.firstChild) {
|
||||
first = first.firstChild;
|
||||
}
|
||||
|
||||
if (options && options.begin && !options.end) {
|
||||
Wysiwyg.setRange(first, 0);
|
||||
} else if (options && !options.begin && options.end) {
|
||||
Wysiwyg.setRange(last, last.textContent.length);
|
||||
} else {
|
||||
Wysiwyg.setRange(first, 0, last, last.tagName ? last.childNodes.length : last.textContent.length);
|
||||
}
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// jQuery extensions
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
$.extend($.expr[':'], {
|
||||
o_editable: function (node, i, m) {
|
||||
while (node) {
|
||||
if (node.attributes) {
|
||||
var className = _.isString(node.className) && node.className || '';
|
||||
if (
|
||||
className.indexOf('o_not_editable') !== -1 ||
|
||||
(node.attributes.contenteditable &&
|
||||
node.attributes.contenteditable.value !== 'true' &&
|
||||
className.indexOf('o_fake_not_editable') === -1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
className.indexOf('o_editable') !== -1 ||
|
||||
(node.attributes.contenteditable &&
|
||||
node.attributes.contenteditable.value === 'true' &&
|
||||
className.indexOf('o_fake_editable') === -1)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return Wysiwyg;
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
odoo.define('web_editor.wysiwyg.iframe', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
var ajax = require('web.ajax');
|
||||
|
||||
|
||||
var _fnSummernoteMaster = $.fn.summernote;
|
||||
var _summernoteMaster = $.summernote;
|
||||
$.fn.summernote = function () {
|
||||
var summernote = this[0].ownerDocument.defaultView._fnSummenoteSlave || _fnSummernoteMaster;
|
||||
return summernote.apply(this, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add option (inIframe) to load Wysiwyg in an iframe.
|
||||
**/
|
||||
Wysiwyg.include({
|
||||
/**
|
||||
* Add options to load Wysiwyg in an iframe.
|
||||
*
|
||||
* @override
|
||||
* @param {boolean} options.inIframe
|
||||
**/
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.options.inIframe) {
|
||||
if (!this.options.iframeCssAssets) {
|
||||
this.options.iframeCssAssets = 'web_editor.wysiwyg_iframe_css_assets';
|
||||
}
|
||||
this._onUpdateIframeId = 'onLoad_' + this.id;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Load assets to inject into iframe.
|
||||
*
|
||||
* @override
|
||||
**/
|
||||
willStart: function () {
|
||||
if (!this.options.inIframe) {
|
||||
return this._super();
|
||||
}
|
||||
if (this.options.iframeCssAssets) {
|
||||
this.defAsset = ajax.loadAsset(this.options.iframeCssAssets);
|
||||
} else {
|
||||
this.defAsset = $.when({cssLibs: [], cssContents: []});
|
||||
}
|
||||
this.$target = this.$el;
|
||||
return this.defAsset
|
||||
.then(this._loadIframe.bind(this))
|
||||
.then(this._super.bind(this)).then(function () {
|
||||
var _summernoteMaster = $.summernote;
|
||||
var _summernoteSlave = this.$iframe[0].contentWindow._summernoteSlave;
|
||||
_summernoteSlave.options = _.extend({}, _summernoteMaster.options, {modules: _summernoteSlave.options.modules});
|
||||
this._enableBootstrapInIframe();
|
||||
}.bind(this));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (!this.options.inIframe) {
|
||||
return this._super();
|
||||
}
|
||||
$(document.body).off('.' + this.id);
|
||||
|
||||
this.$target.insertBefore(this.$iframe);
|
||||
|
||||
delete window.top[this._onUpdateIframeId];
|
||||
if (this.$iframeTarget) {
|
||||
this.$iframeTarget.remove();
|
||||
}
|
||||
if (this.$iframe) {
|
||||
this.$iframe.remove();
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Change fullsreen feature.
|
||||
*
|
||||
* @override
|
||||
* @returns {Object} modules list to load
|
||||
*/
|
||||
_getPlugins: function () {
|
||||
var self = this;
|
||||
var plugins = this._super();
|
||||
plugins.fullscreen = plugins.fullscreen.extend({
|
||||
toggle: function () {
|
||||
if (!self.$iframe) {
|
||||
return this._super();
|
||||
}
|
||||
self.$iframe.toggleClass('o_fullscreen');
|
||||
self.$iframe.contents().find('body').toggleClass('o_fullscreen');
|
||||
},
|
||||
isFullscreen: function () {
|
||||
if (!self.$iframe) {
|
||||
return this._super();
|
||||
}
|
||||
return self.$iframe.hasClass('o_fullscreen');
|
||||
},
|
||||
});
|
||||
return plugins;
|
||||
},
|
||||
/**
|
||||
* This method is called after the iframe is loaded with the editor. This is
|
||||
* to activate the bootstrap features that out of the iframe would launch
|
||||
* automatically when changing the dom.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_enableBootstrapInIframe: function () {
|
||||
var body = this.$iframe[0].contentWindow.document.body;
|
||||
var $toolbarButtons = this._summernote.layoutInfo.toolbar.find('[data-toggle="dropdown"]').dropdown({
|
||||
boundary: body,
|
||||
});
|
||||
|
||||
function hideDrowpdown() {
|
||||
var $expended = $toolbarButtons.filter('[aria-expanded="true"]').parent();
|
||||
$expended.children().removeAttr('aria-expanded').removeClass('show');
|
||||
$expended.removeClass('show');
|
||||
}
|
||||
$(body).on('mouseup.' + this.id, hideDrowpdown);
|
||||
$(document.body).on('click.' + this.id, hideDrowpdown);
|
||||
},
|
||||
/**
|
||||
* Create iframe, inject css and create a link with the content,
|
||||
* then inject the target inside.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_loadIframe: function () {
|
||||
this.$iframe = $('<iframe class="wysiwyg_iframe">').css({
|
||||
'min-height': '400px',
|
||||
width: '100%'
|
||||
});
|
||||
var avoidDoubleLoad = 0; // this bug only appears on some configurations.
|
||||
|
||||
// resolve deferred on load
|
||||
|
||||
var def = $.Deferred();
|
||||
this.$iframe.data('load-def', def); // for unit test
|
||||
window.top[this._onUpdateIframeId] = function (_avoidDoubleLoad) {
|
||||
if (_avoidDoubleLoad !== avoidDoubleLoad) {
|
||||
console.warn('Wysiwyg iframe double load detected');
|
||||
return;
|
||||
}
|
||||
delete window.top[this._onUpdateIframeId];
|
||||
var $iframeTarget = this.$iframe.contents().find('#iframe_target');
|
||||
$iframeTarget.append(this.$target);
|
||||
def.resolve();
|
||||
}.bind(this);
|
||||
|
||||
// inject content in iframe
|
||||
|
||||
this.$iframe.on('load', function onLoad (ev) {
|
||||
var _avoidDoubleLoad = ++avoidDoubleLoad;
|
||||
this.defAsset.then(function (asset) {
|
||||
if (_avoidDoubleLoad !== avoidDoubleLoad) {
|
||||
console.warn('Wysiwyg immediate iframe double load detected');
|
||||
return;
|
||||
}
|
||||
var cwindow = this.$iframe[0].contentWindow;
|
||||
cwindow.document
|
||||
.open("text/html", "replace")
|
||||
.write(
|
||||
'<head>' +
|
||||
'<meta charset="utf-8">' +
|
||||
'<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n' +
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>\n' +
|
||||
_.map(asset.cssLibs, function (cssLib) {
|
||||
return '<link type="text/css" rel="stylesheet" href="' + cssLib + '"/>';
|
||||
}).join('\n') + '\n' +
|
||||
_.map(asset.cssContents, function (cssContent) {
|
||||
return '<style type="text/css">' + cssContent + '</style>';
|
||||
}).join('\n') + '\n' +
|
||||
_.map(asset.jsContents, function (jsContent) {
|
||||
if (jsContent.indexOf('<inline asset>') !== -1) {
|
||||
return '<script type="text/javascript">' + jsContent + '</script>';
|
||||
}
|
||||
}).join('\n') + '\n' +
|
||||
'</head>\n' +
|
||||
'<body class="o_in_iframe">\n' +
|
||||
'<div id="iframe_target" style="height: calc(100vh - 6px);"></div>\n' +
|
||||
'<script type="text/javascript">' +
|
||||
'window.$ = window.jQuery = window.top.jQuery;' +
|
||||
'var _summernoteMaster = $.summernote;' +
|
||||
'var _fnSummernoteMaster = $.fn.summernote;' +
|
||||
'delete $.summernote;' +
|
||||
'delete $.fn.summernote;' +
|
||||
'</script>\n' +
|
||||
'<script type="text/javascript" src="/web_editor/static/lib/summernote/summernote.js"></script>\n' +
|
||||
'<script type="text/javascript">' +
|
||||
'window._summernoteSlave = $.summernote;' +
|
||||
'window._summernoteSlave.iframe = true;' +
|
||||
'window._summernoteSlave.lang = _summernoteMaster.lang;' +
|
||||
'window._fnSummenoteSlave = $.fn.summernote;' +
|
||||
'$.summernote = _summernoteMaster;' +
|
||||
'$.fn.summernote = _fnSummernoteMaster;' +
|
||||
'if (window.top.' + this._onUpdateIframeId + ') {' +
|
||||
'window.top.' + this._onUpdateIframeId + '(' + _avoidDoubleLoad + ')' +
|
||||
'}' +
|
||||
'</script>\n' +
|
||||
'</body>');
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
|
||||
this.$iframe.insertAfter(this.$target);
|
||||
|
||||
return def.promise();
|
||||
},
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public helper
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the current range from Summernote.
|
||||
*
|
||||
* @param {Node} [DOM]
|
||||
* @returns {Object}
|
||||
* @returns {Node} sc - start container
|
||||
* @returns {Number} so - start offset
|
||||
* @returns {Node} ec - end container
|
||||
* @returns {Number} eo - end offset
|
||||
*/
|
||||
Wysiwyg.getRange = function (DOM) {
|
||||
var summernote = (DOM.defaultView || DOM.ownerDocument.defaultView)._summernoteSlave || _summernoteMaster;
|
||||
var range = summernote.range.create();
|
||||
return range && {
|
||||
sc: range.sc,
|
||||
so: range.so,
|
||||
ec: range.ec,
|
||||
eo: range.eo,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @param {Node} sc - start container
|
||||
* @param {Number} so - start offset
|
||||
* @param {Node} ec - end container
|
||||
* @param {Number} eo - end offset
|
||||
*/
|
||||
Wysiwyg.setRange = function (sc, so, ec, eo) {
|
||||
var summernote = sc.ownerDocument.defaultView._summernoteSlave || _summernoteMaster;
|
||||
$(sc).focus();
|
||||
if (ec) {
|
||||
summernote.range.create(sc, so, ec, eo).select();
|
||||
} else {
|
||||
summernote.range.create(sc, so).select();
|
||||
}
|
||||
// trigger for Unbreakable
|
||||
$(sc.tagName ? sc : sc.parentNode).trigger('wysiwyg.range');
|
||||
};
|
||||
|
||||
});
|
||||
@@ -5,6 +5,7 @@ var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var Widget = require('web.Widget');
|
||||
var options = require('web_editor.snippets.options');
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -24,6 +25,7 @@ var SnippetEditor = Widget.extend({
|
||||
'click .oe_snippet_parent': '_onParentButtonClick',
|
||||
'click .oe_snippet_clone': '_onCloneClick',
|
||||
'click .oe_snippet_remove': '_onRemoveClick',
|
||||
'mousedown .oe_options [data-toggle="dropdown"]:first': '_onOpenCusomize',
|
||||
},
|
||||
custom_events: {
|
||||
cover_update: '_onCoverUpdate',
|
||||
@@ -35,9 +37,15 @@ var SnippetEditor = Widget.extend({
|
||||
* @param {Widget} parent
|
||||
* @param {Element} target
|
||||
* @param templateOptions
|
||||
* @param {jQuery} $editable
|
||||
* @param {Object} options
|
||||
*/
|
||||
init: function (parent, target, templateOptions) {
|
||||
init: function (parent, target, templateOptions, $editable, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.options = options;
|
||||
this.$editable = $editable;
|
||||
this.ownerDocument = this.$editable[0].ownerDocument;
|
||||
this.$body = $(this.ownerDocument.body);
|
||||
this.$target = $(target);
|
||||
this.$target.data('snippet-editor', this);
|
||||
this.templateOptions = templateOptions;
|
||||
@@ -59,13 +67,13 @@ var SnippetEditor = Widget.extend({
|
||||
defs.push(this._initializeOptions());
|
||||
|
||||
// Initialize move/clone/remove buttons
|
||||
if (!this.$target.parent().is(':o_editable')) {
|
||||
if (!this.options.isEditableNode(this.$target[0])) {
|
||||
this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
|
||||
} else {
|
||||
this.dropped = false;
|
||||
this.$el.draggable({
|
||||
greedy: true,
|
||||
appendTo: 'body',
|
||||
appendTo: this.$body,
|
||||
cursor: 'move',
|
||||
handle: '.oe_snippet_move',
|
||||
cursorAt: {
|
||||
@@ -76,7 +84,7 @@ var SnippetEditor = Widget.extend({
|
||||
var $clone = $(this).clone().css({width: '24px', height: '24px', border: 0});
|
||||
$clone.find('.oe_overlay_options >:not(:contains(.oe_snippet_move)), .o_handle').remove();
|
||||
$clone.find(':not(.glyphicon)').css({position: 'absolute', top: 0, left: 0});
|
||||
$clone.appendTo('body').removeClass('d-none');
|
||||
$clone.appendTo(self.$body).removeClass('d-none');
|
||||
return $clone;
|
||||
},
|
||||
start: _.bind(self._onDragAndDropStart, self),
|
||||
@@ -134,7 +142,7 @@ var SnippetEditor = Widget.extend({
|
||||
top: offset.top,
|
||||
});
|
||||
this.$('.o_handles').css('height', this.$target.outerHeight());
|
||||
this.$el.toggleClass('o_top_cover', offset.top < 15);
|
||||
this.$el.toggleClass('o_top_cover', offset.top < this.$editable.offset().top);
|
||||
},
|
||||
/**
|
||||
* Removes the associated snippet from the DOM and destroys the associated
|
||||
@@ -159,7 +167,6 @@ var SnippetEditor = Widget.extend({
|
||||
|
||||
var node = $parent[0];
|
||||
if (node && node.firstChild) {
|
||||
$.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
|
||||
if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
@@ -181,11 +188,12 @@ var SnippetEditor = Widget.extend({
|
||||
}
|
||||
|
||||
// clean editor if they are image or table in deleted content
|
||||
$('.note-control-selection').hide();
|
||||
$('.o_table_handler').remove();
|
||||
this.$body.find('.note-control-selection').hide();
|
||||
this.$body.find('.o_table_handler').remove();
|
||||
|
||||
this.trigger_up('snippet_removed');
|
||||
this.destroy();
|
||||
$parent.trigger('content_changed');
|
||||
},
|
||||
/**
|
||||
* Displays/Hides the editor overlay and notifies the associated snippet
|
||||
@@ -195,6 +203,9 @@ var SnippetEditor = Widget.extend({
|
||||
* @param {boolean} focus - true to display, false to hide
|
||||
*/
|
||||
toggleFocus: function (focus) {
|
||||
if (!this.$el) {
|
||||
return;
|
||||
}
|
||||
var do_action = (focus ? _do_action_focus : _do_action_blur);
|
||||
|
||||
// Attach own and parent options on the current overlay
|
||||
@@ -222,6 +233,10 @@ var SnippetEditor = Widget.extend({
|
||||
this.cover();
|
||||
this.$el.toggleClass('oe_active', !!focus);
|
||||
|
||||
if (focus) {
|
||||
this.trigger_up('snippet_focused');
|
||||
}
|
||||
|
||||
function _do_action_focus(style, $dest) {
|
||||
style.$el.insertAfter($dest);
|
||||
style.onFocus();
|
||||
@@ -282,7 +297,8 @@ var SnippetEditor = Widget.extend({
|
||||
self,
|
||||
val.base_target ? self.$target.find(val.base_target).eq(0) : self.$target,
|
||||
self.$el,
|
||||
val.data
|
||||
val.data,
|
||||
self.options
|
||||
);
|
||||
self.styles[optionName || _.uniqueId('option')] = option;
|
||||
option.__order = i++;
|
||||
@@ -324,10 +340,11 @@ var SnippetEditor = Widget.extend({
|
||||
*/
|
||||
_onCloneClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
this.trigger_up('cover_will_change');
|
||||
|
||||
var $clone = this.$target.clone(false);
|
||||
|
||||
this.trigger_up('request_history_undo_record', {$target: this.$target});
|
||||
|
||||
this.$target.after($clone);
|
||||
this.trigger_up('call_for_each_child_snippet', {
|
||||
$snippet: $clone,
|
||||
@@ -340,6 +357,7 @@ var SnippetEditor = Widget.extend({
|
||||
},
|
||||
});
|
||||
this.trigger_up('snippet_cloned', {$target: $clone});
|
||||
$clone.trigger('content_changed');
|
||||
},
|
||||
/**
|
||||
* Called when the overlay dimensions/positions should be recomputed.
|
||||
@@ -347,6 +365,7 @@ var SnippetEditor = Widget.extend({
|
||||
* @private
|
||||
*/
|
||||
_onCoverUpdate: function () {
|
||||
this.trigger_up('cover_will_change');
|
||||
this.cover();
|
||||
},
|
||||
/**
|
||||
@@ -382,11 +401,11 @@ var SnippetEditor = Widget.extend({
|
||||
$selectorChildren: $selectorChildren,
|
||||
});
|
||||
|
||||
$('body').addClass('move-important');
|
||||
this.$body.addClass('move-important');
|
||||
|
||||
$('.oe_drop_zone').droppable({
|
||||
this.$editable.find('.oe_drop_zone').droppable({
|
||||
over: function () {
|
||||
$('.oe_drop_zone.hide').removeClass('hide');
|
||||
self.$editable.find('.oe_drop_zone.hide').removeClass('hide');
|
||||
$(this).addClass('hide').first().after(self.$target);
|
||||
self.dropped = true;
|
||||
},
|
||||
@@ -396,6 +415,8 @@ var SnippetEditor = Widget.extend({
|
||||
self.dropped = false;
|
||||
},
|
||||
});
|
||||
|
||||
this.trigger_up('cover_will_change');
|
||||
},
|
||||
/**
|
||||
* Called when the snippet is dropped after being dragged thanks to the
|
||||
@@ -404,24 +425,23 @@ var SnippetEditor = Widget.extend({
|
||||
* @private
|
||||
*/
|
||||
_onDragAndDropStop: function () {
|
||||
var self = this;
|
||||
|
||||
$('.oe_drop_zone').droppable('destroy').remove();
|
||||
this.$editable.find('.oe_drop_zone').droppable('destroy').remove();
|
||||
|
||||
var prev = this.$target.first()[0].previousSibling;
|
||||
var next = this.$target.last()[0].nextSibling;
|
||||
var $parent = this.$target.parent();
|
||||
|
||||
var $clone = $('.oe_drop_clone');
|
||||
var $clone = this.$editable.find('.oe_drop_clone');
|
||||
if (prev === $clone[0]) {
|
||||
prev = $clone[0].previousSibling;
|
||||
} else if (next === $clone[0]) {
|
||||
next = $clone[0].nextSibling;
|
||||
}
|
||||
$clone.after(this.$target);
|
||||
var $from = $clone.parent();
|
||||
|
||||
this.$el.removeClass('d-none');
|
||||
$('body').removeClass('move-important');
|
||||
this.$body.removeClass('move-important');
|
||||
$clone.remove();
|
||||
|
||||
if (this.dropped) {
|
||||
@@ -438,10 +458,13 @@ var SnippetEditor = Widget.extend({
|
||||
for (var i in this.styles) {
|
||||
this.styles[i].onMove();
|
||||
}
|
||||
|
||||
this.$target.trigger('content_changed');
|
||||
$from.trigger('content_changed');
|
||||
}
|
||||
|
||||
self.trigger_up('drag_and_drop_stop', {
|
||||
$snippet: self.$target,
|
||||
this.trigger_up('drag_and_drop_stop', {
|
||||
$snippet: this.$target,
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -452,6 +475,7 @@ var SnippetEditor = Widget.extend({
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onOptionUpdate: function (ev) {
|
||||
this.trigger_up('cover_will_change');
|
||||
// If multiple option names are given, we suppose it should not be
|
||||
// propagated to parent editor
|
||||
if (ev.data.optionNames) {
|
||||
@@ -474,6 +498,16 @@ var SnippetEditor = Widget.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when the user opens the customize menu.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onOpenCusomize: function (ev) {
|
||||
this.trigger_up('request_history_undo_record', {$target: this.$target});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the 'parent' button is clicked.
|
||||
*
|
||||
@@ -494,6 +528,7 @@ var SnippetEditor = Widget.extend({
|
||||
*/
|
||||
_onRemoveClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
this.trigger_up('cover_will_change');
|
||||
this.trigger_up('request_history_undo_record', {$target: this.$target});
|
||||
this.removeSnippet();
|
||||
},
|
||||
@@ -504,6 +539,7 @@ var SnippetEditor = Widget.extend({
|
||||
*/
|
||||
var SnippetsMenu = Widget.extend({
|
||||
id: 'oe_snippets',
|
||||
cacheSnippetTemplate: {},
|
||||
activeSnippets: [],
|
||||
custom_events: {
|
||||
activate_insertion_zones: '_onActivateInsertionZones',
|
||||
@@ -513,15 +549,34 @@ var SnippetsMenu = Widget.extend({
|
||||
go_to_parent: '_onGoToParent',
|
||||
remove_snippet: '_onRemoveSnippet',
|
||||
snippet_removed: '_onSnippetRemoved',
|
||||
reload_snippet_dropzones: '_disableUndroppableSnippets',
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Widget} parent
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.snippets]
|
||||
* URL of the snippets template. This URL might have been set
|
||||
* in the global 'snippets' variable, otherwise this function
|
||||
* assigns a default one.
|
||||
* default: 'web_editor.snippets'
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, $editable) {
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
options = options || {};
|
||||
this.trigger_up('getRecordInfo', {
|
||||
recordInfo: options,
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(options, recordInfo);
|
||||
},
|
||||
});
|
||||
|
||||
this.$editable = $editable;
|
||||
this.options = options;
|
||||
if (!this.options.snippets) {
|
||||
this.options.snippets = 'web_editor.snippets';
|
||||
}
|
||||
this.$activeSnippet = false;
|
||||
this.snippetEditors = [];
|
||||
},
|
||||
@@ -531,12 +586,14 @@ var SnippetsMenu = Widget.extend({
|
||||
start: function () {
|
||||
var self = this;
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
var $document = $(document);
|
||||
var $window = $(window);
|
||||
this.ownerDocument = this.$el[0].ownerDocument;
|
||||
this.$document = $(this.ownerDocument);
|
||||
this.window = this.ownerDocument.defaultView;
|
||||
this.$window = $(this.window);
|
||||
|
||||
// Fetch snippet templates and compute it
|
||||
var url = this._getSnippetURL();
|
||||
defs.push(this._rpc({route: url}).then(function (html) {
|
||||
|
||||
defs.push(this.loadSnippets().then(function (html) {
|
||||
return self._computeSnippetTemplates(html);
|
||||
}));
|
||||
|
||||
@@ -547,7 +604,7 @@ var SnippetsMenu = Widget.extend({
|
||||
|
||||
// Active snippet editor on click in the page
|
||||
var lastClickedElement;
|
||||
$document.on('click.snippets_menu', '*', function (ev) {
|
||||
this.$document.on('click.snippets_menu', '*', function (ev) {
|
||||
var srcElement = ev.srcElement || (ev.originalEvent && (ev.originalEvent.originalTarget || ev.originalEvent.target) || ev.target);
|
||||
if (lastClickedElement === srcElement || !srcElement) {
|
||||
return;
|
||||
@@ -567,27 +624,21 @@ var SnippetsMenu = Widget.extend({
|
||||
core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
|
||||
core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
|
||||
|
||||
// Some summernote customization
|
||||
var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
|
||||
$.summernote.core.dom.isNotBreakable = function (node) {
|
||||
return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
|
||||
};
|
||||
|
||||
// Adapt overlay covering when the window is resized / content changes
|
||||
var debouncedCoverUpdate = _.debounce(function () {
|
||||
self._updateCurrentSnippetEditorOverlay();
|
||||
self.updateCurrentSnippetEditorOverlay();
|
||||
}, 200);
|
||||
$window.on('resize.snippets_menu', debouncedCoverUpdate);
|
||||
$window.on('content_changed.snippets_menu', debouncedCoverUpdate);
|
||||
this.$window.on('resize.snippets_menu', debouncedCoverUpdate);
|
||||
this.$window.on('content_changed.snippets_menu', debouncedCoverUpdate);
|
||||
|
||||
// On keydown add a class on the active overlay to hide it and show it
|
||||
// again when the mouse moves
|
||||
$document.on('keydown.snippets_menu', function () {
|
||||
this.$document.on('keydown.snippets_menu', function () {
|
||||
if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
|
||||
self.$activeSnippet.data('snippet-editor').$el.addClass('o_keypress');
|
||||
}
|
||||
});
|
||||
$document.on('mousemove.snippets_menu', function () {
|
||||
this.$document.on('mousemove.snippets_menu, mousedown.snippets_menu', function () {
|
||||
if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
|
||||
self.$activeSnippet.data('snippet-editor').$el.removeClass('o_keypress');
|
||||
}
|
||||
@@ -595,12 +646,12 @@ var SnippetsMenu = Widget.extend({
|
||||
|
||||
// Auto-selects text elements with a specific class and remove this
|
||||
// on text changes
|
||||
$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
|
||||
this.$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
|
||||
$(ev.target).selectContent();
|
||||
});
|
||||
$document.on('keyup.snippets_menu', function () {
|
||||
var r = $.summernote.core.range.create();
|
||||
$(r && r.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
|
||||
this.$document.on('keyup.snippets_menu', function () {
|
||||
var range = Wysiwyg.getRange(this);
|
||||
$(range && range.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
|
||||
});
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
@@ -608,7 +659,7 @@ var SnippetsMenu = Widget.extend({
|
||||
// menu will take part of the screen width (delayed because of
|
||||
// animation). (TODO wait for real animation end)
|
||||
setTimeout(function () {
|
||||
$window.trigger('resize');
|
||||
self.$window.trigger('resize');
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
@@ -617,11 +668,16 @@ var SnippetsMenu = Widget.extend({
|
||||
*/
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$snippetEditorArea.remove();
|
||||
$(window).off('.snippets_menu');
|
||||
$(document).off('.snippets_menu');
|
||||
if (this.$window) {
|
||||
this.$snippetEditorArea.remove();
|
||||
this.$window.off('.snippets_menu');
|
||||
this.$document.off('.snippets_menu');
|
||||
}
|
||||
core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);
|
||||
core.bus.off('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
|
||||
if (this._defLoadSnippets.state() === "pending") {
|
||||
delete this.cacheSnippetTemplate[this.options.snippets];
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -634,16 +690,68 @@ var SnippetsMenu = Widget.extend({
|
||||
* - Remove the 'contentEditable' attributes
|
||||
*/
|
||||
cleanForSave: function () {
|
||||
this._activateSnippet(false);
|
||||
this.trigger_up('ready_to_clean_for_save');
|
||||
this._destroyEditors();
|
||||
|
||||
this.$editable.find('[contentEditable]')
|
||||
this.getEditableArea().find('[contentEditable]')
|
||||
.removeAttr('contentEditable')
|
||||
.removeProp('contentEditable');
|
||||
|
||||
this.$editable.find('.o_we_selected_image')
|
||||
this.getEditableArea().find('.o_we_selected_image')
|
||||
.removeClass('o_we_selected_image');
|
||||
},
|
||||
/**
|
||||
* Load snippets.
|
||||
*/
|
||||
loadSnippets: function () {
|
||||
if (this.cacheSnippetTemplate[this.options.snippets]) {
|
||||
this._defLoadSnippets = this.cacheSnippetTemplate[this.options.snippets];
|
||||
return this._defLoadSnippets;
|
||||
}
|
||||
this._defLoadSnippets = this._rpc({
|
||||
model: 'ir.ui.view',
|
||||
method: 'render_template',
|
||||
args: [this.options.snippets, {}],
|
||||
kwargs: {
|
||||
context: this.options.context,
|
||||
},
|
||||
});
|
||||
this.cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;
|
||||
},
|
||||
/**
|
||||
* Sets the instance variables $editor, $body and selectorEditableArea.
|
||||
*
|
||||
* @param {JQuery} $editor
|
||||
* @param {String} selectorEditableArea
|
||||
*/
|
||||
setSelectorEditableArea: function ($editor, selectorEditableArea) {
|
||||
this.selectorEditableArea = selectorEditableArea;
|
||||
this.$editor = $editor;
|
||||
this.$body = $editor.closest('body');
|
||||
},
|
||||
/**
|
||||
* Get the editable area.
|
||||
*
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
getEditableArea: function () {
|
||||
return this.$editor.find(this.selectorEditableArea);
|
||||
},
|
||||
/**
|
||||
* Updates the cover dimensions of the current snippet editor.
|
||||
*/
|
||||
updateCurrentSnippetEditorOverlay: function () {
|
||||
if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) {
|
||||
this.$activeSnippet.data('snippet-editor').cover();
|
||||
}
|
||||
this.snippetEditors = _.filter(this.snippetEditors, function (snippetEditor) {
|
||||
if (snippetEditor.$target.closest('body').length) {
|
||||
return true;
|
||||
}
|
||||
snippetEditor.destroy();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -661,6 +769,7 @@ var SnippetsMenu = Widget.extend({
|
||||
* child
|
||||
*/
|
||||
_activateInsertionZones: function ($selectorSiblings, $selectorChildren) {
|
||||
var self = this;
|
||||
var zone_template = $('<div/>', {
|
||||
class: 'oe_drop_zone oe_insert',
|
||||
});
|
||||
@@ -668,8 +777,8 @@ var SnippetsMenu = Widget.extend({
|
||||
if ($selectorChildren) {
|
||||
$selectorChildren.each(function () {
|
||||
var $zone = $(this);
|
||||
var css = window.getComputedStyle(this);
|
||||
var parentCss = window.getComputedStyle($zone.parent()[0]);
|
||||
var css = self.window.getComputedStyle(this);
|
||||
var parentCss = self.window.getComputedStyle($zone.parent()[0]);
|
||||
var float = css.float || css.cssFloat;
|
||||
var parentDisplay = parentCss.display;
|
||||
var parentFlex = parentCss.flexDirection;
|
||||
@@ -680,7 +789,7 @@ var SnippetsMenu = Widget.extend({
|
||||
var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
|
||||
if (test) {
|
||||
$drop.addClass('oe_vertical').css({
|
||||
height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
|
||||
height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight),
|
||||
float: 'none',
|
||||
display: 'inline-block',
|
||||
});
|
||||
@@ -695,7 +804,7 @@ var SnippetsMenu = Widget.extend({
|
||||
test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
|
||||
if (test) {
|
||||
$drop.addClass('oe_vertical').css({
|
||||
height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
|
||||
height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight),
|
||||
float: 'none',
|
||||
display: 'inline-block'
|
||||
});
|
||||
@@ -715,8 +824,8 @@ var SnippetsMenu = Widget.extend({
|
||||
$selectorSiblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () {
|
||||
var $zone = $(this);
|
||||
var $drop;
|
||||
var css = window.getComputedStyle(this);
|
||||
var parentCss = window.getComputedStyle($zone.parent()[0]);
|
||||
var css = self.window.getComputedStyle(this);
|
||||
var parentCss = self.window.getComputedStyle($zone.parent()[0]);
|
||||
var float = css.float || css.cssFloat;
|
||||
var parentDisplay = parentCss.display;
|
||||
var parentFlex = parentCss.flexDirection;
|
||||
@@ -742,14 +851,14 @@ var SnippetsMenu = Widget.extend({
|
||||
var $zones;
|
||||
do {
|
||||
count = 0;
|
||||
$zones = this.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
|
||||
$zones = this.getEditableArea().find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
|
||||
count += $zones.length;
|
||||
$zones.remove();
|
||||
} while (count > 0);
|
||||
|
||||
// Cleaning consecutive zone and up zones placed between floating or
|
||||
// inline elements. We do not like these kind of zones.
|
||||
$zones = this.$editable.find('.oe_drop_zone:not(.oe_vertical)');
|
||||
$zones = this.getEditableArea().find('.oe_drop_zone:not(.oe_vertical)');
|
||||
$zones.each(function () {
|
||||
var zone = $(this);
|
||||
var prev = zone.prev();
|
||||
@@ -805,6 +914,7 @@ var SnippetsMenu = Widget.extend({
|
||||
}
|
||||
if ($snippet && $snippet.length) {
|
||||
var self = this;
|
||||
this.trigger_up('activate_snippet', $snippet);
|
||||
return this._createSnippetEditor($snippet).then(function (editor) {
|
||||
self.$activeSnippet = $snippet;
|
||||
editor.toggleFocus(true);
|
||||
@@ -821,16 +931,6 @@ var SnippetsMenu = Widget.extend({
|
||||
snippetEditor.destroy();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Updates the cover dimensions of the current snippet editor.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateCurrentSnippetEditorOverlay: function () {
|
||||
if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) {
|
||||
this.$activeSnippet.data('snippet-editor').cover();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Calls a given callback 'on' the given snippet and all its child ones if
|
||||
* any (DOM element with options).
|
||||
@@ -910,7 +1010,7 @@ var SnippetsMenu = Widget.extend({
|
||||
selectorConditions += ':has(' + target + ')';
|
||||
}
|
||||
if (!noCheck) {
|
||||
selectorConditions = ':o_editable' + selectorConditions;
|
||||
selectorConditions = (this.options.addDropSelector || '') + selectorConditions;
|
||||
}
|
||||
|
||||
// (Re)join the subselectors
|
||||
@@ -933,7 +1033,7 @@ var SnippetsMenu = Widget.extend({
|
||||
};
|
||||
} else {
|
||||
functions.closest = function ($from, parentNode) {
|
||||
var parents = self.$editable.get();
|
||||
var parents = self.getEditableArea().get();
|
||||
return $from.closest(selector, parentNode).filter(function () {
|
||||
var node = this;
|
||||
while (node.parentNode) {
|
||||
@@ -946,9 +1046,9 @@ var SnippetsMenu = Widget.extend({
|
||||
});
|
||||
};
|
||||
functions.all = isChildren ? function ($from) {
|
||||
return dom.cssFind($from || self.$editable, selector);
|
||||
return dom.cssFind($from || self.getEditableArea(), selector);
|
||||
} : function ($from) {
|
||||
$from = $from || self.$editable;
|
||||
$from = $from || self.getEditableArea();
|
||||
return $from.filter(selector).add(dom.cssFind($from, selector));
|
||||
};
|
||||
}
|
||||
@@ -1075,7 +1175,6 @@ var SnippetsMenu = Widget.extend({
|
||||
if (!this.$snippets.length) {
|
||||
this.$el.detach();
|
||||
}
|
||||
$('body').toggleClass('editor_has_snippets', this.$snippets.length > 0);
|
||||
|
||||
// Register the text nodes that needs to be auto-selected on click
|
||||
this._registerDefaultTexts();
|
||||
@@ -1123,7 +1222,7 @@ var SnippetsMenu = Widget.extend({
|
||||
}
|
||||
|
||||
return $.when(def).then(function (parentEditor) {
|
||||
snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions);
|
||||
snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions, self.getEditableArea(), self.options);
|
||||
self.snippetEditors.push(snippetEditor);
|
||||
return snippetEditor.appendTo(self.$snippetEditorArea);
|
||||
}).then(function () {
|
||||
@@ -1158,17 +1257,6 @@ var SnippetsMenu = Widget.extend({
|
||||
$snippet.toggleClass('o_disabled', !check);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns the URL where to find the snippets template. This URL might have
|
||||
* been set in the global 'snippetsURL' variable, otherwise this function
|
||||
* returns a default one.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getSnippetURL: function () {
|
||||
return odoo.snippetsURL || '/web_editor/snippets';
|
||||
},
|
||||
/**
|
||||
* Make given snippets be draggable/droppable thanks to their thumbnail.
|
||||
*
|
||||
@@ -1185,8 +1273,7 @@ var SnippetsMenu = Widget.extend({
|
||||
$snippets.draggable({
|
||||
greedy: true,
|
||||
helper: 'clone',
|
||||
zIndex: '1000',
|
||||
appendTo: 'body',
|
||||
appendTo: this.$body,
|
||||
cursor: 'move',
|
||||
handle: '.oe_snippet_thumbnail',
|
||||
distance: 30,
|
||||
@@ -1204,12 +1291,10 @@ var SnippetsMenu = Widget.extend({
|
||||
for (var k in temp) {
|
||||
if ($base_body.is(temp[k].base_selector) && !$base_body.is(temp[k].base_exclude)) {
|
||||
if (temp[k]['drop-near']) {
|
||||
if (!$selectorSiblings) $selectorSiblings = temp[k]['drop-near'].all();
|
||||
else $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
|
||||
$selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
|
||||
}
|
||||
if (temp[k]['drop-in']) {
|
||||
if (!$selectorChildren) $selectorChildren = temp[k]['drop-in'].all();
|
||||
else $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
|
||||
$selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1224,7 +1309,7 @@ var SnippetsMenu = Widget.extend({
|
||||
self._activateSnippet(false);
|
||||
self._activateInsertionZones($selectorSiblings, $selectorChildren);
|
||||
|
||||
$('.oe_drop_zone').droppable({
|
||||
self.getEditableArea().find('.oe_drop_zone').droppable({
|
||||
over: function () {
|
||||
if (!dropped) {
|
||||
dropped = true;
|
||||
@@ -1240,6 +1325,8 @@ var SnippetsMenu = Widget.extend({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
self.trigger_up('cover_will_change');
|
||||
},
|
||||
stop: function (ev, ui) {
|
||||
$toInsert.removeClass('oe_snippet_body');
|
||||
@@ -1252,7 +1339,7 @@ var SnippetsMenu = Widget.extend({
|
||||
}
|
||||
}
|
||||
|
||||
self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
|
||||
self.getEditableArea().find('.oe_drop_zone').droppable('destroy').remove();
|
||||
|
||||
if (dropped) {
|
||||
var prev = $toInsert.first()[0].previousSibling;
|
||||
@@ -1273,8 +1360,6 @@ var SnippetsMenu = Widget.extend({
|
||||
$parent.prepend($toInsert);
|
||||
}
|
||||
|
||||
$toInsert.closest('.o_editable').trigger('content_changed');
|
||||
|
||||
var $target = $toInsert;
|
||||
|
||||
_.defer(function () {
|
||||
@@ -1282,12 +1367,12 @@ var SnippetsMenu = Widget.extend({
|
||||
self._disableUndroppableSnippets();
|
||||
|
||||
self._callForEachChildSnippet($target, function (editor, $snippet) {
|
||||
_.defer(function () {
|
||||
editor.buildSnippet();
|
||||
});
|
||||
editor.buildSnippet();
|
||||
}).then(function () {
|
||||
$target.closest('.o_editable').trigger('content_changed');
|
||||
self._activateSnippet($target);
|
||||
$target.trigger('content_changed');
|
||||
if ($target.closest('body').length) { // can be destroyed (eg in test)
|
||||
self._activateSnippet($target);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -4,8 +4,9 @@ odoo.define('web_editor.snippets.options', function (require) {
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
|
||||
var weWidgets = require('web_editor.widget');
|
||||
var weWidgets = require('wysiwyg.widgets');
|
||||
var FontPlugin = require('web_editor.wysiwyg.plugin.font');
|
||||
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
|
||||
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -18,10 +19,12 @@ var _t = core._t;
|
||||
var SnippetOption = Widget.extend({
|
||||
events: {
|
||||
'mouseenter': '_onLinkEnter',
|
||||
'mouseenter .dropdown-item': '_onLinkEnter',
|
||||
'mouseenter a': '_onLinkEnter',
|
||||
'click': '_onLinkClick',
|
||||
'click a': '_onLinkClick',
|
||||
'mouseleave': '_onMouseleave',
|
||||
'mouseleave .dropdown-item': '_onMouseleave',
|
||||
'mouseleave a': '_onMouseleave',
|
||||
'mouseleave .dropdown-menu': '_onMouseleave',
|
||||
},
|
||||
/**
|
||||
* When editing a snippet, its options are shown alongside the ones of its
|
||||
@@ -38,9 +41,11 @@ var SnippetOption = Widget.extend({
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, $target, $overlay, data) {
|
||||
init: function (parent, $target, $overlay, data, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.options = options;
|
||||
this.$target = $target;
|
||||
this.ownerDocument = this.$target[0].ownerDocument;
|
||||
this.$overlay = $overlay;
|
||||
this.data = data;
|
||||
this.__methodNames = [];
|
||||
@@ -240,6 +245,7 @@ var SnippetOption = Widget.extend({
|
||||
if (!previewMode) {
|
||||
this._reset();
|
||||
this.trigger_up('request_history_undo_record', {$target: this.$target});
|
||||
this.$target.trigger('content_changed');
|
||||
}
|
||||
|
||||
// Search for methods (data-...) (i.e. data-toggle-class) on the
|
||||
@@ -353,7 +359,7 @@ var SnippetOption = Widget.extend({
|
||||
*/
|
||||
_onLinkClick: function (ev) {
|
||||
var $opt = $(ev.target).closest('.dropdown-item');
|
||||
if (!$opt.length || !$opt.is(':hasData')) {
|
||||
if (ev.isDefaultPrevented() || !$opt.length || !$opt.is(':hasData')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -440,7 +446,7 @@ registry.sizing = SnippetOption.extend({
|
||||
var regClass = new RegExp('\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
|
||||
|
||||
var cursor = $handle.css('cursor') + '-important';
|
||||
var $body = $(document.body);
|
||||
var $body = $(this.ownerDocument.body);
|
||||
$body.addClass(cursor);
|
||||
|
||||
var xy = ev['page' + XY];
|
||||
@@ -647,12 +653,19 @@ registry.colorpicker = SnippetOption.extend({
|
||||
}
|
||||
|
||||
if (!this.$el.find('.colorpicker').length) {
|
||||
// TODO remove old UI's code that survived
|
||||
var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
|
||||
var $clpicker = $(qweb.render('web_editor.colorpicker'));
|
||||
// Add common colors to palettes if not excluded
|
||||
var fontPlugin = new FontPlugin({
|
||||
layoutInfo: {
|
||||
editable: $('<div/>'),
|
||||
editingArea: $('<div/>'),
|
||||
},
|
||||
options: {},
|
||||
});
|
||||
|
||||
_.each($clpicker.find('.o_colorpicker_section'), function (elem) {
|
||||
$(elem).prepend("<div class='text-muted mt8'>" + elem.dataset.display + "</div>");
|
||||
var $clpicker = fontPlugin.createPalette('backColor').find('.note-color-palette'); // don't use custom color
|
||||
$clpicker.find('.note-color-reset').remove();
|
||||
$clpicker.find('h6').each(function () {
|
||||
$(this).replaceWith($('<div class="mt8"/>').text($(this).text()));
|
||||
});
|
||||
|
||||
// Retrieve excluded palettes list
|
||||
@@ -662,7 +675,7 @@ registry.colorpicker = SnippetOption.extend({
|
||||
}
|
||||
// Apply a custom title if specified
|
||||
if (this.data.paletteTitle) {
|
||||
$pt.find('.note-palette-title').text(this.data.paletteTitle);
|
||||
$clpicker.find('.note-palette-title').text(this.data.paletteTitle);
|
||||
}
|
||||
|
||||
// Remove excluded palettes
|
||||
@@ -670,39 +683,29 @@ registry.colorpicker = SnippetOption.extend({
|
||||
$clpicker.find('[data-name="' + exc + '"]').remove();
|
||||
});
|
||||
|
||||
// Add common colors to palettes if not excluded
|
||||
if (!('common' in excluded)) {
|
||||
var $commonColorSection = $clpicker.find('[data-name="common"]');
|
||||
_.each(summernoteCustomColors, function (colorRow, i) {
|
||||
var $div = $('<div/>', {class: 'clearfix'}).appendTo($commonColorSection);
|
||||
if (i === 0) {
|
||||
// Ignore the summernote gray palette and use ours
|
||||
return;
|
||||
}
|
||||
_.each(colorRow, function (color) {
|
||||
$div.append('<button class="o_custom_color" style="background-color: ' + color + '" />');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
|
||||
$pt.find('.o_colorpicker_section_tabs').append($clpicker);
|
||||
this.$el.find('.dropdown-menu').append($pt);
|
||||
}
|
||||
|
||||
var bgColor = ColorpickerDialog.formatColor(self.$target.css('background-color'));
|
||||
var classes = [];
|
||||
this.$el.find('.colorpicker button').each(function () {
|
||||
var $color = $(this);
|
||||
var color = $color.data('color');
|
||||
if (!color) {
|
||||
return;
|
||||
if (color) {
|
||||
$color.addClass('bg-' + color);
|
||||
var className = self.colorPrefix + color;
|
||||
if (self.$target.hasClass(className)) {
|
||||
$color.addClass('selected');
|
||||
}
|
||||
classes.push(className);
|
||||
} else {
|
||||
color = $color.data('value');
|
||||
if (bgColor === ColorpickerDialog.formatColor(color)) {
|
||||
$color.addClass('selected');
|
||||
}
|
||||
}
|
||||
|
||||
$color.addClass('bg-' + color);
|
||||
var className = self.colorPrefix + color;
|
||||
if (self.$target.hasClass(className)) {
|
||||
$color.addClass('selected');
|
||||
}
|
||||
classes.push(className);
|
||||
});
|
||||
this.classes = classes.join(' ');
|
||||
|
||||
@@ -736,6 +739,9 @@ registry.colorpicker = SnippetOption.extend({
|
||||
var color = $(ev.currentTarget).data('color');
|
||||
if (color) {
|
||||
this.$target.addClass(this.colorPrefix + color);
|
||||
} else if ($(ev.currentTarget).data('value')) {
|
||||
color = $(ev.currentTarget).data('value');
|
||||
this.$target.css('background-color', color);
|
||||
} else if ($(ev.target).hasClass('o_custom_color')) {
|
||||
this.$target
|
||||
.removeClass(this.classes)
|
||||
@@ -828,7 +834,7 @@ registry.background = SnippetOption.extend({
|
||||
*/
|
||||
chooseImage: function (previewMode, value, $opt) {
|
||||
// Put fake image in the DOM, edit it and use it as background-image
|
||||
var $image = $('<img/>', {class: 'd-none', src: value}).appendTo(this.$target);
|
||||
var $image = $('<img/>', {class: 'd-none', src: value === 'true' ? '' : value}).appendTo(this.$target);
|
||||
|
||||
var $editable = this.$target.closest('.o_editable');
|
||||
var _editor = new weWidgets.MediaDialog(this, {
|
||||
@@ -836,10 +842,11 @@ registry.background = SnippetOption.extend({
|
||||
firstFilters: ['background'],
|
||||
res_model: $editable.data('oe-model'),
|
||||
res_id: $editable.data('oe-id'),
|
||||
}, null, $image[0]).open();
|
||||
}, $image[0]).open();
|
||||
|
||||
_editor.on('save', this, function () {
|
||||
this._setCustomBackground($image.attr('src'));
|
||||
this.$target.trigger('content_changed');
|
||||
});
|
||||
_editor.on('closed', this, function () {
|
||||
$image.remove();
|
||||
@@ -894,7 +901,8 @@ registry.background = SnippetOption.extend({
|
||||
if (value === undefined) {
|
||||
value = this.$target.css('background-image');
|
||||
}
|
||||
return value.replace(/url\(['"]*|['"]*\)|^none$/g, '');
|
||||
var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g;
|
||||
return value && value.replace(srcValueWrapper, '') || '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -1159,6 +1167,11 @@ registry.many2one = SnippetOption.extend({
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.trigger_up('getRecordInfo', _.extend(this.options, {
|
||||
callback: function (recordInfo) {
|
||||
_.defaults(self.options, recordInfo);
|
||||
},
|
||||
}));
|
||||
|
||||
this.Model = this.$target.data('oe-many2one-model');
|
||||
this.ID = +this.$target.data('oe-many2one-id');
|
||||
@@ -1191,6 +1204,7 @@ registry.many2one = SnippetOption.extend({
|
||||
this.$search.find('input')
|
||||
.focus()
|
||||
.on('keyup', function (e) {
|
||||
self.$overlay.removeClass('o_keypress');
|
||||
self._findExisting($(this).val());
|
||||
});
|
||||
|
||||
@@ -1257,6 +1271,7 @@ registry.many2one = SnippetOption.extend({
|
||||
kwargs: {
|
||||
order: [{name: 'name', asc: false}],
|
||||
limit: 5,
|
||||
context: this.options.context,
|
||||
},
|
||||
}).then(function (result) {
|
||||
self.$search.siblings().remove();
|
||||
@@ -1295,6 +1310,7 @@ registry.many2one = SnippetOption.extend({
|
||||
args: [[self.ID]],
|
||||
kwargs: {
|
||||
options: options,
|
||||
context: self.options.context,
|
||||
},
|
||||
}).then(function (html) {
|
||||
$node.html(html);
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
odoo.define('web_editor.wysiwyg.snippets', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
var snippetsEditor = require('web_editor.snippet.editor');
|
||||
|
||||
Wysiwyg.include({
|
||||
events: _.extend({}, Wysiwyg.prototype.events, {
|
||||
'content_changed .o_editable': '_onContentChange',
|
||||
'content_changed .note-editable': '_onContentChange',
|
||||
}),
|
||||
custom_events: _.extend({}, Wysiwyg.prototype.custom_events, {
|
||||
request_history_undo_record: '_onHistoryUndoRecordRequest',
|
||||
cover_will_change: '_onCoverWillChange',
|
||||
snippet_cloned: '_onSnippetCloned',
|
||||
snippet_dropped: '_onSnippetDropped',
|
||||
snippet_focused: '_onSnippetFocused',
|
||||
}),
|
||||
|
||||
selectorEditableArea: '.note-editable',
|
||||
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
options = _.clone(this.options);
|
||||
if (!options.snippets) {
|
||||
return;
|
||||
}
|
||||
if (options.snippets === true) {
|
||||
options.snippets = 'web_editor.snippets';
|
||||
}
|
||||
options.isUnbreakableNode = this.isUnbreakableNode.bind(this);
|
||||
options.isEditableNode = this.isEditableNode.bind(this);
|
||||
this.snippets = new snippetsEditor.Class(this, options);
|
||||
},
|
||||
/**
|
||||
* Preload snippets.
|
||||
*
|
||||
* @override
|
||||
**/
|
||||
willStart: function () {
|
||||
if (this.snippets) {
|
||||
this.snippets.loadSnippets(); // don't use the deferred
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
/**
|
||||
* Add options (snippets) to load snippet building block
|
||||
* snippets can by url begin with '/' or an view xml_id.
|
||||
*
|
||||
* @override
|
||||
* @param {string} [options.snippets]
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super().then(function () {
|
||||
if (!self.snippets) {
|
||||
return;
|
||||
}
|
||||
var $editable = $(self._summernote.layoutInfo.editable);
|
||||
self.snippets.setSelectorEditableArea(self.$el, self.selectorEditableArea);
|
||||
self.snippets.insertBefore(self.$el).then(function () {
|
||||
self.$el.before(self.snippets.$el);
|
||||
var $wrap = $('<div class="o_wrap_editable_snippets"/>');
|
||||
$wrap.on('scroll', function (event) {
|
||||
self._summernote.triggerEvent('scroll', event);
|
||||
});
|
||||
var $contents = self.snippets.$el.siblings('#oe_manipulators')
|
||||
.addClass('o_wysiwyg_to_remove').attr('contentEditable', false);
|
||||
$wrap.append($contents);
|
||||
$editable.before($wrap);
|
||||
$wrap.append($editable);
|
||||
setTimeout(function () { // add a set timeout for the transition
|
||||
self.snippets.$el.addClass('o_loaded');
|
||||
self.$el.addClass('o_snippets_loaded');
|
||||
self.trigger_up('snippets_loaded', self.snippets.$el);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
isUnbreakableNode: function (node) {
|
||||
if (!this.snippets) {
|
||||
return this._super(node);
|
||||
}
|
||||
return this._super(node) || node.tagName === 'DIV' || snippetsEditor.globalSelector.is($(node));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
isEditableNode: function (node) {
|
||||
if (!this._super(node)) {
|
||||
return false;
|
||||
}
|
||||
if (this.snippets && node.tagName === 'DIV' && !node.contentEditable) {
|
||||
if ($(node).parent('.row').length) {
|
||||
return true;
|
||||
}
|
||||
if (!_.find(node.childNodes, function (node) {
|
||||
var char = this.context.invoke('HelperPlugin.getRegex', 'char');
|
||||
return node.tagName !== 'DIV' && (node.tagName || node.textContent.match(char));
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
if (!this.snippets) {
|
||||
return this._super();
|
||||
}
|
||||
var defs = [];
|
||||
this.trigger_up('ready_to_save', {defs: defs});
|
||||
return $.when.apply($, defs)
|
||||
.then(this.snippets.cleanForSave.bind(this.snippets))
|
||||
.then(this._super.bind(this));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryEvent} ev
|
||||
*/
|
||||
_onContentChange: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (this.snippets) {
|
||||
this.addHistoryStep();
|
||||
}
|
||||
this._onChange();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_onChange: function () {
|
||||
if (this.snippets) {
|
||||
this.snippets.updateCurrentSnippetEditorOverlay();
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Called when an element askes to record an history undo -> records it.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onHistoryUndoRecordRequest: function () {
|
||||
this.addHistoryStep();
|
||||
},
|
||||
/**
|
||||
* Triggered when the user click on the snippet to drag&drop, remove or clone
|
||||
* (hide the popover)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onCoverWillChange: function () {
|
||||
var context = this._summernote;
|
||||
var target = context.invoke('editor.restoreTarget', target);
|
||||
context.invoke('MediaPlugin.hidePopovers');
|
||||
},
|
||||
/**
|
||||
* Triggered when a snippet is cloned in the editable area
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {Object} ev.data
|
||||
* @param {Object} ev.data.$target
|
||||
*/
|
||||
_onSnippetCloned: function (ev) {
|
||||
this._summernote.invoke('UnbreakablePlugin.secureArea', ev.data.$target[0]);
|
||||
},
|
||||
/**
|
||||
* Triggered when a snippet is dropped in the editable area
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {Object} ev.data
|
||||
* @param {Object} ev.data.$target
|
||||
*/
|
||||
_onSnippetDropped: function (ev) {
|
||||
this._summernote.invoke('UnbreakablePlugin.secureArea', ev.data.$target[0]);
|
||||
},
|
||||
/**
|
||||
* Triggered when the user focuses on a snippet: hides the popover.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSnippetFocused: function () {
|
||||
var context = this._summernote;
|
||||
var target = context.invoke('editor.restoreTarget', target);
|
||||
context.invoke('MediaPlugin.hidePopovers');
|
||||
context.invoke('MediaPlugin.update', target);
|
||||
},
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// jQuery extensions
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
$.fn.extend({
|
||||
/**
|
||||
* Set the range and focus at the start of the first found node.
|
||||
*
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
focusIn: function () {
|
||||
if (this.length) {
|
||||
Wysiwyg.setRangeFromNode(this[0], {begin: true});
|
||||
$(this).trigger('mouseup');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
/**
|
||||
* Set the range and focus at the end of the first found node.
|
||||
*
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
focusInEnd: function () {
|
||||
if (this.length) {
|
||||
Wysiwyg.setRangeFromNode(this[0], {end: true});
|
||||
$(this).trigger('mouseup');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
/**
|
||||
* Set the range and focus on a selection, accounting for zero-width spaces.
|
||||
*
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
selectContent: function () {
|
||||
if (this.length) {
|
||||
Wysiwyg.setRangeFromNode(this[0]);
|
||||
var range = $.summernote.range.create();
|
||||
if (!range.sc.tagName && range.so === 0 && range.sc.textContent[range.so] === '\u200B') {
|
||||
range.so += 1;
|
||||
}
|
||||
if (!range.ec.tagName && range.eo === range.ec.textContent.length && range.ec.textContent[range.eo - 1] === '\u200B') {
|
||||
range.eo -= 1;
|
||||
}
|
||||
range.select();
|
||||
$(this).trigger('mouseup');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,23 +1,24 @@
|
||||
.oe_form_field_html_text {
|
||||
.oe_form_field_html {
|
||||
position: relative;
|
||||
|
||||
.note-editable {
|
||||
min-height: 180px;
|
||||
font: initial !important;
|
||||
font: inherit !important;
|
||||
font-family: inherit !important;
|
||||
line-height: initial !important;
|
||||
color: initial !important;
|
||||
line-height: inherit !important;
|
||||
color: inherit !important;
|
||||
overflow: visible;
|
||||
|
||||
p, div {
|
||||
font-family: 'Lucida Grande', Helvetica, Verdana, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
a, a:hover {
|
||||
color: initial;
|
||||
}
|
||||
ul > li > p {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
.o_readonly {
|
||||
min-height: 1em;
|
||||
ul > li > p, p {
|
||||
margin: 0px;
|
||||
}
|
||||
> iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -26,66 +27,15 @@
|
||||
ul > li > p {
|
||||
margin: 0px;
|
||||
}
|
||||
min-height: 300px;
|
||||
min-height: -webkit-calc(100vh - 170px);
|
||||
min-height: calc(100vh - 170px);
|
||||
}
|
||||
}
|
||||
.oe_form_field_html iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-height: 300px;
|
||||
min-height: -webkit-calc(100vh - 170px);
|
||||
min-height: calc(100vh - 170px);
|
||||
}
|
||||
.o_field_widgetTextHtml_fullscreen {
|
||||
.oe_form_field_html.o_form_fullscreen_ancestor iframe {
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
min-height: 100% !important;
|
||||
z-index: 1001 !important;
|
||||
border: 0;
|
||||
.rounded {
|
||||
border-radius: .25rem !important;
|
||||
}
|
||||
* {
|
||||
display: none;
|
||||
}
|
||||
.o_form_fullscreen_ancestor {
|
||||
display: block !important;
|
||||
position: static !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: auto !important;
|
||||
overflow: hidden !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.openerp .oe_form .oe_form_field_html_translate textarea {
|
||||
min-width: 60px;
|
||||
width: 100%;
|
||||
color: #4c4c4c;
|
||||
font: normal 13px "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
|
||||
}
|
||||
.openerp .oe_form_readonly .oe_form .oe_form_field_html_translate textarea {
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.openerp .oe_form_editable .oe_form .oe_form_field_html_translate {
|
||||
> button {
|
||||
float: right;
|
||||
margin: -6px -3px 0 0;
|
||||
}
|
||||
> .note-editable {
|
||||
border: 1px solid #ccc;
|
||||
min-height: 22px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.note-editable [contentEditable='true']:hover {
|
||||
box-shadow: #DDCC33 0 0 5px 2px inset;
|
||||
table.table.table-bordered {
|
||||
table-layout: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,46 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
// List
|
||||
ul.o_checklist {
|
||||
list-style: none;
|
||||
|
||||
>li {
|
||||
position: relative;
|
||||
margin-left: $o-checklist-margin-left;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: - $o-checklist-margin-left;
|
||||
display: block;
|
||||
height: $o-checklist-before-size;
|
||||
width: $o-checklist-before-size;
|
||||
margin-top: 4px;
|
||||
border: 1px solid;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.o_checked {
|
||||
text-decoration: line-through;
|
||||
&::after {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: - ($o-checklist-margin-left - $o-checklist-checkmark-width);
|
||||
top: +1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ul > li.o_indent {
|
||||
margin-left: 0;
|
||||
list-style: none;
|
||||
&::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Medias
|
||||
img.shadow {
|
||||
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
@@ -55,6 +95,9 @@ img.padding-large, .img.padding-large, span.fa.padding-large, iframe.padding-lar
|
||||
img.padding-xl, .img.padding-xl, span.fa.padding-xl, iframe.padding-xl {
|
||||
padding: 32px;
|
||||
}
|
||||
img.ml-auto, img.mx-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fa-6x {
|
||||
font-size: 6em;
|
||||
@@ -80,6 +123,7 @@ div.media_iframe_video {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-width: 100px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
@@ -109,7 +153,7 @@ div.media_iframe_video {
|
||||
}
|
||||
|
||||
.css_editable_mode_display {
|
||||
@include o-position-absolute;
|
||||
@include o-position-absolute(0,0,0,0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ $o-we-color-normal: #50545d !default;
|
||||
$o-we-color-paper: #fcfcfc !default;
|
||||
$o-we-color-text-normal: #999999 !default;
|
||||
$o-we-color-text-light: #d4d5d7 !default;
|
||||
$o-we-color-text-lighter: #ebecee !default;
|
||||
$o-we-color-danger: #e6586c !default;
|
||||
$o-we-color-warning: #f0ad4e !default;
|
||||
$o-we-color-success: #40ad67 !default;
|
||||
@@ -42,6 +43,11 @@ $o-we-ace-color: #2F3129 !default;
|
||||
$o-we-switch-size: 2ex !default;
|
||||
$o-we-switch-inactive-color: $o-we-color-paper !default;
|
||||
|
||||
// Checklist
|
||||
$o-checklist-margin-left: 20px;
|
||||
$o-checklist-checkmark-width: 2px;
|
||||
$o-checklist-before-size: 13px;
|
||||
|
||||
// ============ 'Preview' component Mixins ===============
|
||||
// Create a "preview" work-area to edit/customize specific elements
|
||||
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
// NOTE EDITOR
|
||||
|
||||
.note-editor.note-frame {
|
||||
background-color: inherit;
|
||||
|
||||
.note-editing-area .note-editable {
|
||||
background-color: transparent !important;
|
||||
color: inherit;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.note-popover.popover {
|
||||
z-index: $o-wysiwyg-zindex;
|
||||
height: auto;
|
||||
|
||||
.popover-content {
|
||||
padding-bottom: 0;
|
||||
max-width: 334px;
|
||||
|
||||
>note-btn-group {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-editor .note-toolbar {
|
||||
white-space: nowrap;
|
||||
z-index: $o-wysiwyg-zindex + 10;
|
||||
height: $o-wysiwyg-toolbar-height;
|
||||
}
|
||||
|
||||
.note-popover.popover .popover-content,
|
||||
.note-editor .note-toolbar {
|
||||
font-family: $o-wysiwyg-font-family;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6 {
|
||||
font-family: $o-wysiwyg-font-family;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: $o-wysiwyg-toolbar-height;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border-top: 2px solid transparent;
|
||||
transition: all 0.3s ease 0s;
|
||||
|
||||
>.fa {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-top: 2px solid inherit;
|
||||
color: inherit;
|
||||
|
||||
>.fa {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.o_image_alt {
|
||||
@include o-text-overflow(inline-block);
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-top: 2px solid white;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.5);
|
||||
|
||||
>.fa {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-btn-group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
>.btn-group {
|
||||
margin-top: 0;
|
||||
|
||||
&.show,
|
||||
.btn-group.show {
|
||||
.dropdown-toggle {
|
||||
border-top: 2px solid white;
|
||||
background-color: $o-wysiwyg-bg-color-dark;
|
||||
box-shadow: none;
|
||||
|
||||
>.fa,
|
||||
>span {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
|
||||
.dropdown-item {
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
|
||||
>.fa {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $o-wysiwyg-bg-color-dark;
|
||||
color: inherit;
|
||||
|
||||
>.fa {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-para .dropdown-menu {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-color {
|
||||
|
||||
.note-back-color-preview,
|
||||
.note-fore-color-preview {
|
||||
.dropdown-toggle {
|
||||
padding: 0px 7px;
|
||||
height: 35px;
|
||||
width: auto;
|
||||
border-top: none;
|
||||
border-bottom: 3px solid;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 180px;
|
||||
|
||||
.note-custom-color {
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: gray('400');
|
||||
color: black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-palette:first-child {
|
||||
width: 100%;
|
||||
|
||||
.note-color-reset,
|
||||
.note-custom-color {
|
||||
width: 162px;
|
||||
}
|
||||
|
||||
.note-color-reset {
|
||||
padding: 0 3px !important;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.note-holder,
|
||||
.note-color-palette .note-color-row,
|
||||
.note-custom-color-palette .note-color-row {
|
||||
height: auto !important;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.note-color-btn {
|
||||
float: left;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin: 1px 1px 0 0;
|
||||
border: 1px solid black;
|
||||
cursor: pointer;
|
||||
@include o-alpha-button-preview;
|
||||
|
||||
&.o_small {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 3px 3px 2px 2px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.o_clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&.o_btn_transparent {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-popover.popover.note-hint-popover .popover-content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.note-popover,
|
||||
.note-editor {
|
||||
.modal .modal-footer p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-handle {
|
||||
pointer-events: none;
|
||||
|
||||
.note-control-selection {
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.note-control-se {
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_fake_editable:not([data-oe-model]):focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.note-editor.dragover .note-dropzone {
|
||||
z-index: $o-wysiwyg-zindex + 1;
|
||||
}
|
||||
|
||||
// MODALS
|
||||
|
||||
body .modal {
|
||||
|
||||
// SELECT MEDIA
|
||||
.o_select_media_dialog {
|
||||
max-width: 80%;
|
||||
|
||||
.modal-body {
|
||||
.tab-pane {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.existing-attachments {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_existing_attachment_cell {
|
||||
$o-border-image: 2px;
|
||||
|
||||
.o_existing_attachment_remove {
|
||||
@include o-position-absolute($o-border-image, 15px + $o-border-image);
|
||||
padding: 3px 5px 5px 5px;
|
||||
background-color: rgba(white, 0.8);
|
||||
text-shadow: 0 0 1px white;
|
||||
border-radius: 0 0 0 2px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
color: $o-wysiwyg-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.o_image {
|
||||
border: $o-border-image solid transparent;
|
||||
cursor: pointer;
|
||||
transition: opacity 400ms ease 0s;
|
||||
|
||||
&.o_webimage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
&.o_image_loading {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_selected .o_image {
|
||||
border-color: $o-brand-odoo;
|
||||
box-shadow: 0px 0px 2px 2px $o-brand-secondary;
|
||||
}
|
||||
|
||||
&:hover .o_existing_attachment_remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.font-icons-icons {
|
||||
>span {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
margin: 5px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&.o_selected {
|
||||
cursor: auto;
|
||||
background-color: $o-wysiwyg-bg-color-dark;
|
||||
box-shadow: 0px 0px 0px 1px $o-wysiwyg-color-success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editor-media-image,
|
||||
#editor-media-document {
|
||||
.o_we_url_input {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
// VIDEO TAB
|
||||
#editor-media-video {
|
||||
.o_video_dialog_form {
|
||||
#o_video_form_group {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
>textarea {
|
||||
width: 100%;
|
||||
min-height: 95px;
|
||||
padding-bottom: 25px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.o_validate_feedback {
|
||||
@include o-position-absolute(auto, 10px, 10px);
|
||||
|
||||
>.fa {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_has_error .o_validate_feedback>.fa-exclamation-triangle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.o_has_success .o_validate_feedback>.fa-check {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#video-preview {
|
||||
position: relative;
|
||||
@include o-we-preview-box();
|
||||
border: none;
|
||||
|
||||
.media_iframe_video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_video_dialog_iframe {
|
||||
@include o-we-preview-content;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
&.alert {
|
||||
animation: fadeInDownSmall 700ms forwards;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Crop Dialog
|
||||
.o_crop_image_dialog .o_crop_area {
|
||||
height: 350px;
|
||||
|
||||
.cropper-point {
|
||||
background-color: white;
|
||||
transition: all 400ms $o-wysiwyg-md-ease 0s;
|
||||
transform: scale(1.5, 1.5);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 1px 1px rgba(23, 23, 23, 0.5);
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
transform: scale(2.5, 2.5);
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-line {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 2px solid white;
|
||||
}
|
||||
}
|
||||
|
||||
// LINK EDITOR DIALOG COLOR SELECTOR
|
||||
.o_link_dialog {
|
||||
.o_link_dialog_color>.o_link_dialog_color_item {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0; // Important for themes
|
||||
border: 2px solid rgba(0, 0, 0, 0.25);
|
||||
border-radius: 50%;
|
||||
|
||||
input:checked+i::before {
|
||||
content: "\f00c";
|
||||
@include o-position-absolute($top: 6px, $left: 5px);
|
||||
}
|
||||
|
||||
&.btn-link {
|
||||
padding: 3px 9px;
|
||||
|
||||
>span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_link_dialog_preview {
|
||||
border-left: 1px solid gray('200');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight selected image/icon
|
||||
%o-we-selected-image {
|
||||
$o-selected-image-color: rgba(150, 150, 220, 0.3);
|
||||
background: $o-selected-image-color;
|
||||
outline: 3px solid $o-selected-image-color;
|
||||
}
|
||||
|
||||
img.o_we_selected_image {
|
||||
@extend %o-we-selected-image;
|
||||
}
|
||||
|
||||
.fa.o_we_selected_image::before {
|
||||
@extend %o-we-selected-image;
|
||||
}
|
||||
|
||||
.fa.mx-auto,
|
||||
img.o_we_custom_image.mx-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// ENTER IN EDIT MODE
|
||||
|
||||
.note-editor .css_editable_mode_display {
|
||||
border: 1px dashed rgba(153, 153, 153, 0.5);
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.note-editable {
|
||||
|
||||
li,
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
&:empty:after {
|
||||
content: "\00a0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wysiwyg_iframe {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
iframe.wysiwyg_iframe.o_fullscreen {
|
||||
position: fixed !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
min-height: 100% !important;
|
||||
z-index: 1001 !important;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
body.o_in_iframe {
|
||||
background-color: $o-view-background-color;
|
||||
overflow: hidden;
|
||||
|
||||
.note-editable {
|
||||
height: calc(100vh - #{$o-wysiwyg-toolbar-height} - 2px);
|
||||
}
|
||||
|
||||
.note-statusbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,872 @@
|
||||
///
|
||||
/// This file contains all variables and mixins that are specific to the editor.
|
||||
///
|
||||
|
||||
$o-wysiwyg-handles-accent-color: #00e2ff !default;
|
||||
$o-wysiwyg-handle-edge-size: 1px !default;
|
||||
$o-wysiwyg-dropzone-size: 40px !default;
|
||||
$o-wysiwyg-sidebar-width: 210px !default;
|
||||
$o-wysiwyg-sidebar-height: 36px !default;
|
||||
$o-wysiwyg-overlay-option-size: 25px !default;
|
||||
$o-wysiwyg-handles-btn-size: 18px !default;
|
||||
$o-wysiwyg-handle-border-width: 1px !default;
|
||||
|
||||
$o-wysiwyg-handles-offset-to-hide: 10000px !default;
|
||||
|
||||
|
||||
// NOTE EDITOR
|
||||
.note-editor.o_snippets_loaded {
|
||||
transition: padding-left 400ms $o-wysiwyg-md-ease 0s;
|
||||
padding-left: 210px !important;
|
||||
}
|
||||
|
||||
.note-editor.note-frame .note-editing-area .o_wrap_editable_snippets {
|
||||
overflow: auto;
|
||||
|
||||
.note-editable {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.o_in_iframe .note-editor.o_snippets_loaded .note-editable {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
// SNIPPET PANEL
|
||||
#oe_snippets {
|
||||
@include o-w-preserve-btn;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
@include o-position-absolute(0, auto, 0, -$o-wysiwyg-sidebar-width);
|
||||
position: absolute;
|
||||
width: $o-wysiwyg-sidebar-width;
|
||||
z-index: $o-wysiwyg-zindex - 1;
|
||||
|
||||
font-family: Roboto, $font-family-sans-serif;
|
||||
border-right: 1px solid #aaa;
|
||||
transition: left 400ms $o-wysiwyg-md-ease 0s;
|
||||
background-image: linear-gradient(45deg, $o-wysiwyg-bg-color, darken($o-wysiwyg-bg-color, 10%));
|
||||
box-shadow: 0px 10px 10px -10px black inset;
|
||||
border: 1px solid $o-wysiwyg-border-color;
|
||||
|
||||
#snippets_menu {
|
||||
flex: 0 0 auto;
|
||||
height: $o-wysiwyg-sidebar-height;
|
||||
line-height: $o-wysiwyg-sidebar-height;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: $o-wysiwyg-bg-color;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 17px;
|
||||
color: $o-wysiwyg-color;
|
||||
font-family: $o-wysiwyg-font-family;
|
||||
}
|
||||
|
||||
#o_scroll {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.o_panel_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3%;
|
||||
color: $o-wysiwyg-color;
|
||||
margin-bottom: 3%;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_panel_body {
|
||||
&:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.oe_snippet {
|
||||
float: left;
|
||||
width: 45.5%;
|
||||
margin: 0 0 6% 3%;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
user-select: none;
|
||||
|
||||
cursor: pointer;
|
||||
cursor: copy;
|
||||
cursor: grab;
|
||||
|
||||
.oe_snippet_thumbnail {
|
||||
.oe_snippet_thumbnail_img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.oe_snippet_thumbnail_title {
|
||||
border: none;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
&:hover .oe_snippet_thumbnail_img {
|
||||
transition: transform 200ms ease 0s;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
&.o_disabled {
|
||||
.oe_snippet_thumbnail {
|
||||
background-color: rgba(255, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.oe_snippet_thumbnail_img {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_snippet_install {
|
||||
.oe_snippet_thumbnail_img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn.o_install_btn {
|
||||
display: none;
|
||||
@include o-position-absolute(16px, 4px, auto, 4px);
|
||||
}
|
||||
|
||||
&:hover .btn.o_install_btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n+1) {
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_loaded {
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oe_snippet {
|
||||
// No root because can be drag and drop (and the helper is in the body)
|
||||
position: relative;
|
||||
width: 100px;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
z-index: $o-wysiwyg-zindex;
|
||||
|
||||
> :not(.oe_snippet_thumbnail) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.oe_snippet_thumbnail {
|
||||
width: 100%;
|
||||
|
||||
.oe_snippet_thumbnail_img {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border: 1px solid $o-wysiwyg-border-color;
|
||||
padding-bottom: 75%;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img.oe_snippet_thumbnail_img {
|
||||
height: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.oe_snippet_thumbnail_title {
|
||||
display: block;
|
||||
border: 1px solid $o-wysiwyg-bg-color;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
text-shadow: none;
|
||||
color: $o-wysiwyg-color;
|
||||
}
|
||||
|
||||
&:hover .oe_snippet_thumbnail_title {
|
||||
color: $o-wysiwyg-color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SNIPPET OPTIONS
|
||||
.colorpicker {
|
||||
.o_colorpicker_sections {
|
||||
font-size: 14px;
|
||||
|
||||
.o_colorpicker_section_tabs {
|
||||
width: 225px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
float: left;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
margin: 1px 1px 0 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
div[data-group="transparent_grayscale"] button {
|
||||
@include o-alpha-button-preview;
|
||||
|
||||
&.o_small {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
margin: 2px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
box-shadow: 0px 0px 2px 2px $o-wysiwyg-color;
|
||||
}
|
||||
|
||||
&.selected:before {
|
||||
content: "\f00c";
|
||||
font-family: "FontAwesome";
|
||||
padding-top: 1px;
|
||||
color: theme-color('success');
|
||||
}
|
||||
|
||||
&[data-event="foreColor"] {
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
|
||||
&:before {
|
||||
background-color: rgba(white, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&.o_btn_transparent::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.note-color-palette>div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.note-color-row {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.note-palette-title {
|
||||
padding: 10px 0px 10px 10px;
|
||||
color: $o-wysiwyg-color-light;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.palette-reset {
|
||||
@include o-position-absolute(0, 0);
|
||||
margin: 0;
|
||||
padding: 5px 10px 0;
|
||||
|
||||
.note-color-reset {
|
||||
font-size: 20px !important;
|
||||
color: desaturate(rgba($o-wysiwyg-color-danger, 0.6), 40%);
|
||||
margin: 1px 0 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: transparent !important;
|
||||
color: $o-wysiwyg-color-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DROPZONES
|
||||
@keyframes dropZoneInsert {
|
||||
to {
|
||||
background-color: rgba($o-brand-odoo, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.note-editable .oe_drop_zone {
|
||||
background-color: rgba($o-brand-odoo, 0.05);
|
||||
animation: dropZoneInsert 1s linear 0s infinite alternate;
|
||||
|
||||
&.oe_insert {
|
||||
z-index: $o-wysiwyg-zindex - 1;
|
||||
|
||||
border: 2px dashed $o-wysiwyg-border-color;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: $o-wysiwyg-dropzone-size;
|
||||
margin: (-$o-wysiwyg-dropzone-size/2) 0;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
border-bottom: 2px dashed $o-wysiwyg-border-color;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
&.oe_vertical {
|
||||
border: 2px dashed $o-brand-odoo;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
|
||||
width: $o-wysiwyg-dropzone-size;
|
||||
float: left;
|
||||
margin: 0 (-$o-wysiwyg-dropzone-size/2);
|
||||
|
||||
&:after {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
border-bottom: none;
|
||||
border-right: 2px dashed $o-wysiwyg-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MANIPULATORS
|
||||
#oe_manipulators {
|
||||
position: relative;
|
||||
|
||||
// SNIPPET MANIPULATORS
|
||||
.oe_overlay {
|
||||
@include o-position-absolute;
|
||||
display: none;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
transition: opacity 400ms linear 0s;
|
||||
|
||||
&.o_keypress {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.oe_active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// OVERLAY OPTIONS
|
||||
>.oe_overlay_options {
|
||||
font-family: $o-wysiwyg-font-family;
|
||||
@include o-position-absolute($bottom: 0, $left: 0);
|
||||
width: 250px; // the parent oe_overlay has a width equal to the snippet width, the 250px is there to force the options to stay on one line without overriding bootstrap .btn-group
|
||||
text-align: left;
|
||||
z-index: $o-wysiwyg-zindex + 1;
|
||||
|
||||
>.btn-group {
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
width: $o-wysiwyg-overlay-option-size;
|
||||
height: $o-wysiwyg-overlay-option-size;
|
||||
padding: 0;
|
||||
border: 1px solid $o-wysiwyg-bg-color;
|
||||
line-height: $o-wysiwyg-overlay-option-size;
|
||||
font-size: 11px;
|
||||
border-radius: 0;
|
||||
font-weight: normal;
|
||||
|
||||
transition: all 400ms ease 0s;
|
||||
color: $o-wysiwyg-color;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
|
||||
&:hover {
|
||||
color: $o-wysiwyg-color-light;
|
||||
background-color: lighten($o-wysiwyg-bg-color, 10%);
|
||||
}
|
||||
|
||||
&.oe_snippet_remove {
|
||||
color: $o-wysiwyg-color;
|
||||
background-color: $o-wysiwyg-color-danger;
|
||||
border-color: darken($o-wysiwyg-color-danger, 20%);
|
||||
|
||||
>.fa {
|
||||
color: $o-wysiwyg-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: darken($o-wysiwyg-color-danger, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&.oe_snippet_parent>i {
|
||||
transform: scaleX(-1);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
>.fa {
|
||||
color: $o-wysiwyg-color;
|
||||
line-height: inherit;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
// CUSTOMIZE MENU BUTTON
|
||||
>.oe_options {
|
||||
.btn {
|
||||
width: auto;
|
||||
padding: 0 25px 0 5px;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: lighten($o-wysiwyg-bg-color, 5%);
|
||||
border-color: lighten($o-wysiwyg-bg-color, 10%);
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 2px;
|
||||
background-color: $o-wysiwyg-color-light;
|
||||
@include o-position-absolute(46%);
|
||||
transition: all 0.3s ease 0s;
|
||||
}
|
||||
|
||||
&:before {
|
||||
right: 9px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: 5px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Open menu
|
||||
&.show .btn {
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:active:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
right: 10px;
|
||||
transform: translateX(5px) rotate(-45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
// CUSTOMIZE MENU
|
||||
.dropdown-menu {
|
||||
margin: -1px 0 0 0;
|
||||
padding: 0;
|
||||
border: 1px solid $o-wysiwyg-bg-color;
|
||||
border-radius: 0;
|
||||
background-color: $o-wysiwyg-bg-color;
|
||||
|
||||
.dropdown-item {
|
||||
position: relative;
|
||||
color: $o-wysiwyg-color-light;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
padding: 5px 40px 5px 10px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&:before {
|
||||
@include o-position-absolute($top: 5px, $right: 5px);
|
||||
content: "\f00c";
|
||||
font-family: "FontAwesome";
|
||||
color: $o-brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
>i {
|
||||
width: 15px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@include o-position-absolute($top: 12px, $right: 10px);
|
||||
@include o-caret-right(4px);
|
||||
border-left-color: $o-wysiwyg-color-light;
|
||||
}
|
||||
|
||||
&:hover>.dropdown-menu {
|
||||
display: block;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
|
||||
&.o_open_to_left {
|
||||
left: auto;
|
||||
right: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border-top: 1px solid #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
color: $o-wysiwyg-color-light;
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
&.o_main_header {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&.o_parent_editor_header {
|
||||
position: relative;
|
||||
color: $o-wysiwyg-color-light;
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
margin: 10px 0 0 10px;
|
||||
text-transform: uppercase;
|
||||
padding-bottom: .25em;
|
||||
|
||||
&::before {
|
||||
content: "\f148";
|
||||
@include o-position-absolute($top: 5px, $left: 4px);
|
||||
font-family: FontAwesome;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
~.dropdown-submenu .dropdown-item,
|
||||
~.dropdown-item {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
~.dropdown-submenu::before {
|
||||
@include o-position-absolute($top: 10px, $right: 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_top_cover>.oe_overlay_options {
|
||||
bottom: auto;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
// HANDLES
|
||||
>.o_handles {
|
||||
@include o-position-absolute(-$o-wysiwyg-handles-offset-to-hide, 0, auto, 0);
|
||||
z-index: $o-wysiwyg-zindex;
|
||||
|
||||
&:hover>.o_handle {
|
||||
background-color: rgba($o-wysiwyg-handles-accent-color, 0.05);
|
||||
}
|
||||
|
||||
>.o_handle {
|
||||
|
||||
position: relative;
|
||||
border: 0 solid $o-wysiwyg-handles-accent-color;
|
||||
transition: background 300ms ease 0s;
|
||||
|
||||
&:hover,
|
||||
&.o_active {
|
||||
background-color: rgba($o-wysiwyg-handles-accent-color, 0.2);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
@include o-position-absolute(0, $left: 50%);
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
margin-left: -1px;
|
||||
background-color: rgba($o-wysiwyg-handles-accent-color, 0.5);
|
||||
}
|
||||
|
||||
&.w:before {
|
||||
@include o-position-absolute(50%, $left: 0);
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-color: darken($o-wysiwyg-handles-accent-color, 10%);
|
||||
background-color: darken($o-wysiwyg-handles-accent-color, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
width: $o-wysiwyg-handles-btn-size;
|
||||
height: $o-wysiwyg-handles-btn-size;
|
||||
border: solid 1px darken($o-wysiwyg-handles-accent-color, 10%);
|
||||
line-height: $o-wysiwyg-handles-btn-size - 2;
|
||||
font-size: 14px;
|
||||
font-family: FontAwesome;
|
||||
background-color: darken($o-wysiwyg-handles-accent-color, 20%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.w:after,
|
||||
&.e:after {
|
||||
content: "\f07e";
|
||||
}
|
||||
|
||||
&.s:after,
|
||||
&.n:after {
|
||||
content: "\f07d";
|
||||
}
|
||||
|
||||
&.o_handle_start {
|
||||
|
||||
&.w:after,
|
||||
&.e:after {
|
||||
content: '\f061';
|
||||
}
|
||||
|
||||
&.n:after,
|
||||
&.s:after {
|
||||
content: '\f063';
|
||||
}
|
||||
}
|
||||
|
||||
&.o_handle_end {
|
||||
|
||||
&.w:after,
|
||||
&.e:after {
|
||||
content: '\f060';
|
||||
}
|
||||
|
||||
&.n:after,
|
||||
&.s:after {
|
||||
content: '\f062';
|
||||
}
|
||||
}
|
||||
|
||||
&.w {
|
||||
@include o-position-absolute($o-wysiwyg-handles-offset-to-hide, auto, -$o-wysiwyg-handles-offset-to-hide, 0);
|
||||
width: $o-wysiwyg-handle-edge-size;
|
||||
border-width: $o-wysiwyg-handle-border-width;
|
||||
border-right-width: 0;
|
||||
cursor: e-resize;
|
||||
|
||||
&:after {
|
||||
@include o-position-absolute($top: 50%, $left: 40%);
|
||||
margin-top: -$o-wysiwyg-handles-btn-size/2;
|
||||
}
|
||||
}
|
||||
|
||||
&.e {
|
||||
@include o-position-absolute($o-wysiwyg-handles-offset-to-hide, 0, -$o-wysiwyg-handles-offset-to-hide, auto);
|
||||
width: $o-wysiwyg-handle-edge-size;
|
||||
border-right-width: $o-wysiwyg-handle-border-width;
|
||||
cursor: w-resize;
|
||||
|
||||
&:after {
|
||||
@include o-position-absolute($top: 50%, $right: 40%);
|
||||
margin-top: -$o-wysiwyg-handles-btn-size/2;
|
||||
}
|
||||
}
|
||||
|
||||
&.n {
|
||||
@include o-position-absolute($o-wysiwyg-handles-offset-to-hide, 0, auto, 0);
|
||||
border-top-width: $o-wysiwyg-handle-border-width;
|
||||
cursor: ns-resize;
|
||||
|
||||
&:after {
|
||||
@include o-position-absolute($left: 50%, $top: 40%);
|
||||
margin-left: -$o-wysiwyg-handles-btn-size/2;
|
||||
}
|
||||
}
|
||||
|
||||
&.s {
|
||||
@include o-position-absolute(auto, 0, -$o-wysiwyg-handles-offset-to-hide, 0);
|
||||
border-bottom-width: $o-wysiwyg-handle-border-width;
|
||||
cursor: ns-resize;
|
||||
|
||||
&:after {
|
||||
@include o-position-absolute($left: 50%, $bottom: 40%);
|
||||
margin-left: -$o-wysiwyg-handles-btn-size/2;
|
||||
}
|
||||
}
|
||||
|
||||
&.readonly {
|
||||
cursor: auto !important;
|
||||
|
||||
&:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact_menu {
|
||||
display: block;
|
||||
top: -24px;
|
||||
margin: 0px;
|
||||
padding: 2px 0px;
|
||||
position: relative;
|
||||
min-height: 44px;
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
|
||||
.s-resize-important * {
|
||||
cursor: s-resize !important;
|
||||
}
|
||||
|
||||
.n-resize-important * {
|
||||
cursor: n-resize !important;
|
||||
}
|
||||
|
||||
.e-resize-important * {
|
||||
cursor: e-resize !important;
|
||||
}
|
||||
|
||||
.w-resize-important * {
|
||||
cursor: w-resize !important;
|
||||
}
|
||||
|
||||
.move-important * {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
.dropdown-menu label .o_switch {
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.text-input-group {
|
||||
position: relative;
|
||||
margin-bottom: 45px;
|
||||
|
||||
input {
|
||||
font-size: 18px;
|
||||
padding: 10px 10px 10px 5px;
|
||||
display: block;
|
||||
width: 300px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #757575;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* LABEL ======================================= */
|
||||
label {
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
@include o-position-absolute($top: 10px, $left: 5px);
|
||||
pointer-events: none;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
/* active state */
|
||||
input:focus~label,
|
||||
input:valid~label {
|
||||
top: -20px;
|
||||
font-size: 14px;
|
||||
color: #5264AE;
|
||||
}
|
||||
|
||||
/* BOTTOM BARS ================================= */
|
||||
.bar {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.bar:before,
|
||||
.bar:after {
|
||||
content: '';
|
||||
height: 2px;
|
||||
width: 0;
|
||||
bottom: 1px;
|
||||
@include o-position-absolute;
|
||||
background: #5264AE;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.bar:before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.bar:after {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
/* active state */
|
||||
input:focus~.bar:before,
|
||||
input:focus~.bar:after {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* HIGHLIGHTER ================================== */
|
||||
.highlight {
|
||||
@include o-position-absolute($top: 25%, $left: 0);
|
||||
height: 60%;
|
||||
width: 100px;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* active state */
|
||||
input:focus~.highlight {
|
||||
animation: inputHighlighter 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// DRAG&DROP ANIMATIONS
|
||||
.oe_snippet_body {
|
||||
opacity: 0;
|
||||
animation: fadeInDownSmall 700ms forwards;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Mixins for buttons with alpha bg-color
|
||||
@mixin o-alpha-button-preview {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
// Place an overlay that inherit the button's bg-color.
|
||||
&::before {
|
||||
content: "";
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
z-index: -1;
|
||||
background-image: url('/web_editor/static/src/img/transparent.png');
|
||||
background-size: 10px auto;
|
||||
}
|
||||
}
|
||||
|
||||
$o-wysiwyg-zindex: 1000;
|
||||
|
||||
$o-wysiwyg-md-ease: cubic-bezier(0.19, 1, 0.22, 1) !default;
|
||||
|
||||
$o-wysiwyg-font-family: Roboto,
|
||||
"Montserrat",
|
||||
"Segoe UI",
|
||||
"Helvetica Neue",
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
|
||||
$o-wysiwyg-toolbar-height: 32px;
|
||||
|
||||
$o-wysiwyg-color: #212629 !default;
|
||||
$o-wysiwyg-color-light: #616669 !default;
|
||||
$o-wysiwyg-color-danger: #912629 !default;
|
||||
$o-wysiwyg-color-success: #219629 !default;
|
||||
$o-wysiwyg-bg-color: #f7f7f7 !default;
|
||||
$o-wysiwyg-bg-color-dark: #a7a7a7 !default;
|
||||
$o-wysiwyg-border-color: #a9a9a9 !default;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user