[IMP] web_editor,*: Refactoring the wysiwyg editor and 'html' field
_Task #1878502_
- The refactoring allows for the update of the _Summernote_ library.
- A new structure was created 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.
- The cleanup allowed us to remove some of the code that was in `web_editor` but only used in `website` or in `mass_mailing`. Some parts are still in `web_editor` but will be moved at a later time.
- The refactoring also had to be done in order to create a set of unit tests. Each behavior can be tested, including the behaviors performed as a consequence of keyboard interactions (for instance: Enter, Tab...).
- 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. A lot of tests are done, ensuring the consistency of outside behavior.
- 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)` which 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)` which 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.
**REMAINING KNOW ISSUES**
These will be fixed in a subsequent commit.
- A few committed changes made during the development of this refactoring were lost in the process and should be reapplied.
- Some minor (mostly range) issues remain with Firefox.
--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
closes odoo/odoo#29775
This commit is contained in:
@@ -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,434 +0,0 @@
|
||||
odoo.define('mass_mailing.editor', 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({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
|
||||
this.containerWidth = this.$target.parent().closest("td, table, div").width();
|
||||
|
||||
var self = this;
|
||||
var offset, sib_offset, target_width, sib_width;
|
||||
|
||||
this.$overlay.find(".o_handle.e, .o_handle.w").removeClass("readonly");
|
||||
this.isIMG = this.$target.is("img");
|
||||
if (this.isIMG) {
|
||||
this.$overlay.find(".o_handle.w").addClass("readonly");
|
||||
this.$overlay.find(".oe_snippet_move, .oe_snippet_clone").addClass('d-none');
|
||||
}
|
||||
|
||||
var $body = $(document.body);
|
||||
this.$overlay.find(".o_handle").on('mousedown', function (event) {
|
||||
event.preventDefault();
|
||||
var $handle = $(this);
|
||||
var compass = false;
|
||||
|
||||
_.each(['n', 's', 'e', 'w'], function (handler) {
|
||||
if ($handle.hasClass(handler)) { compass = handler; }
|
||||
});
|
||||
if (self.isIMG) { compass = "image"; }
|
||||
|
||||
$body.on("mousemove.mass_mailing_width_x", function (event) {
|
||||
event.preventDefault();
|
||||
offset = self.$target.offset().left;
|
||||
target_width = self.get_max_width(self.$target);
|
||||
if (compass === 'e' && self.$target.next().offset()) {
|
||||
sib_width = self.get_max_width(self.$target.next());
|
||||
sib_offset = self.$target.next().offset().left;
|
||||
self.change_width(event, self.$target, target_width, offset, true);
|
||||
self.change_width(event, self.$target.next(), sib_width, sib_offset, false);
|
||||
}
|
||||
if (compass === 'w' && self.$target.prev().offset()) {
|
||||
sib_width = self.get_max_width(self.$target.prev());
|
||||
sib_offset = self.$target.prev().offset().left;
|
||||
self.change_width(event, self.$target, target_width, offset, false);
|
||||
self.change_width(event, self.$target.prev(), sib_width, sib_offset, true);
|
||||
}
|
||||
if (compass === 'image') {
|
||||
self.change_width(event, self.$target, target_width, offset, true);
|
||||
}
|
||||
});
|
||||
$body.one("mouseup", function () {
|
||||
$body.off('.mass_mailing_width_x');
|
||||
});
|
||||
});
|
||||
|
||||
return def;
|
||||
},
|
||||
change_width: function (event, target, target_width, offset, grow) {
|
||||
target.css("width", grow ? (event.pageX - offset) : (offset + target_width - event.pageX));
|
||||
this.trigger_up('cover_update');
|
||||
},
|
||||
get_int_width: function (el) {
|
||||
return parseInt($(el).css("width"), 10);
|
||||
},
|
||||
get_max_width: function ($el) {
|
||||
return this.containerWidth - _.reduce(_.map($el.siblings(), this.get_int_width), function (memo, w) { return memo + w; });
|
||||
},
|
||||
onFocus: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (this.$target.is("td, th")) {
|
||||
this.$overlay.find(".o_handle.e, .o_handle.w").toggleClass("readonly", this.$target.siblings().length === 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.table_item = options.Class.extend({
|
||||
onClone: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// If we cloned a td or th element...
|
||||
if (options.isCurrent && this.$target.is("td, th")) {
|
||||
// ... and that the td or th element was alone on its row ...
|
||||
if (this.$target.siblings().length === 1) {
|
||||
var $tr = this.$target.parent();
|
||||
$tr.clone().empty().insertAfter($tr).append(this.$target); // ... move the clone in a new row instead
|
||||
return;
|
||||
}
|
||||
|
||||
// ... if not, if the clone neighbor is an empty cell, remove this empty cell (like if the clone content had been put in that cell)
|
||||
var $next = this.$target.next();
|
||||
if ($next.length && $next.text().trim() === "") {
|
||||
$next.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// ... if not, insert an empty col in each other row, at the index of the clone
|
||||
var width = this.$target.width();
|
||||
var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
|
||||
_.each($trs.children(":nth-child(" + this.$target.index() + ")"), function (col) {
|
||||
$(col).after($("<td/>", {style: "width: " + width + "px;"}));
|
||||
});
|
||||
}
|
||||
},
|
||||
onRemove: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// If we are removing a td or th element which was not alone on its row ...
|
||||
if (this.$target.is("td, th") && this.$target.siblings().length > 0) {
|
||||
var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
|
||||
if ($trs.length) { // ... if there are other rows in the table ...
|
||||
var $last_tds = $trs.children(":last-child");
|
||||
if (_.reduce($last_tds, function (memo, td) { return memo + (td.innerHTML || ""); }, "").trim() === "") {
|
||||
$last_tds.remove(); // ... remove the potential full empty column in the table
|
||||
} else {
|
||||
this.$target.parent().append("<td/>"); // ... else, if there is no full empty column, append an empty col in the current row
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
start: function () {
|
||||
this._super();
|
||||
var $table_target = this.$target.find('table:first');
|
||||
if ($table_target) {
|
||||
this.$target = $table_target;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
odoo.define('mass_mailing.snippets.options', function (require) {
|
||||
"use strict";
|
||||
|
||||
var options = require('web_editor.snippets.options');
|
||||
|
||||
// Snippet option for resizing image and column width inline like excel
|
||||
options.registry.mass_mailing_sizing_x = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
|
||||
this.containerWidth = this.$target.parent().closest("td, table, div").width();
|
||||
|
||||
var self = this;
|
||||
var offset, sib_offset, target_width, sib_width;
|
||||
|
||||
this.$overlay.find(".o_handle.e, .o_handle.w").removeClass("readonly");
|
||||
this.isIMG = this.$target.is("img");
|
||||
if (this.isIMG) {
|
||||
this.$overlay.find(".o_handle.w").addClass("readonly");
|
||||
this.$overlay.find(".oe_snippet_move, .oe_snippet_clone").addClass('d-none');
|
||||
}
|
||||
|
||||
var $body = $(this.ownerDocument.body);
|
||||
this.$overlay.find(".o_handle").on('mousedown', function (event) {
|
||||
event.preventDefault();
|
||||
var $handle = $(this);
|
||||
var compass = false;
|
||||
|
||||
_.each(['n', 's', 'e', 'w'], function (handler) {
|
||||
if ($handle.hasClass(handler)) { compass = handler; }
|
||||
});
|
||||
if (self.isIMG) { compass = "image"; }
|
||||
|
||||
$body.on("mousemove.mass_mailing_width_x", function (event) {
|
||||
event.preventDefault();
|
||||
offset = self.$target.offset().left;
|
||||
target_width = self.get_max_width(self.$target);
|
||||
if (compass === 'e' && self.$target.next().offset()) {
|
||||
sib_width = self.get_max_width(self.$target.next());
|
||||
sib_offset = self.$target.next().offset().left;
|
||||
self.change_width(event, self.$target, target_width, offset, true);
|
||||
self.change_width(event, self.$target.next(), sib_width, sib_offset, false);
|
||||
}
|
||||
if (compass === 'w' && self.$target.prev().offset()) {
|
||||
sib_width = self.get_max_width(self.$target.prev());
|
||||
sib_offset = self.$target.prev().offset().left;
|
||||
self.change_width(event, self.$target, target_width, offset, false);
|
||||
self.change_width(event, self.$target.prev(), sib_width, sib_offset, true);
|
||||
}
|
||||
if (compass === 'image') {
|
||||
self.change_width(event, self.$target, target_width, offset, true);
|
||||
}
|
||||
});
|
||||
$body.one("mouseup", function () {
|
||||
$body.off('.mass_mailing_width_x');
|
||||
});
|
||||
});
|
||||
|
||||
return def;
|
||||
},
|
||||
change_width: function (event, target, target_width, offset, grow) {
|
||||
target.css("width", grow ? (event.pageX - offset) : (offset + target_width - event.pageX));
|
||||
this.trigger_up('cover_update');
|
||||
},
|
||||
get_int_width: function (el) {
|
||||
return parseInt($(el).css("width"), 10);
|
||||
},
|
||||
get_max_width: function ($el) {
|
||||
return this.containerWidth - _.reduce(_.map($el.siblings(), this.get_int_width), function (memo, w) { return memo + w; });
|
||||
},
|
||||
onFocus: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (this.$target.is("td, th")) {
|
||||
this.$overlay.find(".o_handle.e, .o_handle.w").toggleClass("readonly", this.$target.siblings().length === 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.mass_mailing_table_item = options.Class.extend({
|
||||
onClone: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// If we cloned a td or th element...
|
||||
if (options.isCurrent && this.$target.is("td, th")) {
|
||||
// ... and that the td or th element was alone on its row ...
|
||||
if (this.$target.siblings().length === 1) {
|
||||
var $tr = this.$target.parent();
|
||||
$tr.clone().empty().insertAfter($tr).append(this.$target); // ... move the clone in a new row instead
|
||||
return;
|
||||
}
|
||||
|
||||
// ... if not, if the clone neighbor is an empty cell, remove this empty cell (like if the clone content had been put in that cell)
|
||||
var $next = this.$target.next();
|
||||
if ($next.length && $next.text().trim() === "") {
|
||||
$next.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// ... if not, insert an empty col in each other row, at the index of the clone
|
||||
var width = this.$target.width();
|
||||
var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
|
||||
_.each($trs.children(":nth-child(" + this.$target.index() + ")"), function (col) {
|
||||
$(col).after($("<td/>", {style: "width: " + width + "px;"}));
|
||||
});
|
||||
}
|
||||
},
|
||||
onRemove: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// If we are removing a td or th element which was not alone on its row ...
|
||||
if (this.$target.is("td, th") && this.$target.siblings().length > 0) {
|
||||
var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
|
||||
if ($trs.length) { // ... if there are other rows in the table ...
|
||||
var $last_tds = $trs.children(":last-child");
|
||||
if (_.reduce($last_tds, function (memo, td) { return memo + (td.innerHTML || ""); }, "").trim() === "") {
|
||||
$last_tds.remove(); // ... remove the potential full empty column in the table
|
||||
} else {
|
||||
this.$target.parent().append("<td/>"); // ... else, if there is no full empty column, append an empty col in the current row
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Adding compatibility for the outlook compliance of mailings.
|
||||
// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
|
||||
options.registry.background = options.registry.background.extend({
|
||||
start: function () {
|
||||
this._super();
|
||||
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: self.dataPointID,
|
||||
changes: _.object([fieldName], [self._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;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
odoo.define('mass_mailing.field_html_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var FormView = require('web.FormView');
|
||||
var testUtils = require('web.test_utils');
|
||||
var weTestUtils = require('web_editor.test_utils');
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
|
||||
|
||||
QUnit.module('mass_mailing', {}, function () {
|
||||
QUnit.module('field html', {
|
||||
beforeEach: function () {
|
||||
this.data = weTestUtils.wysiwygData({
|
||||
'mail.mass_mailing': {
|
||||
fields: {
|
||||
display_name: {
|
||||
string: "Displayed name",
|
||||
type: "char"
|
||||
},
|
||||
body_html: {
|
||||
string: "Message Body inline (to send)",
|
||||
type: "html"
|
||||
},
|
||||
body_arch: {
|
||||
string: "Message Body for edition",
|
||||
type: "html"
|
||||
},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
body_html: "<div class='field_body' style='background-color: red;'><p>code to edit</p></div>",
|
||||
body_arch: "<div class='field_body'><p>code to edit</p></div>",
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.mock.patch(ajax, {
|
||||
loadAsset: function (xmlId) {
|
||||
if (xmlId === 'template.assets') {
|
||||
return $.when({
|
||||
cssLibs: [],
|
||||
cssContents: ['.field_body {background-color: red;}']
|
||||
});
|
||||
}
|
||||
if (xmlId === 'template.assets_all_style') {
|
||||
return $.when({
|
||||
cssLibs: $('head link[href]:not([type="image/x-icon"])').map(function () {
|
||||
return $(this).attr('href');
|
||||
}).get(),
|
||||
cssContents: ['.field_body {background-color: red;}']
|
||||
});
|
||||
}
|
||||
throw 'Wrong template';
|
||||
},
|
||||
});
|
||||
},
|
||||
afterEach: function () {
|
||||
testUtils.mock.unpatch(ajax);
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test('save arch and html', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(6);
|
||||
|
||||
testUtils.createAsyncView({
|
||||
View: FormView,
|
||||
model: 'mail.mass_mailing',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
' <field name="body_html" class="oe_read_only" widget="html"'+
|
||||
' options="{'+
|
||||
' \'cssReadonly\': \'template.assets\','+
|
||||
' }"'+
|
||||
' />'+
|
||||
' <field name="body_arch" class="oe_edit_only" widget="mass_mailing_html"'+
|
||||
' options="{'+
|
||||
' \'snippets\': \'template.assets\','+
|
||||
' \'cssEdit\': \'template.assets\','+
|
||||
' \'inline-field\': \'body_html\''+
|
||||
' }"'+
|
||||
' />'+
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "write") {
|
||||
var values = args.args[1];
|
||||
assert.strictEqual(values.body_html,
|
||||
'<div class="field_body" style="background-color:red;"><p>c<b>od</b>e to edit</p></div>',
|
||||
"should save the content");
|
||||
assert.strictEqual(values.body_arch,
|
||||
'<div class="field_body"><p>c<b>od</b>e to edit</p></div>',
|
||||
"should save the content");
|
||||
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
}).then(function (form) {
|
||||
var $fieldReadonly = form.$('.oe_form_field[name="body_html"]');
|
||||
var $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
|
||||
|
||||
assert.strictEqual($fieldReadonly.css('display'), 'block', "should display the readonly mode");
|
||||
assert.strictEqual($fieldEdit.css('display'), 'none', "should hide the edit mode");
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
$fieldReadonly = form.$('.oe_form_field[name="body_html"]');
|
||||
$fieldEdit = form.$('.oe_form_field[name="body_arch"]');
|
||||
|
||||
assert.strictEqual($fieldReadonly.css('display'), 'none', "should hide the readonly mode");
|
||||
assert.strictEqual($fieldEdit.css('display'), 'block', "should display the edit mode");
|
||||
|
||||
var $iframe = $fieldEdit.find('iframe');
|
||||
|
||||
$iframe.data('load-def').then(function () {
|
||||
var doc = $iframe.contents()[0];
|
||||
var $content = $('#iframe_target', doc);
|
||||
|
||||
// select the text
|
||||
var pText = $content.find('.note-editable p').first().contents()[0];
|
||||
Wysiwyg.setRange(pText, 1, pText, 3);
|
||||
// text is selected
|
||||
|
||||
$content.find('.note-toolbar .note-font .note-btn-bold').mousedown().click();
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,42 +6,27 @@
|
||||
<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>
|
||||
|
||||
<template id="qunit_suite" inherit_id="web.qunit_suite">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/mass_mailing/static/tests/mass_mailing_html_tests.js"/>
|
||||
</xpath>
|
||||
</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;
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="web.colorpicker">
|
||||
<div class="o_colorpicker_widget">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<div class="o_color_pick_area d-inline-block border">
|
||||
<div class="o_picker_pointer rounded-circle"/>
|
||||
</div>
|
||||
<div class="o_color_slider d-inline-block ml-2 border">
|
||||
<div class="o_slider_pointer"/>
|
||||
</div>
|
||||
<div class="o_opacity_slider d-inline-block ml-2 border">
|
||||
<div class="o_opacity_pointer"/>
|
||||
</div>
|
||||
</div>
|
||||
<form class="col o_color_picker_inputs">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend o_color_preview_parent">
|
||||
<span class="input-group-text rounded-0 o_color_preview"/>
|
||||
</div>
|
||||
<input type="text" class="form-control o_hex_input" data-color-method="hex"/>
|
||||
</div>
|
||||
<div class="form-row mt-2">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="red">Red</label>
|
||||
<input type="text" class="form-control o_red_input" data-color-method="rgb" id="red"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="green">Green</label>
|
||||
<input type="text" class="form-control o_green_input" data-color-method="rgb" id="green"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="blue">Blue</label>
|
||||
<input type="text" class="form-control o_blue_input" data-color-method="rgb" id="blue"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="hue">Hue</label>
|
||||
<input type="text" class="form-control o_hue_input" data-color-method="hsl" id="hue"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="saturation">Saturation %</label>
|
||||
<input type="text" class="form-control o_saturation_input" data-color-method="hsl" id="saturation"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lightness">Lightness %</label>
|
||||
<input type="text" class="form-control o_lightness_input" data-color-method="hsl" id="lightness"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="opacity">Opacity %</label>
|
||||
<input type="text" class="form-control o_opacity_input" data-color-method="opacity" id="opacity"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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>
|
||||
|
||||
@@ -69,6 +69,7 @@ function removeSrcAttribute(el, widget) {
|
||||
if (widget) {
|
||||
widget._rpc({ route: src });
|
||||
}
|
||||
$(node).trigger('load');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +229,7 @@ function addMockEnvironment(widget, params) {
|
||||
}
|
||||
|
||||
$('body').off('DOMNodeInserted.removeSRC');
|
||||
$('body').removeClass('debug');
|
||||
$('.blockUI').remove();
|
||||
|
||||
widgetDestroy.call(this);
|
||||
|
||||
@@ -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")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
@@ -0,0 +1,16 @@
|
||||
.ext-databasic {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: 50px;
|
||||
background-color: cyan;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border: 1px solid white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ext-databasic p {
|
||||
color: white;
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
(function(factory) {
|
||||
/* global define */
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// Node/CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(window.jQuery);
|
||||
}
|
||||
}(function($) {
|
||||
// pull in some summernote core functions
|
||||
var ui = $.summernote.ui;
|
||||
var dom = $.summernote.dom;
|
||||
|
||||
// define the popover plugin
|
||||
var DataBasicPlugin = function(context) {
|
||||
var self = this;
|
||||
var options = context.options;
|
||||
var lang = options.langInfo;
|
||||
|
||||
self.icon = '<i class="fa fa-object-group"/>';
|
||||
|
||||
// add context menu button for dialog
|
||||
context.memo('button.databasic', function() {
|
||||
return ui.button({
|
||||
contents: self.icon,
|
||||
tooltip: lang.databasic.insert,
|
||||
click: context.createInvokeHandler('databasic.showDialog')
|
||||
}).render();
|
||||
});
|
||||
|
||||
// add popover edit button
|
||||
context.memo('button.databasicDialog', function() {
|
||||
return ui.button({
|
||||
contents: self.icon,
|
||||
tooltip: lang.databasic.edit,
|
||||
click: context.createInvokeHandler('databasic.showDialog')
|
||||
}).render();
|
||||
});
|
||||
|
||||
// add popover size buttons
|
||||
context.memo('button.databasicSize100', function() {
|
||||
return ui.button({
|
||||
contents: '<span class="note-fontsize-10">100%</span>',
|
||||
tooltip: lang.image.resizeFull,
|
||||
click: context.createInvokeHandler('editor.resize', '1')
|
||||
}).render();
|
||||
});
|
||||
context.memo('button.databasicSize50', function() {
|
||||
return ui.button({
|
||||
contents: '<span class="note-fontsize-10">50%</span>',
|
||||
tooltip: lang.image.resizeHalf,
|
||||
click: context.createInvokeHandler('editor.resize', '0.5')
|
||||
}).render();
|
||||
});
|
||||
context.memo('button.databasicSize25', function() {
|
||||
return ui.button({
|
||||
contents: '<span class="note-fontsize-10">25%</span>',
|
||||
tooltip: lang.image.resizeQuarter,
|
||||
click: context.createInvokeHandler('editor.resize', '0.25')
|
||||
}).render();
|
||||
});
|
||||
|
||||
self.events = {
|
||||
'summernote.init': function(we, e) {
|
||||
// update existing containers
|
||||
$('data.ext-databasic', e.editable).each(function() { self.setContent($(this)); });
|
||||
// TODO: make this an undo snapshot...
|
||||
},
|
||||
'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function() {
|
||||
self.update();
|
||||
},
|
||||
'summernote.dialog.shown': function() {
|
||||
self.hidePopover();
|
||||
}
|
||||
};
|
||||
|
||||
self.initialize = function() {
|
||||
// create dialog markup
|
||||
var $container = options.dialogsInBody ? $(document.body) : context.layoutInfo.editor;
|
||||
|
||||
var body = '<div class="form-group row-fluid">' +
|
||||
'<label>' + lang.databasic.testLabel + '</label>' +
|
||||
'<input class="ext-databasic-test form-control" type="text" />' +
|
||||
'</div>';
|
||||
var footer = '<button href="#" class="btn btn-primary ext-databasic-save">' + lang.databasic.insert + '</button>';
|
||||
|
||||
self.$dialog = ui.dialog({
|
||||
title: lang.databasic.name,
|
||||
fade: options.dialogsFade,
|
||||
body: body,
|
||||
footer: footer
|
||||
}).render().appendTo($container);
|
||||
|
||||
// create popover
|
||||
self.$popover = ui.popover({
|
||||
className: 'ext-databasic-popover'
|
||||
}).render().appendTo('body');
|
||||
var $content = self.$popover.find('.popover-content');
|
||||
|
||||
context.invoke('buttons.build', $content, options.popover.databasic);
|
||||
};
|
||||
|
||||
self.destroy = function() {
|
||||
self.$popover.remove();
|
||||
self.$popover = null;
|
||||
self.$dialog.remove();
|
||||
self.$dialog = null;
|
||||
};
|
||||
|
||||
self.update = function() {
|
||||
// Prevent focusing on editable when invoke('code') is executed
|
||||
if (!context.invoke('editor.hasFocus')) {
|
||||
self.hidePopover();
|
||||
return;
|
||||
}
|
||||
|
||||
var rng = context.invoke('editor.createRange');
|
||||
var visible = false;
|
||||
|
||||
if (rng.isOnData()) {
|
||||
var $data = $(rng.sc).closest('data.ext-databasic');
|
||||
|
||||
if ($data.length) {
|
||||
var pos = dom.posFromPlaceholder($data[0]);
|
||||
|
||||
self.$popover.css({
|
||||
display: 'block',
|
||||
left: pos.left,
|
||||
top: pos.top
|
||||
});
|
||||
|
||||
// save editor target to let size buttons resize the container
|
||||
context.invoke('editor.saveTarget', $data[0]);
|
||||
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// hide if not visible
|
||||
if (!visible) {
|
||||
self.hidePopover();
|
||||
}
|
||||
};
|
||||
|
||||
self.hidePopover = function() {
|
||||
self.$popover.hide();
|
||||
};
|
||||
|
||||
// define plugin dialog
|
||||
self.getInfo = function() {
|
||||
var rng = context.invoke('editor.createRange');
|
||||
|
||||
if (rng.isOnData()) {
|
||||
var $data = $(rng.sc).closest('data.ext-databasic');
|
||||
|
||||
if ($data.length) {
|
||||
// Get the first node on range(for edit).
|
||||
return {
|
||||
node: $data,
|
||||
test: $data.attr('data-test')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
self.setContent = function($node) {
|
||||
$node.html('<p contenteditable="false">' + self.icon + ' ' + lang.databasic.name + ': ' +
|
||||
$node.attr('data-test') + '</p>');
|
||||
};
|
||||
|
||||
self.updateNode = function(info) {
|
||||
self.setContent(info.node
|
||||
.attr('data-test', info.test));
|
||||
};
|
||||
|
||||
self.createNode = function(info) {
|
||||
var $node = $('<data class="ext-databasic"></data>');
|
||||
|
||||
if ($node) {
|
||||
// save node to info structure
|
||||
info.node = $node;
|
||||
// insert node into editor dom
|
||||
context.invoke('editor.insertNode', $node[0]);
|
||||
}
|
||||
|
||||
return $node;
|
||||
};
|
||||
|
||||
self.showDialog = function() {
|
||||
var info = self.getInfo();
|
||||
var newNode = !info.node;
|
||||
context.invoke('editor.saveRange');
|
||||
|
||||
self
|
||||
.openDialog(info)
|
||||
.then(function(dialogInfo) {
|
||||
// [workaround] hide dialog before restore range for IE range focus
|
||||
ui.hideDialog(self.$dialog);
|
||||
context.invoke('editor.restoreRange');
|
||||
|
||||
// insert a new node
|
||||
if (newNode) {
|
||||
self.createNode(info);
|
||||
}
|
||||
|
||||
// update info with dialog info
|
||||
$.extend(info, dialogInfo);
|
||||
|
||||
self.updateNode(info);
|
||||
})
|
||||
.fail(function() {
|
||||
context.invoke('editor.restoreRange');
|
||||
});
|
||||
};
|
||||
|
||||
self.openDialog = function(info) {
|
||||
return $.Deferred(function(deferred) {
|
||||
var $inpTest = self.$dialog.find('.ext-databasic-test');
|
||||
var $saveBtn = self.$dialog.find('.ext-databasic-save');
|
||||
var onKeyup = function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
$saveBtn.trigger('click');
|
||||
}
|
||||
};
|
||||
|
||||
ui.onDialogShown(self.$dialog, function() {
|
||||
context.triggerEvent('dialog.shown');
|
||||
|
||||
$inpTest.val(info.test).on('input', function() {
|
||||
ui.toggleBtn($saveBtn, $inpTest.val());
|
||||
}).trigger('focus').on('keyup', onKeyup);
|
||||
|
||||
$saveBtn
|
||||
.text(info.node ? lang.databasic.edit : lang.databasic.insert)
|
||||
.click(function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
deferred.resolve({ test: $inpTest.val() });
|
||||
});
|
||||
|
||||
// init save button
|
||||
ui.toggleBtn($saveBtn, $inpTest.val());
|
||||
});
|
||||
|
||||
ui.onDialogHidden(self.$dialog, function() {
|
||||
$inpTest.off('input keyup');
|
||||
$saveBtn.off('click');
|
||||
|
||||
if (deferred.state() === 'pending') {
|
||||
deferred.reject();
|
||||
}
|
||||
});
|
||||
|
||||
ui.showDialog(self.$dialog);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// Extends summernote
|
||||
$.extend(true, $.summernote, {
|
||||
plugins: {
|
||||
databasic: DataBasicPlugin
|
||||
},
|
||||
|
||||
options: {
|
||||
popover: {
|
||||
databasic: [
|
||||
['databasic', ['databasicDialog', 'databasicSize100', 'databasicSize50', 'databasicSize25']]
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// add localization texts
|
||||
lang: {
|
||||
'en-US': {
|
||||
databasic: {
|
||||
name: 'Basic Data Container',
|
||||
insert: 'insert basic data container',
|
||||
edit: 'edit basic data container',
|
||||
testLabel: 'test input'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}));
|
||||
@@ -0,0 +1,81 @@
|
||||
(function(factory) {
|
||||
/* global define */
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// Node/CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(window.jQuery);
|
||||
}
|
||||
}(function($) {
|
||||
// Extends plugins for adding hello.
|
||||
// - plugin is external module for customizing.
|
||||
$.extend($.summernote.plugins, {
|
||||
/**
|
||||
* @param {Object} context - context object has status of editor.
|
||||
*/
|
||||
'hello': function(context) {
|
||||
var self = this;
|
||||
|
||||
// ui has renders to build ui elements.
|
||||
// - you can create a button with `ui.button`
|
||||
var ui = $.summernote.ui;
|
||||
|
||||
// add hello button
|
||||
context.memo('button.hello', function() {
|
||||
// create button
|
||||
var button = ui.button({
|
||||
contents: '<i class="fa fa-child"/> Hello',
|
||||
tooltip: 'hello',
|
||||
click: function() {
|
||||
self.$panel.show();
|
||||
self.$panel.hide(500);
|
||||
// invoke insertText method with 'hello' on editor module.
|
||||
context.invoke('editor.insertText', 'hello');
|
||||
}
|
||||
});
|
||||
|
||||
// create jQuery object from button instance.
|
||||
var $hello = button.render();
|
||||
return $hello;
|
||||
});
|
||||
|
||||
// This events will be attached when editor is initialized.
|
||||
this.events = {
|
||||
// This will be called after modules are initialized.
|
||||
'summernote.init': function(we, e) {
|
||||
console.log('summernote initialized', we, e);
|
||||
},
|
||||
// This will be called when user releases a key on editable.
|
||||
'summernote.keyup': function(we, e) {
|
||||
console.log('summernote keyup', we, e);
|
||||
}
|
||||
};
|
||||
|
||||
// This method will be called when editor is initialized by $('..').summernote();
|
||||
// You can create elements for plugin
|
||||
this.initialize = function() {
|
||||
this.$panel = $('<div class="hello-panel"/>').css({
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 100,
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
background: 'red'
|
||||
}).hide();
|
||||
|
||||
this.$panel.appendTo('body');
|
||||
};
|
||||
|
||||
// This methods will be called when editor is destroyed by $('..').summernote('destroy');
|
||||
// You should remove elements on `initialize`.
|
||||
this.destroy = function() {
|
||||
this.$panel.remove();
|
||||
this.$panel = null;
|
||||
};
|
||||
}
|
||||
});
|
||||
}));
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
(function(factory) {
|
||||
/* global define */
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// Node/CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(window.jQuery);
|
||||
}
|
||||
}(function($) {
|
||||
$.extend($.summernote.plugins, {
|
||||
'specialchars': function(context) {
|
||||
var self = this;
|
||||
var ui = $.summernote.ui;
|
||||
|
||||
var $editor = context.layoutInfo.editor;
|
||||
var options = context.options;
|
||||
var lang = options.langInfo;
|
||||
|
||||
var KEY = {
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
ENTER: 13
|
||||
};
|
||||
var COLUMN_LENGTH = 15;
|
||||
var COLUMN_WIDTH = 35;
|
||||
|
||||
var currentColumn = 0;
|
||||
var currentRow = 0;
|
||||
var totalColumn = 0;
|
||||
var totalRow = 0;
|
||||
|
||||
// special characters data set
|
||||
var specialCharDataSet = [
|
||||
'"', '&', '<', '>', '¡', '¢',
|
||||
'£', '¤', '¥', '¦', '§',
|
||||
'¨', '©', 'ª', '«', '¬',
|
||||
'®', '¯', '°', '±', '²',
|
||||
'³', '´', 'µ', '¶', '·',
|
||||
'¸', '¹', 'º', '»', '¼',
|
||||
'½', '¾', '¿', '×', '÷',
|
||||
'ƒ', 'ˆ', '˜', '–', '—',
|
||||
'‘', '’', '‚', '“', '”',
|
||||
'„', '†', '‡', '•', '…',
|
||||
'‰', '′', '″', '‹', '›',
|
||||
'‾', '⁄', '€', 'ℑ', '℘',
|
||||
'ℜ', '™', 'ℵ', '←', '↑',
|
||||
'→', '↓', '↔', '↵', '⇐',
|
||||
'⇑', '⇒', '⇓', '⇔', '∀',
|
||||
'∂', '∃', '∅', '∇', '∈',
|
||||
'∉', '∋', '∏', '∑', '−',
|
||||
'∗', '√', '∝', '∞', '∠',
|
||||
'∧', '∨', '∩', '∪', '∫',
|
||||
'∴', '∼', '≅', '≈', '≠',
|
||||
'≡', '≤', '≥', '⊂', '⊃',
|
||||
'⊄', '⊆', '⊇', '⊕', '⊗',
|
||||
'⊥', '⋅', '⌈', '⌉', '⌊',
|
||||
'⌋', '◊', '♠', '♣', '♥',
|
||||
'♦'
|
||||
];
|
||||
|
||||
context.memo('button.specialchars', function() {
|
||||
return ui.button({
|
||||
contents: '<i class="fa fa-font fa-flip-vertical">',
|
||||
tooltip: lang.specialChar.specialChar,
|
||||
click: function() {
|
||||
self.show();
|
||||
}
|
||||
}).render();
|
||||
});
|
||||
|
||||
/**
|
||||
* Make Special Characters Table
|
||||
*
|
||||
* @member plugin.specialChar
|
||||
* @private
|
||||
* @return {jQuery}
|
||||
*/
|
||||
this.makeSpecialCharSetTable = function() {
|
||||
var $table = $('<table/>');
|
||||
$.each(specialCharDataSet, function(idx, text) {
|
||||
var $td = $('<td/>').addClass('note-specialchar-node');
|
||||
var $tr = (idx % COLUMN_LENGTH === 0) ? $('<tr/>') : $table.find('tr').last();
|
||||
|
||||
var $button = ui.button({
|
||||
callback: function($node) {
|
||||
$node.html(text);
|
||||
$node.attr('title', text);
|
||||
$node.attr('data-value', encodeURIComponent(text));
|
||||
$node.css({
|
||||
width: COLUMN_WIDTH,
|
||||
'margin-right': '2px',
|
||||
'margin-bottom': '2px'
|
||||
});
|
||||
}
|
||||
}).render();
|
||||
|
||||
$td.append($button);
|
||||
|
||||
$tr.append($td);
|
||||
if (idx % COLUMN_LENGTH === 0) {
|
||||
$table.append($tr);
|
||||
}
|
||||
});
|
||||
|
||||
totalRow = $table.find('tr').length;
|
||||
totalColumn = COLUMN_LENGTH;
|
||||
|
||||
return $table;
|
||||
};
|
||||
|
||||
this.initialize = function() {
|
||||
var $container = options.dialogsInBody ? $(document.body) : $editor;
|
||||
|
||||
var body = '<div class="form-group row-fluid">' + this.makeSpecialCharSetTable()[0].outerHTML + '</div>';
|
||||
|
||||
this.$dialog = ui.dialog({
|
||||
title: lang.specialChar.select,
|
||||
body: body
|
||||
}).render().appendTo($container);
|
||||
};
|
||||
|
||||
this.show = function() {
|
||||
var text = context.invoke('editor.getSelectedText');
|
||||
context.invoke('editor.saveRange');
|
||||
this.showSpecialCharDialog(text).then(function(selectChar) {
|
||||
context.invoke('editor.restoreRange');
|
||||
|
||||
// build node
|
||||
var $node = $('<span></span>').html(selectChar)[0];
|
||||
|
||||
if ($node) {
|
||||
// insert video node
|
||||
context.invoke('editor.insertNode', $node);
|
||||
}
|
||||
}).fail(function() {
|
||||
context.invoke('editor.restoreRange');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* show image dialog
|
||||
*
|
||||
* @param {jQuery} $dialog
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.showSpecialCharDialog = function(text) {
|
||||
return $.Deferred(function(deferred) {
|
||||
var $specialCharDialog = self.$dialog;
|
||||
var $specialCharNode = $specialCharDialog.find('.note-specialchar-node');
|
||||
var $selectedNode = null;
|
||||
var ARROW_KEYS = [KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT];
|
||||
var ENTER_KEY = KEY.ENTER;
|
||||
|
||||
function addActiveClass($target) {
|
||||
if (!$target) {
|
||||
return;
|
||||
}
|
||||
$target.find('button').addClass('active');
|
||||
$selectedNode = $target;
|
||||
}
|
||||
|
||||
function removeActiveClass($target) {
|
||||
$target.find('button').removeClass('active');
|
||||
$selectedNode = null;
|
||||
}
|
||||
|
||||
// find next node
|
||||
function findNextNode(row, column) {
|
||||
var findNode = null;
|
||||
$.each($specialCharNode, function(idx, $node) {
|
||||
var findRow = Math.ceil((idx + 1) / COLUMN_LENGTH);
|
||||
var findColumn = ((idx + 1) % COLUMN_LENGTH === 0) ? COLUMN_LENGTH : (idx + 1) % COLUMN_LENGTH;
|
||||
if (findRow === row && findColumn === column) {
|
||||
findNode = $node;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return $(findNode);
|
||||
}
|
||||
|
||||
function arrowKeyHandler(keyCode) {
|
||||
// left, right, up, down key
|
||||
var $nextNode;
|
||||
var lastRowColumnLength = $specialCharNode.length % totalColumn;
|
||||
|
||||
if (KEY.LEFT === keyCode) {
|
||||
if (currentColumn > 1) {
|
||||
currentColumn = currentColumn - 1;
|
||||
} else if (currentRow === 1 && currentColumn === 1) {
|
||||
currentColumn = lastRowColumnLength;
|
||||
currentRow = totalRow;
|
||||
} else {
|
||||
currentColumn = totalColumn;
|
||||
currentRow = currentRow - 1;
|
||||
}
|
||||
} else if (KEY.RIGHT === keyCode) {
|
||||
if (currentRow === totalRow && lastRowColumnLength === currentColumn) {
|
||||
currentColumn = 1;
|
||||
currentRow = 1;
|
||||
} else if (currentColumn < totalColumn) {
|
||||
currentColumn = currentColumn + 1;
|
||||
} else {
|
||||
currentColumn = 1;
|
||||
currentRow = currentRow + 1;
|
||||
}
|
||||
} else if (KEY.UP === keyCode) {
|
||||
if (currentRow === 1 && lastRowColumnLength < currentColumn) {
|
||||
currentRow = totalRow - 1;
|
||||
} else {
|
||||
currentRow = currentRow - 1;
|
||||
}
|
||||
} else if (KEY.DOWN === keyCode) {
|
||||
currentRow = currentRow + 1;
|
||||
}
|
||||
|
||||
if (currentRow === totalRow && currentColumn > lastRowColumnLength) {
|
||||
currentRow = 1;
|
||||
} else if (currentRow > totalRow) {
|
||||
currentRow = 1;
|
||||
} else if (currentRow < 1) {
|
||||
currentRow = totalRow;
|
||||
}
|
||||
|
||||
$nextNode = findNextNode(currentRow, currentColumn);
|
||||
|
||||
if ($nextNode) {
|
||||
removeActiveClass($selectedNode);
|
||||
addActiveClass($nextNode);
|
||||
}
|
||||
}
|
||||
|
||||
function enterKeyHandler() {
|
||||
if (!$selectedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
deferred.resolve(decodeURIComponent($selectedNode.find('button').attr('data-value')));
|
||||
$specialCharDialog.modal('hide');
|
||||
}
|
||||
|
||||
function keyDownEventHandler(event) {
|
||||
event.preventDefault();
|
||||
var keyCode = event.keyCode;
|
||||
if (keyCode === undefined || keyCode === null) {
|
||||
return;
|
||||
}
|
||||
// check arrowKeys match
|
||||
if (ARROW_KEYS.indexOf(keyCode) > -1) {
|
||||
if ($selectedNode === null) {
|
||||
addActiveClass($specialCharNode.eq(0));
|
||||
currentColumn = 1;
|
||||
currentRow = 1;
|
||||
return;
|
||||
}
|
||||
arrowKeyHandler(keyCode);
|
||||
} else if (keyCode === ENTER_KEY) {
|
||||
enterKeyHandler();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove class
|
||||
removeActiveClass($specialCharNode);
|
||||
|
||||
// find selected node
|
||||
if (text) {
|
||||
for (var i = 0; i < $specialCharNode.length; i++) {
|
||||
var $checkNode = $($specialCharNode[i]);
|
||||
if ($checkNode.text() === text) {
|
||||
addActiveClass($checkNode);
|
||||
currentRow = Math.ceil((i + 1) / COLUMN_LENGTH);
|
||||
currentColumn = (i + 1) % COLUMN_LENGTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.onDialogShown(self.$dialog, function() {
|
||||
$(document).on('keydown', keyDownEventHandler);
|
||||
|
||||
self.$dialog.find('button').tooltip();
|
||||
|
||||
$specialCharNode.on('click', function(event) {
|
||||
event.preventDefault();
|
||||
deferred.resolve(decodeURIComponent($(event.currentTarget).find('button').attr('data-value')));
|
||||
ui.hideDialog(self.$dialog);
|
||||
});
|
||||
});
|
||||
|
||||
ui.onDialogHidden(self.$dialog, function() {
|
||||
$specialCharNode.off('click');
|
||||
|
||||
self.$dialog.find('button').tooltip('destroy');
|
||||
|
||||
$(document).off('keydown', keyDownEventHandler);
|
||||
|
||||
if (deferred.state() === 'pending') {
|
||||
deferred.reject();
|
||||
}
|
||||
});
|
||||
|
||||
ui.showDialog(self.$dialog);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
}));
|
||||
@@ -1,447 +0,0 @@
|
||||
/* Theme Variables
|
||||
------------------------------------------*/
|
||||
/* Frame Mode Layout
|
||||
------------------------------------------*/
|
||||
.note-editor {
|
||||
border: 1px solid #a9a9a9;
|
||||
position: relative;
|
||||
/* overflow: hidden; ODOO: removed for embedded editor */
|
||||
/* dropzone */
|
||||
/* codeview mode */
|
||||
/* fullscreen mode */
|
||||
/* statusbar */
|
||||
}
|
||||
.note-editor .note-dropzone {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
color: lightskyblue;
|
||||
background-color: white;
|
||||
opacity: 0.95;
|
||||
pointer-event: none;
|
||||
}
|
||||
.note-editor .note-dropzone .note-dropzone-message {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.note-editor .note-dropzone.hover {
|
||||
color: #098ddf;
|
||||
}
|
||||
.note-editor.dragover .note-dropzone {
|
||||
display: table;
|
||||
}
|
||||
.note-editor.codeview .note-editing-area .note-editable {
|
||||
display: none;
|
||||
}
|
||||
.note-editor.codeview .note-editing-area .note-codable {
|
||||
display: block;
|
||||
}
|
||||
.note-editor.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1050;
|
||||
/* bs3 modal-backdrop: 1030, bs2: 1040 */
|
||||
}
|
||||
.note-editor.fullscreen .note-editable {
|
||||
background-color: white;
|
||||
}
|
||||
.note-editor.fullscreen .note-resizebar {
|
||||
display: none;
|
||||
}
|
||||
.note-editor .note-editing-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* editable */
|
||||
/* codeable */
|
||||
}
|
||||
.note-editor .note-editing-area .note-editable {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
}
|
||||
.note-editor .note-editing-area .note-editable[contenteditable=true]:empty:not(:focus):before {
|
||||
content: attr(data-placeholder);
|
||||
}
|
||||
.note-editor .note-editing-area .note-editable[contenteditable="false"] {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
.note-editor .note-editing-area .note-codable {
|
||||
display: none;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
font-family: Menlo, Monaco, monospace, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
background-color: #222;
|
||||
resize: none;
|
||||
/* override BS2 default style */
|
||||
box-sizing: border-box;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.note-editor .note-statusbar {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.note-editor .note-statusbar .note-resizebar {
|
||||
padding-top: 1px;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.note-editor .note-statusbar .note-resizebar .note-icon-bar {
|
||||
width: 20px;
|
||||
margin: 1px auto;
|
||||
border-top: 1px solid #a9a9a9;
|
||||
}
|
||||
/* Air Mode Layout
|
||||
------------------------------------------*/
|
||||
.note-air-editor {
|
||||
outline: none;
|
||||
}
|
||||
/* Popover
|
||||
------------------------------------------*/
|
||||
.note-popover .popover {
|
||||
max-width: none;
|
||||
}
|
||||
.note-popover .popover .popover-body a {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
/* for FF */
|
||||
vertical-align: middle;
|
||||
/* for FF */
|
||||
}
|
||||
.note-popover .popover .arrow {
|
||||
left: 20px;
|
||||
}
|
||||
/* Popover and Toolbar (Button container)
|
||||
------------------------------------------*/
|
||||
.note-popover .popover .popover-body,
|
||||
.panel-heading.note-toolbar {
|
||||
margin: 0;
|
||||
padding: 0 0 5px 5px;
|
||||
/* dropdown-menu for toolbar and popover */
|
||||
/* color palette for toolbar and popover */
|
||||
}
|
||||
.note-popover .popover .popover-body > .btn-group,
|
||||
.panel-heading.note-toolbar > .btn-group {
|
||||
margin-top: 5px;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .btn-group .note-table,
|
||||
.panel-heading.note-toolbar .btn-group .note-table {
|
||||
min-width: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .btn-group .note-table .note-dimension-picker,
|
||||
.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker {
|
||||
font-size: 18px;
|
||||
}
|
||||
.note-popover .popover .popover-body .btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher,
|
||||
.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher {
|
||||
position: absolute !important;
|
||||
z-index: 3;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-popover .popover .popover-body .btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted,
|
||||
.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted {
|
||||
position: relative !important;
|
||||
z-index: 1;
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIj4+Pjp6ekKlAqjAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKhmnaJzPAAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat;
|
||||
}
|
||||
.note-popover .popover .popover-body .btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted,
|
||||
.panel-heading.note-toolbar .btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted {
|
||||
position: absolute !important;
|
||||
z-index: 2;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIjd6vvD2f9LKLW+AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKwNDEVT0AAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-style h1,
|
||||
.panel-heading.note-toolbar .note-style h1,
|
||||
.note-popover .popover .popover-body .note-style h2,
|
||||
.panel-heading.note-toolbar .note-style h2,
|
||||
.note-popover .popover .popover-body .note-style h3,
|
||||
.panel-heading.note-toolbar .note-style h3,
|
||||
.note-popover .popover .popover-body .note-style h4,
|
||||
.panel-heading.note-toolbar .note-style h4,
|
||||
.note-popover .popover .popover-body .note-style h5,
|
||||
.panel-heading.note-toolbar .note-style h5,
|
||||
.note-popover .popover .popover-body .note-style h6,
|
||||
.panel-heading.note-toolbar .note-style h6,
|
||||
.note-popover .popover .popover-body .note-style blockquote,
|
||||
.panel-heading.note-toolbar .note-style blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-toggle,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-toggle {
|
||||
width: 20px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu {
|
||||
min-width: 340px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu .btn-group,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group {
|
||||
margin: 0;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu .btn-group:first-child,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group:first-child {
|
||||
margin: 0 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu .btn-group .note-palette-title,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-palette-title {
|
||||
font-size: 12px;
|
||||
margin: 2px 7px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu .btn-group .note-color-reset,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-color-reset {
|
||||
font-size: 11px;
|
||||
margin: 3px;
|
||||
padding: 0 3px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu .btn-group .note-color-row,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-color-row {
|
||||
height: 20px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color .dropdown-menu .btn-group .note-color-reset:hover,
|
||||
.panel-heading.note-toolbar .note-color .dropdown-menu .btn-group .note-color-reset:hover {
|
||||
background: #eee;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-para .dropdown-menu,
|
||||
.panel-heading.note-toolbar .note-para .dropdown-menu {
|
||||
min-width: 216px;
|
||||
padding: 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-para .dropdown-menu > div:first-child,
|
||||
.panel-heading.note-toolbar .note-para .dropdown-menu > div:first-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.note-popover .popover .popover-body .dropdown-menu,
|
||||
.panel-heading.note-toolbar .dropdown-menu {
|
||||
min-width: 90px;
|
||||
/* dropdown-menu right position */
|
||||
/* http://forrst.com/posts/Bootstrap_right_positioned_dropdown-2KB */
|
||||
/* dropdown-menu for selectbox */
|
||||
}
|
||||
.note-popover .popover .popover-body .dropdown-menu.right,
|
||||
.panel-heading.note-toolbar .dropdown-menu.right {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
.note-popover .popover .popover-body .dropdown-menu.right::before,
|
||||
.panel-heading.note-toolbar .dropdown-menu.right::before {
|
||||
right: 9px;
|
||||
left: auto !important;
|
||||
}
|
||||
.note-popover .popover .popover-body .dropdown-menu.right::after,
|
||||
.panel-heading.note-toolbar .dropdown-menu.right::after {
|
||||
right: 10px;
|
||||
left: auto !important;
|
||||
}
|
||||
.note-popover .popover .popover-body .dropdown-menu.note-check li a i,
|
||||
.panel-heading.note-toolbar .dropdown-menu.note-check li a i {
|
||||
color: deepskyblue;
|
||||
visibility: hidden;
|
||||
}
|
||||
.note-popover .popover .popover-body .dropdown-menu.note-check li a.checked i,
|
||||
.panel-heading.note-toolbar .dropdown-menu.note-check li a.checked i {
|
||||
visibility: visible;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-fontsize-10,
|
||||
.panel-heading.note-toolbar .note-fontsize-10 {
|
||||
font-size: 10px;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color-palette,
|
||||
.panel-heading.note-toolbar .note-color-palette {
|
||||
line-height: 1;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color-palette div .note-color-btn,
|
||||
.panel-heading.note-toolbar .note-color-palette div .note-color-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
.note-popover .popover .popover-body .note-color-palette div .note-color-btn:hover,
|
||||
.panel-heading.note-toolbar .note-color-palette div .note-color-btn:hover {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
/* Dialog
|
||||
------------------------------------------*/
|
||||
.note-dialog > div {
|
||||
display: none;
|
||||
/* BS2's hide pacth. */
|
||||
}
|
||||
.note-dialog .form-group {
|
||||
/* overwrite BS's form-horizontal minus margins */
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.note-dialog .note-modal-form {
|
||||
margin: 0;
|
||||
/* overwrite BS2's form margin bottom */
|
||||
}
|
||||
.note-dialog .note-image-dialog .note-dropzone {
|
||||
min-height: 100px;
|
||||
font-size: 30px;
|
||||
line-height: 4;
|
||||
/* vertical-align */
|
||||
color: lightgray;
|
||||
text-align: center;
|
||||
border: 4px dashed lightgray;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.note-dialog .note-help-dialog {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
background-color: #222 !important;
|
||||
opacity: 0.9;
|
||||
/* BS2's background pacth. */
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.note-dialog .note-help-dialog .modal-content {
|
||||
background: transparent;
|
||||
border: 1px solid white;
|
||||
box-shadow: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.note-dialog .note-help-dialog a {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
.note-dialog .note-help-dialog .title {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: white 1px solid;
|
||||
}
|
||||
.note-dialog .note-help-dialog .modal-close {
|
||||
font-size: 14px;
|
||||
color: #dd0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-dialog .note-help-dialog .text-center {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
.note-dialog .note-help-dialog .note-shortcut {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.note-dialog .note-help-dialog .note-shortcut-row {
|
||||
margin-right: -5px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
.note-dialog .note-help-dialog .note-shortcut-col {
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.note-dialog .note-help-dialog .note-shortcut-title {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #dd0;
|
||||
}
|
||||
.note-dialog .note-help-dialog .note-shortcut-key {
|
||||
font-family: "Courier New";
|
||||
color: #dd0;
|
||||
text-align: right;
|
||||
}
|
||||
/* Handle
|
||||
------------------------------------------*/
|
||||
.note-handle {
|
||||
/* control selection */
|
||||
}
|
||||
.note-handle .note-control-selection {
|
||||
position: absolute;
|
||||
display: none;
|
||||
border: 1px solid black;
|
||||
}
|
||||
.note-handle .note-control-selection > div {
|
||||
position: absolute;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-selection-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-handle {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-holder {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-sizing {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-nw {
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-ne {
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-sw {
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-se {
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-se.note-control-holder {
|
||||
cursor: default;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
}
|
||||
.note-handle .note-control-selection .note-control-selection-info {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
color: white;
|
||||
background-color: black;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
define([
|
||||
'summernote/core/agent',
|
||||
'summernote/core/func',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/async',
|
||||
'summernote/core/key',
|
||||
'summernote/core/list',
|
||||
'summernote/editing/History',
|
||||
'summernote/module/Editor',
|
||||
'summernote/module/Toolbar',
|
||||
'summernote/module/Statusbar',
|
||||
'summernote/module/Popover',
|
||||
'summernote/module/Handle',
|
||||
'summernote/module/Fullscreen',
|
||||
'summernote/module/Codeview',
|
||||
'summernote/module/DragAndDrop',
|
||||
'summernote/module/Clipboard',
|
||||
'summernote/module/LinkDialog',
|
||||
'summernote/module/ImageDialog',
|
||||
'summernote/module/HelpDialog'
|
||||
], function (agent, func, dom, async, key, list, History,
|
||||
Editor, Toolbar, Statusbar, Popover, Handle, Fullscreen, Codeview,
|
||||
DragAndDrop, Clipboard, LinkDialog, ImageDialog, HelpDialog) {
|
||||
|
||||
/**
|
||||
* @class EventHandler
|
||||
*
|
||||
* EventHandler
|
||||
* - TODO: new instance per a editor
|
||||
*/
|
||||
var EventHandler = function () {
|
||||
var self = this;
|
||||
|
||||
/**
|
||||
* Modules
|
||||
*/
|
||||
var modules = this.modules = {
|
||||
editor: new Editor(this),
|
||||
toolbar: new Toolbar(this),
|
||||
statusbar: new Statusbar(this),
|
||||
popover: new Popover(this),
|
||||
handle: new Handle(this),
|
||||
fullscreen: new Fullscreen(this),
|
||||
codeview: new Codeview(this),
|
||||
dragAndDrop: new DragAndDrop(this),
|
||||
clipboard: new Clipboard(this),
|
||||
linkDialog: new LinkDialog(this),
|
||||
imageDialog: new ImageDialog(this),
|
||||
helpDialog: new HelpDialog(this)
|
||||
};
|
||||
|
||||
/**
|
||||
* invoke module's method
|
||||
*
|
||||
* @param {String} moduleAndMethod - ex) 'editor.redo'
|
||||
* @param {...*} arguments - arguments of method
|
||||
* @return {*}
|
||||
*/
|
||||
this.invoke = function () {
|
||||
var moduleAndMethod = list.head(list.from(arguments));
|
||||
var args = list.tail(list.from(arguments));
|
||||
|
||||
var splits = moduleAndMethod.split('.');
|
||||
var hasSeparator = splits.length > 1;
|
||||
var moduleName = hasSeparator && list.head(splits);
|
||||
var methodName = hasSeparator ? list.last(splits) : list.head(splits);
|
||||
|
||||
var module = this.getModule(moduleName);
|
||||
var method = module[methodName];
|
||||
|
||||
return method && method.apply(module, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns module
|
||||
*
|
||||
* @param {String} moduleName - name of module
|
||||
* @return {Module} - defaults is editor
|
||||
*/
|
||||
this.getModule = function (moduleName) {
|
||||
return this.modules[moduleName] || this.modules.editor;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {jQuery} $holder
|
||||
* @param {Object} callbacks
|
||||
* @param {String} eventNamespace
|
||||
* @returns {Function}
|
||||
*/
|
||||
var bindCustomEvent = this.bindCustomEvent = function ($holder, callbacks, eventNamespace) {
|
||||
return function () {
|
||||
var callback = callbacks[func.namespaceToCamel(eventNamespace, 'on')];
|
||||
if (callback) {
|
||||
callback.apply($holder[0], arguments);
|
||||
}
|
||||
return $holder.trigger('summernote.' + eventNamespace, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* insert Images from file array.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} layoutInfo
|
||||
* @param {File[]} files
|
||||
*/
|
||||
this.insertImages = function (layoutInfo, files) {
|
||||
var $editor = layoutInfo.editor(),
|
||||
$editable = layoutInfo.editable(),
|
||||
$holder = layoutInfo.holder();
|
||||
|
||||
var callbacks = $editable.data('callbacks');
|
||||
var options = $editor.data('options');
|
||||
|
||||
// If onImageUpload options setted
|
||||
if (callbacks.onImageUpload) {
|
||||
bindCustomEvent($holder, callbacks, 'image.upload')(files);
|
||||
// else insert Image as dataURL
|
||||
} else {
|
||||
$.each(files, function (idx, file) {
|
||||
var filename = file.name;
|
||||
if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) {
|
||||
bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError);
|
||||
} else {
|
||||
async.readFileAsDataURL(file).then(function (sDataURL) {
|
||||
modules.editor.insertImage($editable, sDataURL, filename);
|
||||
}).fail(function () {
|
||||
bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var commands = {
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
showLinkDialog: function (layoutInfo) {
|
||||
modules.linkDialog.show(layoutInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
showImageDialog: function (layoutInfo) {
|
||||
modules.imageDialog.show(layoutInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
showHelpDialog: function (layoutInfo) {
|
||||
modules.helpDialog.show(layoutInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
fullscreen: function (layoutInfo) {
|
||||
modules.fullscreen.toggle(layoutInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
codeview: function (layoutInfo) {
|
||||
modules.codeview.toggle(layoutInfo);
|
||||
}
|
||||
};
|
||||
|
||||
var hMousedown = function (event) {
|
||||
//preventDefault Selection for FF, IE8+
|
||||
if (dom.isImg(event.target)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
var hKeyupAndMouseup = function (event) {
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
modules.editor.removeBogus(layoutInfo.editable());
|
||||
hToolbarAndPopoverUpdate(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* update sytle info
|
||||
* @param {Object} styleInfo
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.updateStyleInfo = function (styleInfo, layoutInfo) {
|
||||
if (!styleInfo) {
|
||||
return;
|
||||
}
|
||||
var isAirMode = (layoutInfo.editor().data('options') || {}).airMode;
|
||||
if (!isAirMode) {
|
||||
modules.toolbar.update(layoutInfo.toolbar(), styleInfo);
|
||||
}
|
||||
|
||||
modules.popover.update(layoutInfo.popover(), styleInfo, isAirMode);
|
||||
modules.handle.update(layoutInfo.handle(), styleInfo, isAirMode);
|
||||
};
|
||||
|
||||
var hToolbarAndPopoverUpdate = function (event) {
|
||||
var target = event.target;
|
||||
// delay for range after mouseup
|
||||
setTimeout(function () {
|
||||
var layoutInfo = dom.makeLayoutInfo(target);
|
||||
/* ODOO: (start_modification */
|
||||
var $editable = layoutInfo.editable();
|
||||
if (event.setStyleInfoFromEditable) {
|
||||
var styleInfo = modules.editor.styleFromNode($editable);
|
||||
} else {
|
||||
if (!event.isDefaultPrevented()) {
|
||||
modules.editor.saveRange($editable);
|
||||
}
|
||||
var styleInfo = modules.editor.currentStyle(target);
|
||||
}
|
||||
/* ODOO: end_modification) */
|
||||
self.updateStyleInfo(styleInfo, layoutInfo);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
var hScroll = function (event) {
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
//hide popover and handle when scrolled
|
||||
modules.popover.hide(layoutInfo.popover());
|
||||
modules.handle.hide(layoutInfo.handle());
|
||||
};
|
||||
|
||||
var hToolbarAndPopoverMousedown = function (event) {
|
||||
// prevent default event when insertTable (FF, Webkit)
|
||||
var $btn = $(event.target).closest('[data-event]');
|
||||
if ($btn.length) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
var hToolbarAndPopoverClick = function (event) {
|
||||
var $btn = $(event.target).closest('[data-event]');
|
||||
|
||||
if (!$btn.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var eventName = $btn.attr('data-event'),
|
||||
value = $btn.attr('data-value'),
|
||||
hide = $btn.attr('data-hide');
|
||||
|
||||
var layoutInfo = dom.makeLayoutInfo(event.target);
|
||||
|
||||
// before command: detect control selection element($target)
|
||||
var $target;
|
||||
if ($.inArray(eventName, ['resize', 'floatMe', 'removeMedia', 'imageShape']) !== -1) {
|
||||
var $selection = layoutInfo.handle().find('.note-control-selection');
|
||||
$target = $($selection.data('target'));
|
||||
}
|
||||
|
||||
// If requested, hide the popover when the button is clicked.
|
||||
// Useful for things like showHelpDialog.
|
||||
if (hide) {
|
||||
$btn.parents('.popover').hide();
|
||||
}
|
||||
|
||||
if ($.isFunction($.summernote.pluginEvents[eventName])) {
|
||||
$.summernote.pluginEvents[eventName](event, modules.editor, layoutInfo, value);
|
||||
} else if (modules.editor[eventName]) { // on command
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.focus();
|
||||
modules.editor[eventName]($editable, value, $target);
|
||||
event.preventDefault();
|
||||
} else if (commands[eventName]) {
|
||||
commands[eventName].call(this, layoutInfo);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// after command
|
||||
if ($.inArray(eventName, ['backColor', 'foreColor']) !== -1) {
|
||||
var options = layoutInfo.editor().data('options', options);
|
||||
var module = options.airMode ? modules.popover : modules.toolbar;
|
||||
module.updateRecentColor(list.head($btn), eventName, value);
|
||||
}
|
||||
|
||||
hToolbarAndPopoverUpdate(event);
|
||||
};
|
||||
|
||||
var PX_PER_EM = 18;
|
||||
var hDimensionPickerMove = function (event, options) {
|
||||
var $picker = $(event.target.parentNode); // target is mousecatcher
|
||||
var $dimensionDisplay = $picker.next();
|
||||
var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
|
||||
var $highlighted = $picker.find('.note-dimension-picker-highlighted');
|
||||
var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
|
||||
|
||||
var posOffset;
|
||||
// HTML5 with jQuery - e.offsetX is undefined in Firefox
|
||||
if (event.offsetX === undefined) {
|
||||
var posCatcher = $(event.target).offset();
|
||||
posOffset = {
|
||||
x: event.pageX - posCatcher.left,
|
||||
y: event.pageY - posCatcher.top
|
||||
};
|
||||
} else {
|
||||
posOffset = {
|
||||
x: event.offsetX,
|
||||
y: event.offsetY
|
||||
};
|
||||
}
|
||||
|
||||
var dim = {
|
||||
c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
|
||||
r: Math.ceil(posOffset.y / PX_PER_EM) || 1
|
||||
};
|
||||
|
||||
$highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
|
||||
$catcher.attr('data-value', dim.c + 'x' + dim.r);
|
||||
|
||||
if (3 < dim.c && dim.c < options.insertTableMaxSize.col) {
|
||||
$unhighlighted.css({ width: dim.c + 1 + 'em'});
|
||||
}
|
||||
|
||||
if (3 < dim.r && dim.r < options.insertTableMaxSize.row) {
|
||||
$unhighlighted.css({ height: dim.r + 1 + 'em'});
|
||||
}
|
||||
|
||||
$dimensionDisplay.html(dim.c + ' x ' + dim.r);
|
||||
};
|
||||
|
||||
/**
|
||||
* bind KeyMap on keydown
|
||||
*
|
||||
* @param {Object} layoutInfo
|
||||
* @param {Object} keyMap
|
||||
*/
|
||||
this.bindKeyMap = function (layoutInfo, keyMap) {
|
||||
var $editor = layoutInfo.editor();
|
||||
var $editable = layoutInfo.editable();
|
||||
|
||||
$editable.on('keydown', function (event) {
|
||||
var keys = [];
|
||||
|
||||
// modifier
|
||||
if (event.metaKey) { keys.push('CMD'); }
|
||||
if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); }
|
||||
if (event.shiftKey) { keys.push('SHIFT'); }
|
||||
|
||||
// keycode
|
||||
var keyName = key.nameFromCode[event.keyCode];
|
||||
if (keyName) {
|
||||
keys.push(keyName);
|
||||
}
|
||||
|
||||
var pluginEvent;
|
||||
var keyString = keys.join('+');
|
||||
var eventName = keyMap[keyString];
|
||||
|
||||
// ODOO: (start_modification
|
||||
// odoo change: add visible event to overwrite the browser comportment
|
||||
var keycode = event.keyCode;
|
||||
if (!eventName &&
|
||||
!event.ctrlKey && !event.metaKey && ( // special code/command
|
||||
(keycode > 47 && keycode < 58) || // number keys
|
||||
keycode == 32 || keycode == 13 || // spacebar & return
|
||||
(keycode > 64 && keycode < 91) || // letter keys
|
||||
(keycode > 95 && keycode < 112) || // numpad keys
|
||||
(keycode > 185 && keycode < 193) || // ;=,-./` (in order)
|
||||
(keycode > 218 && keycode < 223))) { // [\]' (in order))
|
||||
eventName = 'visible';
|
||||
} else if (!keycode && event.key !== 'Dead') {
|
||||
self.invoke('restoreRange', $editable);
|
||||
}
|
||||
// ODOO: end_modification)
|
||||
|
||||
if (eventName) {
|
||||
// FIXME Summernote doesn't support event pipeline yet.
|
||||
// - Plugin -> Base Code
|
||||
pluginEvent = $.summernote.pluginEvents[keyString];
|
||||
if ($.isFunction(pluginEvent)) {
|
||||
if (pluginEvent(event, modules.editor, layoutInfo)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
pluginEvent = $.summernote.pluginEvents[eventName];
|
||||
|
||||
if ($.isFunction(pluginEvent)) {
|
||||
pluginEvent(event, modules.editor, layoutInfo);
|
||||
} else if (modules.editor[eventName]) {
|
||||
modules.editor[eventName]($editable, $editor.data('options'));
|
||||
event.preventDefault();
|
||||
} else if (commands[eventName]) {
|
||||
commands[eventName].call(this, layoutInfo);
|
||||
event.preventDefault();
|
||||
}
|
||||
} else if (key.isEdit(event.keyCode)) {
|
||||
modules.editor.afterCommand($editable);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* attach eventhandler
|
||||
*
|
||||
* @param {Object} layoutInfo - layout Informations
|
||||
* @param {Object} options - user options include custom event handlers
|
||||
*/
|
||||
this.attach = function (layoutInfo, options) {
|
||||
// handlers for editable
|
||||
if (options.shortcuts) {
|
||||
this.bindKeyMap(layoutInfo, options.keyMap[agent.isMac ? 'mac' : 'pc']);
|
||||
}
|
||||
layoutInfo.editable().on('mousedown', hMousedown);
|
||||
layoutInfo.editable().on('keyup mouseup', hKeyupAndMouseup);
|
||||
layoutInfo.editable().on('scroll', hScroll);
|
||||
|
||||
// handler for clipboard
|
||||
modules.clipboard.attach(layoutInfo, options);
|
||||
|
||||
// handler for handle and popover
|
||||
modules.handle.attach(layoutInfo, options);
|
||||
layoutInfo.popover().on('click', hToolbarAndPopoverClick);
|
||||
layoutInfo.popover().on('mousedown', hToolbarAndPopoverMousedown);
|
||||
|
||||
// handler for drag and drop
|
||||
modules.dragAndDrop.attach(layoutInfo, options);
|
||||
|
||||
// handlers for frame mode (toolbar, statusbar)
|
||||
if (!options.airMode) {
|
||||
// handler for toolbar
|
||||
layoutInfo.toolbar().on('click', hToolbarAndPopoverClick);
|
||||
layoutInfo.toolbar().on('mousedown', hToolbarAndPopoverMousedown);
|
||||
|
||||
// handler for statusbar
|
||||
modules.statusbar.attach(layoutInfo, options);
|
||||
}
|
||||
|
||||
// handler for table dimension
|
||||
var $catcherContainer = options.airMode ? layoutInfo.popover() :
|
||||
layoutInfo.toolbar();
|
||||
var $catcher = $catcherContainer.find('.note-dimension-picker-mousecatcher');
|
||||
$catcher.css({
|
||||
width: options.insertTableMaxSize.col + 'em',
|
||||
height: options.insertTableMaxSize.row + 'em'
|
||||
}).on('mousemove', function (event) {
|
||||
hDimensionPickerMove(event, options);
|
||||
});
|
||||
|
||||
// save options on editor
|
||||
layoutInfo.editor().data('options', options);
|
||||
|
||||
// ret styleWithCSS for backColor / foreColor clearing with 'inherit'.
|
||||
if (!agent.isMSIE) {
|
||||
// [workaround] for Firefox
|
||||
// - protect FF Error: NS_ERROR_FAILURE: Failure
|
||||
setTimeout(function () {
|
||||
document.execCommand('styleWithCSS', 0, options.styleWithSpan);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// History
|
||||
var history = new History(layoutInfo.editable());
|
||||
layoutInfo.editable().data('NoteHistory', history);
|
||||
|
||||
// All editor status will be saved on editable with jquery's data
|
||||
// for support multiple editor with singleton object.
|
||||
layoutInfo.editable().data('callbacks', {
|
||||
onInit: options.onInit,
|
||||
onFocus: options.onFocus,
|
||||
onBlur: options.onBlur,
|
||||
onKeydown: options.onKeydown,
|
||||
onKeyup: options.onKeyup,
|
||||
onMousedown: options.onMousedown,
|
||||
onEnter: options.onEnter,
|
||||
onPaste: options.onPaste,
|
||||
onBeforeCommand: options.onBeforeCommand,
|
||||
onChange: options.onChange,
|
||||
onImageUpload: options.onImageUpload,
|
||||
onImageUploadError: options.onImageUploadError,
|
||||
onMediaDelete: options.onMediaDelete,
|
||||
onToolbarClick: options.onToolbarClick,
|
||||
onUpload: options.onUpload,
|
||||
});
|
||||
|
||||
var styleInfo = modules.editor.styleFromNode(layoutInfo.editable());
|
||||
this.updateStyleInfo(styleInfo, layoutInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* attach jquery custom event
|
||||
*
|
||||
* @param {Object} layoutInfo - layout Informations
|
||||
*/
|
||||
this.attachCustomEvent = function (layoutInfo, options) {
|
||||
var $holder = layoutInfo.holder();
|
||||
var $editable = layoutInfo.editable();
|
||||
var callbacks = $editable.data('callbacks');
|
||||
|
||||
$editable.focus(bindCustomEvent($holder, callbacks, 'focus'));
|
||||
$editable.blur(bindCustomEvent($holder, callbacks, 'blur'));
|
||||
|
||||
$editable.keydown(function (event) {
|
||||
if (event.keyCode === key.code.ENTER) {
|
||||
bindCustomEvent($holder, callbacks, 'enter').call(this, event);
|
||||
}
|
||||
bindCustomEvent($holder, callbacks, 'keydown').call(this, event);
|
||||
});
|
||||
$editable.keyup(bindCustomEvent($holder, callbacks, 'keyup'));
|
||||
|
||||
$editable.on('mousedown', bindCustomEvent($holder, callbacks, 'mousedown'));
|
||||
$editable.on('mouseup', bindCustomEvent($holder, callbacks, 'mouseup'));
|
||||
$editable.on('scroll', bindCustomEvent($holder, callbacks, 'scroll'));
|
||||
|
||||
$editable.on('paste', bindCustomEvent($holder, callbacks, 'paste'));
|
||||
|
||||
// [workaround] IE doesn't have input events for contentEditable
|
||||
// - see: https://goo.gl/4bfIvA
|
||||
var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';
|
||||
$editable.on(changeEventName, function () {
|
||||
bindCustomEvent($holder, callbacks, 'change')($editable.html(), $editable);
|
||||
});
|
||||
|
||||
if (!options.airMode) {
|
||||
layoutInfo.toolbar().click(bindCustomEvent($holder, callbacks, 'toolbar.click'));
|
||||
layoutInfo.popover().click(bindCustomEvent($holder, callbacks, 'popover.click'));
|
||||
}
|
||||
|
||||
// Textarea: auto filling the code before form submit.
|
||||
if (dom.isTextarea(list.head($holder))) {
|
||||
$holder.closest('form').submit(function (e) {
|
||||
layoutInfo.holder().val(layoutInfo.holder().code());
|
||||
bindCustomEvent($holder, callbacks, 'submit').call(this, e, $holder.code());
|
||||
});
|
||||
}
|
||||
|
||||
// textarea auto sync
|
||||
if (dom.isTextarea(list.head($holder)) && options.textareaAutoSync) {
|
||||
$holder.on('summernote.change', function () {
|
||||
layoutInfo.holder().val(layoutInfo.holder().code());
|
||||
});
|
||||
}
|
||||
|
||||
// fire init event
|
||||
bindCustomEvent($holder, callbacks, 'init')(layoutInfo);
|
||||
|
||||
// fire plugin init event
|
||||
for (var i = 0, len = $.summernote.plugins.length; i < len; i++) {
|
||||
if ($.isFunction($.summernote.plugins[i].init)) {
|
||||
$.summernote.plugins[i].init(layoutInfo);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.detach = function (layoutInfo, options) {
|
||||
layoutInfo.holder().off();
|
||||
layoutInfo.editable().off();
|
||||
|
||||
layoutInfo.popover().off();
|
||||
layoutInfo.handle().off();
|
||||
layoutInfo.dialog().off();
|
||||
|
||||
if (!options.airMode) {
|
||||
layoutInfo.dropzone().off();
|
||||
layoutInfo.toolbar().off();
|
||||
layoutInfo.statusbar().off();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return EventHandler;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
||||
require.config({
|
||||
baseUrl: 'src/js',
|
||||
paths: {
|
||||
jquery: '//code.jquery.com/jquery-1.11.3',
|
||||
bootstrap: '//netdna.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min',
|
||||
summernotevideo: '/../../plugin/summernote-ext-video',
|
||||
CodeMirror: '//cdnjs.cloudflare.com/ajax/libs/codemirror/3.20.0/codemirror',
|
||||
CodeMirrorXml: '//cdnjs.cloudflare.com/ajax/libs/codemirror/3.20.0/mode/xml/xml.min',
|
||||
CodeMirrorFormatting: '//cdnjs.cloudflare.com/ajax/libs/codemirror/2.36.0/formatting.min'
|
||||
},
|
||||
shim: {
|
||||
bootstrap: ['jquery'],
|
||||
CodeMirror: { exports: 'CodeMirror' },
|
||||
CodeMirrorXml: ['CodeMirror'],
|
||||
CodeMirrorFormatting: ['CodeMirror', 'CodeMirrorXml'],
|
||||
summernotevideo: ['summernote']
|
||||
},
|
||||
packages: [{
|
||||
name: 'summernote',
|
||||
location: './',
|
||||
main: 'summernote'
|
||||
}]
|
||||
});
|
||||
|
||||
require([
|
||||
'jquery', 'bootstrap', 'CodeMirrorFormatting',
|
||||
'summernote', 'summernotevideo'
|
||||
], function ($) {
|
||||
// summernote
|
||||
$('.summernote').summernote({
|
||||
height: 300, // set editable area's height
|
||||
focus: true, // set focus editable area after summernote loaded
|
||||
tabsize: 2, // size of tab
|
||||
placeholder: 'Type your message here...', // set editable area's placeholder text
|
||||
codemirror: { // code mirror options
|
||||
mode: 'text/html',
|
||||
htmlMode: true,
|
||||
lineNumbers: true,
|
||||
theme: 'monokai'
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,170 +0,0 @@
|
||||
define(['jquery'], function ($) {
|
||||
if (!Array.prototype.reduce) {
|
||||
/**
|
||||
* Array.prototype.reduce polyfill
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @param {Value} [initialValue]
|
||||
* @return {Value}
|
||||
*
|
||||
* @see http://goo.gl/WNriQD
|
||||
*/
|
||||
Array.prototype.reduce = function (callback) {
|
||||
var t = Object(this), len = t.length >>> 0, k = 0, value;
|
||||
if (arguments.length === 2) {
|
||||
value = arguments[1];
|
||||
} else {
|
||||
while (k < len && !(k in t)) {
|
||||
k++;
|
||||
}
|
||||
if (k >= len) {
|
||||
throw new TypeError('Reduce of empty array with no initial value');
|
||||
}
|
||||
value = t[k++];
|
||||
}
|
||||
for (; k < len; k++) {
|
||||
if (k in t) {
|
||||
value = callback(value, t[k], k, t);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
if ('function' !== typeof Array.prototype.filter) {
|
||||
/**
|
||||
* Array.prototype.filter polyfill
|
||||
*
|
||||
* @param {Function} func
|
||||
* @return {Array}
|
||||
*
|
||||
* @see http://goo.gl/T1KFnq
|
||||
*/
|
||||
Array.prototype.filter = function (func) {
|
||||
var t = Object(this), len = t.length >>> 0;
|
||||
|
||||
var res = [];
|
||||
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
var val = t[i];
|
||||
if (func.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.prototype.map) {
|
||||
/**
|
||||
* Array.prototype.map polyfill
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @return {Array}
|
||||
*
|
||||
* @see https://goo.gl/SMWaMK
|
||||
*/
|
||||
Array.prototype.map = function (callback, thisArg) {
|
||||
var T, A, k;
|
||||
if (this === null) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
var O = Object(this);
|
||||
var len = O.length >>> 0;
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
A = new Array(len);
|
||||
k = 0;
|
||||
|
||||
while (k < len) {
|
||||
var kValue, mappedValue;
|
||||
if (k in O) {
|
||||
kValue = O[k];
|
||||
mappedValue = callback.call(T, kValue, k, O);
|
||||
A[k] = mappedValue;
|
||||
}
|
||||
k++;
|
||||
}
|
||||
return A;
|
||||
};
|
||||
}
|
||||
|
||||
var isSupportAmd = typeof define === 'function' && define.amd;
|
||||
|
||||
/**
|
||||
* returns whether font is installed or not.
|
||||
*
|
||||
* @param {String} fontName
|
||||
* @return {Boolean}
|
||||
*/
|
||||
var isFontInstalled = function (fontName) {
|
||||
var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
|
||||
var $tester = $('<div>').css({
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
top: '-9999px',
|
||||
fontSize: '200px'
|
||||
}).text('mmmmmmmmmwwwwwww').appendTo(document.body);
|
||||
|
||||
var originalWidth = $tester.css('fontFamily', testFontName).width();
|
||||
var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
|
||||
|
||||
$tester.remove();
|
||||
|
||||
return originalWidth !== width;
|
||||
};
|
||||
|
||||
var userAgent = navigator.userAgent;
|
||||
var isMSIE = /MSIE|Trident/i.test(userAgent);
|
||||
var browserVersion;
|
||||
if (isMSIE) {
|
||||
var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
|
||||
if (matches) {
|
||||
browserVersion = parseFloat(matches[1]);
|
||||
}
|
||||
matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
|
||||
if (matches) {
|
||||
browserVersion = parseFloat(matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class core.agent
|
||||
*
|
||||
* Object which check platform and agent
|
||||
*
|
||||
* @singleton
|
||||
* @alternateClassName agent
|
||||
*/
|
||||
var agent = {
|
||||
/** @property {Boolean} [isMac=false] true if this agent is Mac */
|
||||
isMac: navigator.appVersion.indexOf('Mac') > -1,
|
||||
/** @property {Boolean} [isMSIE=false] true if this agent is a Internet Explorer */
|
||||
isMSIE: isMSIE,
|
||||
/** @property {Boolean} [isFF=false] true if this agent is a Firefox */
|
||||
isFF: /firefox/i.test(userAgent),
|
||||
isWebkit: /webkit/i.test(userAgent),
|
||||
/** @property {Boolean} [isSafari=false] true if this agent is a Safari */
|
||||
isSafari: /safari/i.test(userAgent),
|
||||
/** @property {Float} browserVersion current browser version */
|
||||
browserVersion: browserVersion,
|
||||
/** @property {String} jqueryVersion current jQuery version string */
|
||||
jqueryVersion: parseFloat($.fn.jquery),
|
||||
isSupportAmd: isSupportAmd,
|
||||
hasCodeMirror: isSupportAmd ? require.specified('CodeMirror') : !!window.CodeMirror,
|
||||
isFontInstalled: isFontInstalled,
|
||||
isW3CRangeSupport: !!document.createRange
|
||||
};
|
||||
|
||||
return agent;
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
define('summernote/core/async', function () {
|
||||
/**
|
||||
* @class core.async
|
||||
*
|
||||
* Async functions which returns `Promise`
|
||||
*
|
||||
* @singleton
|
||||
* @alternateClassName async
|
||||
*/
|
||||
var async = (function () {
|
||||
/**
|
||||
* @method readFileAsDataURL
|
||||
*
|
||||
* read contents of file as representing URL
|
||||
*
|
||||
* @param {File} file
|
||||
* @return {Promise} - then: sDataUrl
|
||||
*/
|
||||
var readFileAsDataURL = function (file) {
|
||||
return $.Deferred(function (deferred) {
|
||||
$.extend(new FileReader(), {
|
||||
onload: function (e) {
|
||||
var sDataURL = e.target.result;
|
||||
deferred.resolve(sDataURL);
|
||||
},
|
||||
onerror: function () {
|
||||
deferred.reject(this);
|
||||
}
|
||||
}).readAsDataURL(file);
|
||||
}).promise();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method createImage
|
||||
*
|
||||
* create `<image>` from url string
|
||||
*
|
||||
* @param {String} sUrl
|
||||
* @param {String} filename
|
||||
* @return {Promise} - then: $image
|
||||
*/
|
||||
var createImage = function (sUrl, filename) {
|
||||
return $.Deferred(function (deferred) {
|
||||
var $img = $('<img>');
|
||||
|
||||
$img.one('load', function () {
|
||||
$img.off('error abort');
|
||||
deferred.resolve($img);
|
||||
}).one('error abort', function () {
|
||||
$img.off('load').detach();
|
||||
deferred.reject($img);
|
||||
}).css({
|
||||
display: 'none'
|
||||
}).appendTo(document.body).attr({
|
||||
'src': sUrl,
|
||||
'data-filename': filename
|
||||
});
|
||||
}).promise();
|
||||
};
|
||||
|
||||
return {
|
||||
readFileAsDataURL: readFileAsDataURL,
|
||||
createImage: createImage
|
||||
};
|
||||
})();
|
||||
|
||||
return async;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,130 +0,0 @@
|
||||
define('summernote/core/func', function () {
|
||||
/**
|
||||
* @class core.func
|
||||
*
|
||||
* func utils (for high-order func's arg)
|
||||
*
|
||||
* @singleton
|
||||
* @alternateClassName func
|
||||
*/
|
||||
var func = (function () {
|
||||
var eq = function (itemA) {
|
||||
return function (itemB) {
|
||||
return itemA === itemB;
|
||||
};
|
||||
};
|
||||
|
||||
var eq2 = function (itemA, itemB) {
|
||||
return itemA === itemB;
|
||||
};
|
||||
|
||||
var peq2 = function (propName) {
|
||||
return function (itemA, itemB) {
|
||||
return itemA[propName] === itemB[propName];
|
||||
};
|
||||
};
|
||||
|
||||
var ok = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
var fail = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
var not = function (f) {
|
||||
return function () {
|
||||
return !f.apply(f, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
var and = function (fA, fB) {
|
||||
return function (item) {
|
||||
return fA(item) && fB(item);
|
||||
};
|
||||
};
|
||||
|
||||
var self = function (a) {
|
||||
return a;
|
||||
};
|
||||
|
||||
var idCounter = 0;
|
||||
|
||||
/**
|
||||
* generate a globally-unique id
|
||||
*
|
||||
* @param {String} [prefix]
|
||||
*/
|
||||
var uniqueId = function (prefix) {
|
||||
var id = ++idCounter + '';
|
||||
return prefix ? prefix + id : id;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns bnd (bounds) from rect
|
||||
*
|
||||
* - IE Compatability Issue: http://goo.gl/sRLOAo
|
||||
* - Scroll Issue: http://goo.gl/sNjUc
|
||||
*
|
||||
* @param {Rect} rect
|
||||
* @return {Object} bounds
|
||||
* @return {Number} bounds.top
|
||||
* @return {Number} bounds.left
|
||||
* @return {Number} bounds.width
|
||||
* @return {Number} bounds.height
|
||||
*/
|
||||
var rect2bnd = function (rect) {
|
||||
var $document = $(document);
|
||||
return {
|
||||
top: rect.top + $document.scrollTop(),
|
||||
left: rect.left + $document.scrollLeft(),
|
||||
width: rect.right - rect.left,
|
||||
height: rect.bottom - rect.top
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* returns a copy of the object where the keys have become the values and the values the keys.
|
||||
* @param {Object} obj
|
||||
* @return {Object}
|
||||
*/
|
||||
var invertObject = function (obj) {
|
||||
var inverted = {};
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
inverted[obj[key]] = key;
|
||||
}
|
||||
}
|
||||
return inverted;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} namespace
|
||||
* @param {String} [prefix]
|
||||
* @return {String}
|
||||
*/
|
||||
var namespaceToCamel = function (namespace, prefix) {
|
||||
prefix = prefix || '';
|
||||
return prefix + namespace.split('.').map(function (name) {
|
||||
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
}).join('');
|
||||
};
|
||||
|
||||
return {
|
||||
eq: eq,
|
||||
eq2: eq2,
|
||||
peq2: peq2,
|
||||
ok: ok,
|
||||
fail: fail,
|
||||
self: self,
|
||||
not: not,
|
||||
and: and,
|
||||
uniqueId: uniqueId,
|
||||
rect2bnd: rect2bnd,
|
||||
invertObject: invertObject,
|
||||
namespaceToCamel: namespaceToCamel
|
||||
};
|
||||
})();
|
||||
|
||||
return func;
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
define([
|
||||
'summernote/core/list',
|
||||
'summernote/core/func'
|
||||
], function (list, func) {
|
||||
/**
|
||||
* @class core.key
|
||||
*
|
||||
* Object for keycodes.
|
||||
*
|
||||
* @singleton
|
||||
* @alternateClassName key
|
||||
*/
|
||||
var key = (function () {
|
||||
var keyMap = {
|
||||
'BACKSPACE': 8,
|
||||
'TAB': 9,
|
||||
'ENTER': 13,
|
||||
'SPACE': 32,
|
||||
|
||||
// Arrow
|
||||
'LEFT': 37,
|
||||
'UP': 38,
|
||||
'RIGHT': 39,
|
||||
'DOWN': 40,
|
||||
|
||||
// Number: 0-9
|
||||
'NUM0': 48,
|
||||
'NUM1': 49,
|
||||
'NUM2': 50,
|
||||
'NUM3': 51,
|
||||
'NUM4': 52,
|
||||
'NUM5': 53,
|
||||
'NUM6': 54,
|
||||
'NUM7': 55,
|
||||
'NUM8': 56,
|
||||
|
||||
// Alphabet: a-z
|
||||
'B': 66,
|
||||
'E': 69,
|
||||
'I': 73,
|
||||
'J': 74,
|
||||
'K': 75,
|
||||
'L': 76,
|
||||
'R': 82,
|
||||
'S': 83,
|
||||
'U': 85,
|
||||
'V': 86,
|
||||
'Y': 89,
|
||||
'Z': 90,
|
||||
|
||||
'SLASH': 191,
|
||||
'LEFTBRACKET': 219,
|
||||
'BACKSLASH': 220,
|
||||
'RIGHTBRACKET': 221
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* @method isEdit
|
||||
*
|
||||
* @param {Number} keyCode
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isEdit: function (keyCode) {
|
||||
return list.contains([
|
||||
keyMap.BACKSPACE,
|
||||
keyMap.TAB,
|
||||
keyMap.ENTER,
|
||||
keyMap.SPACe
|
||||
], keyCode);
|
||||
},
|
||||
/**
|
||||
* @method isMove
|
||||
*
|
||||
* @param {Number} keyCode
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isMove: function (keyCode) {
|
||||
return list.contains([
|
||||
keyMap.LEFT,
|
||||
keyMap.UP,
|
||||
keyMap.RIGHT,
|
||||
keyMap.DOWN
|
||||
], keyCode);
|
||||
},
|
||||
/**
|
||||
* @property {Object} nameFromCode
|
||||
* @property {String} nameFromCode.8 "BACKSPACE"
|
||||
*/
|
||||
nameFromCode: func.invertObject(keyMap),
|
||||
code: keyMap
|
||||
};
|
||||
})();
|
||||
|
||||
return key;
|
||||
});
|
||||
@@ -1,191 +0,0 @@
|
||||
define(['summernote/core/func'], function (func) {
|
||||
/**
|
||||
* @class core.list
|
||||
*
|
||||
* list utils
|
||||
*
|
||||
* @singleton
|
||||
* @alternateClassName list
|
||||
*/
|
||||
var list = (function () {
|
||||
/**
|
||||
* returns the first item of an array.
|
||||
*
|
||||
* @param {Array} array
|
||||
*/
|
||||
var head = function (array) {
|
||||
return array[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the last item of an array.
|
||||
*
|
||||
* @param {Array} array
|
||||
*/
|
||||
var last = function (array) {
|
||||
return array[array.length - 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* returns everything but the last entry of the array.
|
||||
*
|
||||
* @param {Array} array
|
||||
*/
|
||||
var initial = function (array) {
|
||||
return array.slice(0, array.length - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the rest of the items in an array.
|
||||
*
|
||||
* @param {Array} array
|
||||
*/
|
||||
var tail = function (array) {
|
||||
return array.slice(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns item of array
|
||||
*/
|
||||
var find = function (array, pred) {
|
||||
for (var idx = 0, len = array.length; idx < len; idx ++) {
|
||||
var item = array[idx];
|
||||
if (pred(item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns true if all of the values in the array pass the predicate truth test.
|
||||
*/
|
||||
var all = function (array, pred) {
|
||||
for (var idx = 0, len = array.length; idx < len; idx ++) {
|
||||
if (!pred(array[idx])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns index of item
|
||||
*/
|
||||
var indexOf = function (array, item) {
|
||||
return $.inArray(item, array);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns true if the value is present in the list.
|
||||
*/
|
||||
var contains = function (array, item) {
|
||||
return indexOf(array, item) !== -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* get sum from a list
|
||||
*
|
||||
* @param {Array} array - array
|
||||
* @param {Function} fn - iterator
|
||||
*/
|
||||
var sum = function (array, fn) {
|
||||
fn = fn || func.self;
|
||||
return array.reduce(function (memo, v) {
|
||||
return memo + fn(v);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns a copy of the collection with array type.
|
||||
* @param {Collection} collection - collection eg) node.childNodes, ...
|
||||
*/
|
||||
var from = function (collection) {
|
||||
var result = [], idx = -1, length = collection.length;
|
||||
while (++idx < length) {
|
||||
result[idx] = collection[idx];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* cluster elements by predicate function.
|
||||
*
|
||||
* @param {Array} array - array
|
||||
* @param {Function} fn - predicate function for cluster rule
|
||||
* @param {Array[]}
|
||||
*/
|
||||
var clusterBy = function (array, fn) {
|
||||
if (!array.length) { return []; }
|
||||
var aTail = tail(array);
|
||||
return aTail.reduce(function (memo, v) {
|
||||
var aLast = last(memo);
|
||||
if (fn(last(aLast), v)) {
|
||||
aLast[aLast.length] = v;
|
||||
} else {
|
||||
memo[memo.length] = [v];
|
||||
}
|
||||
return memo;
|
||||
}, [[head(array)]]);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns a copy of the array with all falsy values removed
|
||||
*
|
||||
* @param {Array} array - array
|
||||
* @param {Function} fn - predicate function for cluster rule
|
||||
*/
|
||||
var compact = function (array) {
|
||||
var aResult = [];
|
||||
for (var idx = 0, len = array.length; idx < len; idx ++) {
|
||||
if (array[idx]) { aResult.push(array[idx]); }
|
||||
}
|
||||
return aResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* produces a duplicate-free version of the array
|
||||
*
|
||||
* @param {Array} array
|
||||
*/
|
||||
var unique = function (array) {
|
||||
var results = [];
|
||||
|
||||
for (var idx = 0, len = array.length; idx < len; idx ++) {
|
||||
if (!contains(results, array[idx])) {
|
||||
results.push(array[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns next item.
|
||||
* @param {Array} array
|
||||
*/
|
||||
var next = function (array, item) {
|
||||
var idx = indexOf(array, item);
|
||||
if (idx === -1) { return null; }
|
||||
|
||||
return array[idx + 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* returns prev item.
|
||||
* @param {Array} array
|
||||
*/
|
||||
var prev = function (array, item) {
|
||||
var idx = indexOf(array, item);
|
||||
if (idx === -1) { return null; }
|
||||
|
||||
return array[idx - 1];
|
||||
};
|
||||
|
||||
return { head: head, last: last, initial: initial, tail: tail,
|
||||
prev: prev, next: next, find: find, contains: contains,
|
||||
all: all, sum: sum, from: from,
|
||||
clusterBy: clusterBy, compact: compact, unique: unique };
|
||||
})();
|
||||
|
||||
return list;
|
||||
});
|
||||
@@ -1,790 +0,0 @@
|
||||
define([
|
||||
'summernote/core/agent',
|
||||
'summernote/core/func',
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom'
|
||||
], function (agent, func, list, dom) {
|
||||
|
||||
var range = (function () {
|
||||
|
||||
/**
|
||||
* return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
|
||||
*
|
||||
* @param {TextRange} textRange
|
||||
* @param {Boolean} isStart
|
||||
* @return {BoundaryPoint}
|
||||
*
|
||||
* @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
|
||||
*/
|
||||
var textRangeToPoint = function (textRange, isStart) {
|
||||
var container = textRange.parentElement(), offset;
|
||||
|
||||
var tester = document.body.createTextRange(), prevContainer;
|
||||
var childNodes = list.from(container.childNodes);
|
||||
for (offset = 0; offset < childNodes.length; offset++) {
|
||||
if (dom.isText(childNodes[offset])) {
|
||||
continue;
|
||||
}
|
||||
tester.moveToElementText(childNodes[offset]);
|
||||
if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
|
||||
break;
|
||||
}
|
||||
prevContainer = childNodes[offset];
|
||||
}
|
||||
|
||||
if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
|
||||
var textRangeStart = document.body.createTextRange(), curTextNode = null;
|
||||
textRangeStart.moveToElementText(prevContainer || container);
|
||||
textRangeStart.collapse(!prevContainer);
|
||||
curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
|
||||
|
||||
var pointTester = textRange.duplicate();
|
||||
pointTester.setEndPoint('StartToStart', textRangeStart);
|
||||
var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
|
||||
|
||||
while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
|
||||
textCount -= curTextNode.nodeValue.length;
|
||||
curTextNode = curTextNode.nextSibling;
|
||||
}
|
||||
|
||||
/* jshint ignore:start */
|
||||
var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
|
||||
/* jshint ignore:end */
|
||||
|
||||
if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
|
||||
textCount === curTextNode.nodeValue.length) {
|
||||
textCount -= curTextNode.nodeValue.length;
|
||||
curTextNode = curTextNode.nextSibling;
|
||||
}
|
||||
|
||||
container = curTextNode;
|
||||
offset = textCount;
|
||||
}
|
||||
|
||||
return {
|
||||
cont: container,
|
||||
offset: offset
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* return TextRange from boundary point (inspired by google closure-library)
|
||||
* @param {BoundaryPoint} point
|
||||
* @return {TextRange}
|
||||
*/
|
||||
var pointToTextRange = function (point) {
|
||||
var textRangeInfo = function (container, offset) {
|
||||
var node, isCollapseToStart;
|
||||
|
||||
if (dom.isText(container)) {
|
||||
var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
|
||||
var prevContainer = list.last(prevTextNodes).previousSibling;
|
||||
node = prevContainer || container.parentNode;
|
||||
offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
|
||||
isCollapseToStart = !prevContainer;
|
||||
} else {
|
||||
node = container.childNodes[offset] || container;
|
||||
if (dom.isText(node)) {
|
||||
return textRangeInfo(node, 0);
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
isCollapseToStart = false;
|
||||
}
|
||||
|
||||
return {
|
||||
node: node,
|
||||
collapseToStart: isCollapseToStart,
|
||||
offset: offset
|
||||
};
|
||||
};
|
||||
|
||||
var textRange = document.body.createTextRange();
|
||||
var info = textRangeInfo(point.node, point.offset);
|
||||
|
||||
textRange.moveToElementText(info.node);
|
||||
textRange.collapse(info.collapseToStart);
|
||||
textRange.moveStart('character', info.offset);
|
||||
return textRange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapped Range
|
||||
*
|
||||
* @constructor
|
||||
* @param {Node} sc - start container
|
||||
* @param {Number} so - start offset
|
||||
* @param {Node} ec - end container
|
||||
* @param {Number} eo - end offset
|
||||
*/
|
||||
var WrappedRange = function (sc, so, ec, eo) {
|
||||
this.sc = sc;
|
||||
this.so = so;
|
||||
this.ec = ec;
|
||||
this.eo = eo;
|
||||
|
||||
// nativeRange: get nativeRange from sc, so, ec, eo
|
||||
var nativeRange = function () {
|
||||
if (agent.isW3CRangeSupport) {
|
||||
var w3cRange = document.createRange();
|
||||
w3cRange.setStart(sc, so);
|
||||
w3cRange.setEnd(ec, eo);
|
||||
|
||||
return w3cRange;
|
||||
} else {
|
||||
var textRange = pointToTextRange({
|
||||
node: sc,
|
||||
offset: so
|
||||
});
|
||||
|
||||
textRange.setEndPoint('EndToEnd', pointToTextRange({
|
||||
node: ec,
|
||||
offset: eo
|
||||
}));
|
||||
|
||||
return textRange;
|
||||
}
|
||||
};
|
||||
|
||||
this.getPoints = function () {
|
||||
return {
|
||||
sc: sc,
|
||||
so: so,
|
||||
ec: ec,
|
||||
eo: eo
|
||||
};
|
||||
};
|
||||
|
||||
this.getStartPoint = function () {
|
||||
return {
|
||||
node: sc,
|
||||
offset: so
|
||||
};
|
||||
};
|
||||
|
||||
this.getEndPoint = function () {
|
||||
return {
|
||||
node: ec,
|
||||
offset: eo
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* select update visible range
|
||||
*/
|
||||
this.select = function () {
|
||||
var nativeRng = nativeRange();
|
||||
if (agent.isW3CRangeSupport) {
|
||||
var selection = document.getSelection();
|
||||
if (selection.rangeCount > 0) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
selection.addRange(nativeRng);
|
||||
} else {
|
||||
nativeRng.select();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
this.normalize = function () {
|
||||
|
||||
/**
|
||||
* @param {BoundaryPoint} point
|
||||
* @param {Boolean} isLeftToRight
|
||||
* @return {BoundaryPoint}
|
||||
*/
|
||||
var getVisiblePoint = function (point, isLeftToRight) {
|
||||
if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
|
||||
(dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
|
||||
(dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
|
||||
(dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
|
||||
return point;
|
||||
}
|
||||
|
||||
// point on block's edge
|
||||
var block = dom.ancestor(point.node, dom.isBlock);
|
||||
if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
|
||||
((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
|
||||
|
||||
// returns point already on visible point
|
||||
if (dom.isVisiblePoint(point)) {
|
||||
return point;
|
||||
}
|
||||
// reverse direction
|
||||
isLeftToRight = !isLeftToRight;
|
||||
}
|
||||
|
||||
var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
|
||||
dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
|
||||
return nextPoint || point;
|
||||
};
|
||||
|
||||
var endPoint = getVisiblePoint(this.getEndPoint(), false);
|
||||
var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
|
||||
|
||||
return new WrappedRange(
|
||||
startPoint.node,
|
||||
startPoint.offset,
|
||||
endPoint.node,
|
||||
endPoint.offset
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns matched nodes on range
|
||||
*
|
||||
* @param {Function} [pred] - predicate function
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [options.includeAncestor]
|
||||
* @param {Boolean} [options.fullyContains]
|
||||
* @return {Node[]}
|
||||
*/
|
||||
this.nodes = function (pred, options) {
|
||||
pred = pred || func.ok;
|
||||
|
||||
var includeAncestor = options && options.includeAncestor;
|
||||
var fullyContains = options && options.fullyContains;
|
||||
|
||||
// TODO compare points and sort
|
||||
var startPoint = this.getStartPoint();
|
||||
var endPoint = this.getEndPoint();
|
||||
|
||||
var nodes = [];
|
||||
var leftEdgeNodes = [];
|
||||
|
||||
dom.walkPoint(startPoint, endPoint, function (point) {
|
||||
if (dom.isEditable(point.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var node;
|
||||
if (fullyContains) {
|
||||
if (dom.isLeftEdgePoint(point)) {
|
||||
leftEdgeNodes.push(point.node);
|
||||
}
|
||||
if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
|
||||
node = point.node;
|
||||
}
|
||||
} else if (includeAncestor) {
|
||||
node = dom.ancestor(point.node, pred);
|
||||
} else {
|
||||
node = point.node;
|
||||
}
|
||||
|
||||
if (node && pred(node)) {
|
||||
nodes.push(node);
|
||||
}
|
||||
}, true);
|
||||
|
||||
return list.unique(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns commonAncestor of range
|
||||
* @return {Element} - commonAncestor
|
||||
*/
|
||||
this.commonAncestor = function () {
|
||||
return dom.commonAncestor(sc, ec);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns expanded range by pred
|
||||
*
|
||||
* @param {Function} pred - predicate function
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
this.expand = function (pred) {
|
||||
var startAncestor = dom.ancestor(sc, pred);
|
||||
var endAncestor = dom.ancestor(ec, pred);
|
||||
|
||||
if (!startAncestor && !endAncestor) {
|
||||
return new WrappedRange(sc, so, ec, eo);
|
||||
}
|
||||
|
||||
var boundaryPoints = this.getPoints();
|
||||
|
||||
if (startAncestor) {
|
||||
boundaryPoints.sc = startAncestor;
|
||||
boundaryPoints.so = 0;
|
||||
}
|
||||
|
||||
if (endAncestor) {
|
||||
boundaryPoints.ec = endAncestor;
|
||||
boundaryPoints.eo = dom.nodeLength(endAncestor);
|
||||
}
|
||||
|
||||
return new WrappedRange(
|
||||
boundaryPoints.sc,
|
||||
boundaryPoints.so,
|
||||
boundaryPoints.ec,
|
||||
boundaryPoints.eo
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Boolean} isCollapseToStart
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
this.collapse = function (isCollapseToStart) {
|
||||
if (isCollapseToStart) {
|
||||
return new WrappedRange(sc, so, sc, so);
|
||||
} else {
|
||||
return new WrappedRange(ec, eo, ec, eo);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* splitText on range
|
||||
*/
|
||||
this.splitText = function () {
|
||||
var isSameContainer = sc === ec;
|
||||
var boundaryPoints = this.getPoints();
|
||||
|
||||
if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
|
||||
ec.splitText(eo);
|
||||
}
|
||||
|
||||
if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
|
||||
boundaryPoints.sc = sc.splitText(so);
|
||||
boundaryPoints.so = 0;
|
||||
|
||||
if (isSameContainer) {
|
||||
boundaryPoints.ec = boundaryPoints.sc;
|
||||
boundaryPoints.eo = eo - so;
|
||||
}
|
||||
}
|
||||
|
||||
return new WrappedRange(
|
||||
boundaryPoints.sc,
|
||||
boundaryPoints.so,
|
||||
boundaryPoints.ec,
|
||||
boundaryPoints.eo
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* delete contents on range
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
if(_.isUndefined(this.deleteContents)) // ODOO: ability to override by prototype
|
||||
this.deleteContents = function () {
|
||||
if (this.isCollapsed()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var rng = this.splitText();
|
||||
var nodes = rng.nodes(null, {
|
||||
fullyContains: true
|
||||
});
|
||||
|
||||
// find new cursor point
|
||||
var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
|
||||
return !list.contains(nodes, point.node);
|
||||
});
|
||||
|
||||
var emptyParents = [];
|
||||
$.each(nodes, function (idx, node) {
|
||||
// find empty parents
|
||||
var parent = node.parentNode;
|
||||
if (point.node !== parent && dom.nodeLength(parent) === 1) {
|
||||
emptyParents.push(parent);
|
||||
}
|
||||
dom.remove(node, false);
|
||||
});
|
||||
|
||||
// remove empty parents
|
||||
$.each(emptyParents, function (idx, node) {
|
||||
dom.remove(node, false);
|
||||
});
|
||||
|
||||
return new WrappedRange(
|
||||
point.node,
|
||||
point.offset,
|
||||
point.node,
|
||||
point.offset
|
||||
).normalize();
|
||||
};
|
||||
|
||||
/**
|
||||
* makeIsOn: return isOn(pred) function
|
||||
*/
|
||||
var makeIsOn = function (pred) {
|
||||
return function () {
|
||||
var ancestor = dom.ancestor(sc, pred);
|
||||
return !!ancestor && (ancestor === dom.ancestor(ec, pred));
|
||||
};
|
||||
};
|
||||
|
||||
// isOnEditable: judge whether range is on editable or not
|
||||
this.isOnEditable = makeIsOn(dom.isEditable);
|
||||
// isOnList: judge whether range is on list node or not
|
||||
this.isOnList = makeIsOn(dom.isList);
|
||||
// isOnAnchor: judge whether range is on anchor node or not
|
||||
this.isOnAnchor = makeIsOn(dom.isAnchor);
|
||||
// isOnAnchor: judge whether range is on cell node or not
|
||||
this.isOnCell = makeIsOn(dom.isCell);
|
||||
|
||||
/**
|
||||
* @param {Function} pred
|
||||
* @return {Boolean}
|
||||
*/
|
||||
this.isLeftEdgeOf = function (pred) {
|
||||
if (!dom.isLeftEdgePoint(this.getStartPoint())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var node = dom.ancestor(this.sc, pred);
|
||||
return node && dom.isLeftEdgeOf(this.sc, node);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns whether range was collapsed or not
|
||||
*/
|
||||
this.isCollapsed = function () {
|
||||
return sc === ec && so === eo;
|
||||
};
|
||||
|
||||
/**
|
||||
* wrap inline nodes which children of body with paragraph
|
||||
*
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
this.wrapBodyInlineWithPara = function () {
|
||||
if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
|
||||
sc.innerHTML = dom.emptyPara;
|
||||
return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* [workaround] firefox often create range on not visible point. so normalize here.
|
||||
* - firefox: |<p>text</p>|
|
||||
* - chrome: <p>|text|</p>
|
||||
*/
|
||||
var rng = this.normalize();
|
||||
if (dom.isParaInline(sc) || dom.isPara(sc)) {
|
||||
return rng;
|
||||
}
|
||||
|
||||
// ODOO: insert a p tag when try to insert a br with insertNode method, if the editor is inside a p, li, h1... (start_modification
|
||||
// if apply the editor to a P, LI... or inside a P, LI...
|
||||
if (dom.isText(sc)) {
|
||||
var node = sc;
|
||||
while (node.parentNode !== document) {
|
||||
node = node.parentNode;
|
||||
if (/^(P|LI|H[1-7]|BUTTON|A|SPAN)/.test(node.nodeName.toUpperCase())) {
|
||||
return this.normalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
// ODOO: end_modification)
|
||||
|
||||
// find inline top ancestor
|
||||
var topAncestor;
|
||||
if (dom.isInline(rng.sc)) {
|
||||
var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
|
||||
topAncestor = list.last(ancestors);
|
||||
if (!dom.isInline(topAncestor)) {
|
||||
topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
|
||||
}
|
||||
} else {
|
||||
topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
|
||||
}
|
||||
|
||||
// siblings not in paragraph
|
||||
var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
|
||||
inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
|
||||
|
||||
// wrap with paragraph
|
||||
if (inlineSiblings.length) {
|
||||
var para = dom.wrap(list.head(inlineSiblings), 'p');
|
||||
dom.appendChildNodes(para, list.tail(inlineSiblings));
|
||||
}
|
||||
|
||||
return this.normalize();
|
||||
};
|
||||
|
||||
/**
|
||||
* insert node at current cursor
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Node}
|
||||
*/
|
||||
this.insertNode = function (node) {
|
||||
var rng = this.wrapBodyInlineWithPara().deleteContents();
|
||||
// ODOO: override to not split world for inserting inline
|
||||
// original: var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
|
||||
var info = dom.splitPoint(rng.getStartPoint(), !dom.isBodyContainer(dom.ancestor(rng.sc, function(node) { return dom.isBodyContainer(node) || dom.isPara(node) })));
|
||||
|
||||
if (info.rightNode) {
|
||||
info.rightNode.parentNode.insertBefore(node, info.rightNode);
|
||||
} else {
|
||||
info.container.appendChild(node);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* insert html at current cursor
|
||||
*/
|
||||
this.pasteHTML = function (markup) {
|
||||
var contentsContainer = $('<div></div>').html(markup)[0];
|
||||
var childNodes = list.from(contentsContainer.childNodes);
|
||||
|
||||
var rng = this.wrapBodyInlineWithPara().deleteContents();
|
||||
|
||||
return childNodes.reverse().map(function (childNode) {
|
||||
return rng.insertNode(childNode);
|
||||
}).reverse();
|
||||
};
|
||||
|
||||
/**
|
||||
* returns text in range
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
this.toString = function () {
|
||||
var nativeRng = nativeRange();
|
||||
return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns range for word before cursor
|
||||
*
|
||||
* @param {Boolean} [findAfter] - find after cursor, default: false
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
this.getWordRange = function (findAfter) {
|
||||
var endPoint = this.getEndPoint();
|
||||
|
||||
if (!dom.isCharPoint(endPoint)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var startPoint = dom.prevPointUntil(endPoint, function (point) {
|
||||
return !dom.isCharPoint(point);
|
||||
});
|
||||
|
||||
if (findAfter) {
|
||||
endPoint = dom.nextPointUntil(endPoint, function (point) {
|
||||
return !dom.isCharPoint(point);
|
||||
});
|
||||
}
|
||||
|
||||
return new WrappedRange(
|
||||
startPoint.node,
|
||||
startPoint.offset,
|
||||
endPoint.node,
|
||||
endPoint.offset
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* create offsetPath bookmark
|
||||
*
|
||||
* @param {Node} editable
|
||||
*/
|
||||
this.bookmark = function (editable) {
|
||||
return {
|
||||
s: {
|
||||
path: dom.makeOffsetPath(editable, sc),
|
||||
offset: so
|
||||
},
|
||||
e: {
|
||||
path: dom.makeOffsetPath(editable, ec),
|
||||
offset: eo
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* create offsetPath bookmark base on paragraph
|
||||
*
|
||||
* @param {Node[]} paras
|
||||
*/
|
||||
this.paraBookmark = function (paras) {
|
||||
return {
|
||||
s: {
|
||||
path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
|
||||
offset: so
|
||||
},
|
||||
e: {
|
||||
path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
|
||||
offset: eo
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* getClientRects
|
||||
* @return {Rect[]}
|
||||
*/
|
||||
this.getClientRects = function () {
|
||||
var nativeRng = nativeRange();
|
||||
return nativeRng.getClientRects();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @class core.range
|
||||
*
|
||||
* Data structure
|
||||
* * BoundaryPoint: a point of dom tree
|
||||
* * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
|
||||
*
|
||||
* See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
|
||||
*
|
||||
* @singleton
|
||||
* @alternateClassName range
|
||||
*/
|
||||
return {
|
||||
WrappedRange: WrappedRange, // ODOO: give access to WrappedRange
|
||||
/**
|
||||
* @method
|
||||
*
|
||||
* create Range Object From arguments or Browser Selection
|
||||
*
|
||||
* @param {Node} sc - start container
|
||||
* @param {Number} so - start offset
|
||||
* @param {Node} ec - end container
|
||||
* @param {Number} eo - end offset
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
create : function (sc, so, ec, eo) {
|
||||
if (!arguments.length) { // from Browser Selection
|
||||
if (agent.isW3CRangeSupport) {
|
||||
var selection = document.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return null;
|
||||
} else if (dom.isBody(selection.anchorNode)) {
|
||||
// Firefox: returns entire body as range on initialization. We won't never need it.
|
||||
return null;
|
||||
}
|
||||
|
||||
var nativeRng = selection.getRangeAt(0);
|
||||
sc = nativeRng.startContainer;
|
||||
so = nativeRng.startOffset;
|
||||
ec = nativeRng.endContainer;
|
||||
eo = nativeRng.endOffset;
|
||||
} else { // IE8: TextRange
|
||||
var textRange = document.selection.createRange();
|
||||
var textRangeEnd = textRange.duplicate();
|
||||
textRangeEnd.collapse(false);
|
||||
var textRangeStart = textRange;
|
||||
textRangeStart.collapse(true);
|
||||
|
||||
var startPoint = textRangeToPoint(textRangeStart, true),
|
||||
endPoint = textRangeToPoint(textRangeEnd, false);
|
||||
|
||||
// same visible point case: range was collapsed.
|
||||
if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
|
||||
dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
|
||||
endPoint.node.nextSibling === startPoint.node) {
|
||||
startPoint = endPoint;
|
||||
}
|
||||
|
||||
sc = startPoint.cont;
|
||||
so = startPoint.offset;
|
||||
ec = endPoint.cont;
|
||||
eo = endPoint.offset;
|
||||
}
|
||||
} else if (arguments.length === 2) { //collapsed
|
||||
ec = sc;
|
||||
eo = so;
|
||||
}
|
||||
return new WrappedRange(sc, so, ec, eo);
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*
|
||||
* create WrappedRange from node
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
createFromNode: function (node) {
|
||||
var sc = node;
|
||||
var so = 0;
|
||||
var ec = node;
|
||||
var eo = dom.nodeLength(ec);
|
||||
|
||||
// browsers can't target a picture or void node
|
||||
if (dom.isVoid(sc)) {
|
||||
so = dom.listPrev(sc).length - 1;
|
||||
sc = sc.parentNode;
|
||||
}
|
||||
if (dom.isBR(ec)) {
|
||||
eo = dom.listPrev(ec).length - 1;
|
||||
ec = ec.parentNode;
|
||||
} else if (dom.isVoid(ec)) {
|
||||
eo = dom.listPrev(ec).length;
|
||||
ec = ec.parentNode;
|
||||
}
|
||||
|
||||
return this.create(sc, so, ec, eo);
|
||||
},
|
||||
|
||||
/**
|
||||
* create WrappedRange from node after position
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
createFromNodeBefore: function (node) {
|
||||
return this.createFromNode(node).collapse(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* create WrappedRange from node after position
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
createFromNodeAfter: function (node) {
|
||||
return this.createFromNode(node).collapse();
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*
|
||||
* create WrappedRange from bookmark
|
||||
*
|
||||
* @param {Node} editable
|
||||
* @param {Object} bookmark
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
createFromBookmark : function (editable, bookmark) {
|
||||
var sc = dom.fromOffsetPath(editable, bookmark.s.path);
|
||||
var so = bookmark.s.offset;
|
||||
var ec = dom.fromOffsetPath(editable, bookmark.e.path);
|
||||
var eo = bookmark.e.offset;
|
||||
return new WrappedRange(sc, so, ec, eo);
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*
|
||||
* create WrappedRange from paraBookmark
|
||||
*
|
||||
* @param {Object} bookmark
|
||||
* @param {Node[]} paras
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
createFromParaBookmark: function (bookmark, paras) {
|
||||
var so = bookmark.s.offset;
|
||||
var eo = bookmark.e.offset;
|
||||
var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
|
||||
var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
|
||||
|
||||
return new WrappedRange(sc, so, ec, eo);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
return range;
|
||||
});
|
||||
@@ -1,416 +0,0 @@
|
||||
define('summernote/defaults', function () {
|
||||
/**
|
||||
* @class defaults
|
||||
*
|
||||
* @singleton
|
||||
*/
|
||||
var defaults = {
|
||||
/** @property */
|
||||
version: '@VERSION',
|
||||
|
||||
/**
|
||||
*
|
||||
* for event options, reference to EventHandler.attach
|
||||
*
|
||||
* @property {Object} options
|
||||
* @property {String/Number} [options.width=null] set editor width
|
||||
* @property {String/Number} [options.height=null] set editor height, ex) 300
|
||||
* @property {String/Number} options.minHeight set minimum height of editor
|
||||
* @property {String/Number} options.maxHeight
|
||||
* @property {String/Number} options.focus
|
||||
* @property {Number} options.tabsize
|
||||
* @property {Boolean} options.styleWithSpan
|
||||
* @property {Object} options.codemirror
|
||||
* @property {Object} [options.codemirror.mode='text/html']
|
||||
* @property {Object} [options.codemirror.htmlMode=true]
|
||||
* @property {Object} [options.codemirror.lineNumbers=true]
|
||||
* @property {String} [options.lang=en-US] language 'en-US', 'ko-KR', ...
|
||||
* @property {String} [options.direction=null] text direction, ex) 'rtl'
|
||||
* @property {Array} [options.toolbar]
|
||||
* @property {Boolean} [options.airMode=false]
|
||||
* @property {Array} [options.airPopover]
|
||||
* @property {Fucntion} [options.onInit] initialize
|
||||
* @property {Fucntion} [options.onsubmit]
|
||||
*/
|
||||
options: {
|
||||
width: null, // set editor width
|
||||
height: null, // set editor height, ex) 300
|
||||
|
||||
minHeight: null, // set minimum height of editor
|
||||
maxHeight: null, // set maximum height of editor
|
||||
|
||||
focus: false, // set focus to editable area after initializing summernote
|
||||
|
||||
tabsize: 4, // size of tab ex) 2 or 4
|
||||
styleWithSpan: true, // style with span (Chrome and FF only)
|
||||
|
||||
disableLinkTarget: false, // hide link Target Checkbox
|
||||
disableDragAndDrop: false, // disable drag and drop event
|
||||
disableResizeEditor: false, // disable resizing editor
|
||||
disableResizeImage: false, // disable resizing image
|
||||
|
||||
shortcuts: true, // enable keyboard shortcuts
|
||||
|
||||
textareaAutoSync: true, // enable textarea auto sync
|
||||
|
||||
placeholder: false, // enable placeholder text
|
||||
prettifyHtml: true, // enable prettifying html while toggling codeview
|
||||
|
||||
iconPrefix: 'fa fa-', // prefix for css icon classes
|
||||
|
||||
icons: {
|
||||
font: {
|
||||
bold: 'bold',
|
||||
italic: 'italic',
|
||||
underline: 'underline',
|
||||
clear: 'eraser',
|
||||
height: 'text-height',
|
||||
strikethrough: 'strikethrough',
|
||||
superscript: 'superscript',
|
||||
subscript: 'subscript'
|
||||
},
|
||||
image: {
|
||||
image: 'picture-o',
|
||||
floatLeft: 'align-left',
|
||||
floatRight: 'align-right',
|
||||
floatNone: 'align-justify',
|
||||
shapeRounded: 'square',
|
||||
shapeCircle: 'circle-o',
|
||||
shapeThumbnail: 'picture-o',
|
||||
shapeNone: 'times',
|
||||
remove: 'trash-o'
|
||||
},
|
||||
link: {
|
||||
link: 'link',
|
||||
unlink: 'unlink',
|
||||
edit: 'edit'
|
||||
},
|
||||
table: {
|
||||
table: 'table'
|
||||
},
|
||||
hr: {
|
||||
insert: 'minus'
|
||||
},
|
||||
style: {
|
||||
style: 'magic'
|
||||
},
|
||||
lists: {
|
||||
unordered: 'list-ul',
|
||||
ordered: 'list-ol'
|
||||
},
|
||||
options: {
|
||||
help: 'question',
|
||||
fullscreen: 'arrows-alt',
|
||||
codeview: 'code'
|
||||
},
|
||||
paragraph: {
|
||||
paragraph: 'align-left',
|
||||
outdent: 'outdent',
|
||||
indent: 'indent',
|
||||
left: 'align-left',
|
||||
center: 'align-center',
|
||||
right: 'align-right',
|
||||
justify: 'align-justify'
|
||||
},
|
||||
color: {
|
||||
recent: 'font'
|
||||
},
|
||||
history: {
|
||||
undo: 'undo',
|
||||
redo: 'repeat'
|
||||
},
|
||||
misc: {
|
||||
check: 'check'
|
||||
}
|
||||
},
|
||||
|
||||
dialogsInBody: false, // false will add dialogs into editor
|
||||
|
||||
codemirror: { // codemirror options
|
||||
mode: 'text/html',
|
||||
htmlMode: true,
|
||||
lineNumbers: true
|
||||
},
|
||||
|
||||
// language
|
||||
lang: 'en-US', // language 'en-US', 'ko-KR', ...
|
||||
direction: null, // text direction, ex) 'rtl'
|
||||
|
||||
// toolbar
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'italic', 'underline', 'clear']],
|
||||
// ['font', ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'clear']],
|
||||
['fontname', ['fontname']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['height', ['height']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture', 'hr']],
|
||||
['view', ['fullscreen', 'codeview']],
|
||||
['help', ['help']]
|
||||
],
|
||||
|
||||
plugin : { },
|
||||
|
||||
// air mode: inline editor
|
||||
airMode: false,
|
||||
// airPopover: [
|
||||
// ['style', ['style']],
|
||||
// ['font', ['bold', 'italic', 'underline', 'clear']],
|
||||
// ['fontname', ['fontname']],
|
||||
// ['color', ['color']],
|
||||
// ['para', ['ul', 'ol', 'paragraph']],
|
||||
// ['height', ['height']],
|
||||
// ['table', ['table']],
|
||||
// ['insert', ['link', 'picture']],
|
||||
// ['help', ['help']]
|
||||
// ],
|
||||
airPopover: [
|
||||
['color', ['color']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['para', ['ul', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture']]
|
||||
],
|
||||
|
||||
// style tag
|
||||
styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||||
|
||||
// default fontName
|
||||
defaultFontName: 'Helvetica Neue',
|
||||
|
||||
// fontName
|
||||
fontNames: [
|
||||
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New',
|
||||
'Helvetica Neue', 'Helvetica', 'Impact', 'Lucida Grande',
|
||||
'Tahoma', 'Times New Roman', 'Verdana'
|
||||
],
|
||||
fontNamesIgnoreCheck: [],
|
||||
|
||||
fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'],
|
||||
|
||||
// pallete colors(n x n)
|
||||
colors: [
|
||||
['#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']
|
||||
],
|
||||
|
||||
// lineHeight
|
||||
lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'],
|
||||
|
||||
// insertTable max size
|
||||
insertTableMaxSize: {
|
||||
col: 10,
|
||||
row: 10
|
||||
},
|
||||
|
||||
// image
|
||||
maximumImageFileSize: null, // size in bytes, null = no limit
|
||||
|
||||
// callbacks
|
||||
oninit: null, // initialize
|
||||
onfocus: null, // editable has focus
|
||||
onblur: null, // editable out of focus
|
||||
onenter: null, // enter key pressed
|
||||
onkeyup: null, // keyup
|
||||
onkeydown: null, // keydown
|
||||
onImageUpload: null, // imageUpload
|
||||
onImageUploadError: null, // imageUploadError
|
||||
onMediaDelete: null, // media delete
|
||||
onToolbarClick: null,
|
||||
onsubmit: null,
|
||||
|
||||
/**
|
||||
* manipulate link address when user create link
|
||||
* @param {String} sLinkUrl
|
||||
* @return {String}
|
||||
*/
|
||||
onCreateLink: function (sLinkUrl) {
|
||||
if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
|
||||
sLinkUrl = 'mailto:' + sLinkUrl;
|
||||
}
|
||||
|
||||
return sLinkUrl;
|
||||
},
|
||||
|
||||
keyMap: {
|
||||
pc: {
|
||||
'ENTER': 'insertParagraph',
|
||||
'CTRL+Z': 'undo',
|
||||
'CTRL+Y': 'redo',
|
||||
'TAB': 'tab',
|
||||
'SHIFT+TAB': 'untab',
|
||||
'CTRL+B': 'bold',
|
||||
'CTRL+I': 'italic',
|
||||
'CTRL+U': 'underline',
|
||||
'CTRL+SHIFT+S': 'strikethrough',
|
||||
'CTRL+BACKSLASH': 'removeFormat',
|
||||
'CTRL+SHIFT+L': 'justifyLeft',
|
||||
'CTRL+SHIFT+E': 'justifyCenter',
|
||||
'CTRL+SHIFT+R': 'justifyRight',
|
||||
'CTRL+SHIFT+J': 'justifyFull',
|
||||
'CTRL+SHIFT+NUM7': 'insertUnorderedList',
|
||||
'CTRL+SHIFT+NUM8': 'insertOrderedList',
|
||||
'CTRL+LEFTBRACKET': 'outdent',
|
||||
'CTRL+RIGHTBRACKET': 'indent',
|
||||
'CTRL+NUM0': 'formatPara',
|
||||
'CTRL+NUM1': 'formatH1',
|
||||
'CTRL+NUM2': 'formatH2',
|
||||
'CTRL+NUM3': 'formatH3',
|
||||
'CTRL+NUM4': 'formatH4',
|
||||
'CTRL+NUM5': 'formatH5',
|
||||
'CTRL+NUM6': 'formatH6',
|
||||
'CTRL+ENTER': 'insertHorizontalRule',
|
||||
'CTRL+K': 'showLinkDialog'
|
||||
},
|
||||
|
||||
mac: {
|
||||
'ENTER': 'insertParagraph',
|
||||
'CMD+Z': 'undo',
|
||||
'CMD+SHIFT+Z': 'redo',
|
||||
'TAB': 'tab',
|
||||
'SHIFT+TAB': 'untab',
|
||||
'CMD+B': 'bold',
|
||||
'CMD+I': 'italic',
|
||||
'CMD+U': 'underline',
|
||||
'CMD+SHIFT+S': 'strikethrough',
|
||||
'CMD+BACKSLASH': 'removeFormat',
|
||||
'CMD+SHIFT+L': 'justifyLeft',
|
||||
'CMD+SHIFT+E': 'justifyCenter',
|
||||
'CMD+SHIFT+R': 'justifyRight',
|
||||
'CMD+SHIFT+J': 'justifyFull',
|
||||
'CMD+SHIFT+NUM7': 'insertUnorderedList',
|
||||
'CMD+SHIFT+NUM8': 'insertOrderedList',
|
||||
'CMD+LEFTBRACKET': 'outdent',
|
||||
'CMD+RIGHTBRACKET': 'indent',
|
||||
'CMD+NUM0': 'formatPara',
|
||||
'CMD+NUM1': 'formatH1',
|
||||
'CMD+NUM2': 'formatH2',
|
||||
'CMD+NUM3': 'formatH3',
|
||||
'CMD+NUM4': 'formatH4',
|
||||
'CMD+NUM5': 'formatH5',
|
||||
'CMD+NUM6': 'formatH6',
|
||||
'CMD+ENTER': 'insertHorizontalRule',
|
||||
'CMD+K': 'showLinkDialog'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// default language: en-US
|
||||
lang: {
|
||||
'en-US': {
|
||||
font: {
|
||||
bold: 'Bold',
|
||||
italic: 'Italic',
|
||||
underline: 'Underline',
|
||||
clear: 'Remove Font Style',
|
||||
height: 'Line Height',
|
||||
name: 'Font Family',
|
||||
strikethrough: 'Strikethrough',
|
||||
subscript: 'Subscript',
|
||||
superscript: 'Superscript',
|
||||
size: 'Font Size'
|
||||
},
|
||||
image: {
|
||||
image: 'Picture',
|
||||
insert: 'Insert Image',
|
||||
resizeFull: 'Resize Full',
|
||||
resizeHalf: 'Resize Half',
|
||||
resizeQuarter: 'Resize Quarter',
|
||||
floatLeft: 'Float Left',
|
||||
floatRight: 'Float Right',
|
||||
floatNone: 'Float None',
|
||||
shapeRounded: 'Shape: Rounded',
|
||||
shapeCircle: 'Shape: Circle',
|
||||
shapeThumbnail: 'Shape: Thumbnail',
|
||||
shapeNone: 'Shape: None',
|
||||
dragImageHere: 'Drag image or text here',
|
||||
dropImage: 'Drop image or Text',
|
||||
selectFromFiles: 'Select from files',
|
||||
maximumFileSize: 'Maximum file size',
|
||||
maximumFileSizeError: 'Maximum file size exceeded.',
|
||||
url: 'Image URL',
|
||||
remove: 'Remove Image'
|
||||
},
|
||||
link: {
|
||||
link: 'Link',
|
||||
insert: 'Insert Link',
|
||||
unlink: 'Unlink',
|
||||
edit: 'Edit',
|
||||
textToDisplay: 'Text to display',
|
||||
url: 'To what URL should this link go?',
|
||||
openInNewWindow: 'Open in new window'
|
||||
},
|
||||
table: {
|
||||
table: 'Table'
|
||||
},
|
||||
hr: {
|
||||
insert: 'Insert Horizontal Rule'
|
||||
},
|
||||
style: {
|
||||
style: 'Style',
|
||||
normal: 'Paragraph',
|
||||
blockquote: 'Quote',
|
||||
pre: 'Code',
|
||||
h1: 'Header 1',
|
||||
h2: 'Header 2',
|
||||
h3: 'Header 3',
|
||||
h4: 'Header 4',
|
||||
h5: 'Header 5',
|
||||
h6: 'Header 6'
|
||||
},
|
||||
lists: {
|
||||
unordered: 'Unordered list',
|
||||
ordered: 'Ordered list'
|
||||
},
|
||||
options: {
|
||||
help: 'Help',
|
||||
fullscreen: 'Full Screen',
|
||||
codeview: 'Code View'
|
||||
},
|
||||
paragraph: {
|
||||
paragraph: 'Paragraph',
|
||||
outdent: 'Outdent',
|
||||
indent: 'Indent',
|
||||
left: 'Align left',
|
||||
center: 'Align center',
|
||||
right: 'Align right',
|
||||
justify: 'Justify full'
|
||||
},
|
||||
color: {
|
||||
recent: 'Recent Color',
|
||||
more: 'More Color',
|
||||
background: 'Background Color',
|
||||
foreground: 'Foreground Color',
|
||||
transparent: 'Transparent',
|
||||
setTransparent: 'Set transparent',
|
||||
reset: 'Reset',
|
||||
resetToDefault: 'Reset to default'
|
||||
},
|
||||
shortcut: {
|
||||
shortcuts: 'Keyboard shortcuts',
|
||||
close: 'Close',
|
||||
textFormatting: 'Text formatting',
|
||||
action: 'Action',
|
||||
paragraphFormatting: 'Paragraph formatting',
|
||||
documentStyle: 'Document Style',
|
||||
extraKeys: 'Extra keys'
|
||||
},
|
||||
history: {
|
||||
undo: 'Undo',
|
||||
redo: 'Redo'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return defaults;
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
define([
|
||||
'summernote/core/list',
|
||||
'summernote/core/func',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/range'
|
||||
], function (list, func, dom, range) {
|
||||
|
||||
/**
|
||||
* @class editing.Bullet
|
||||
*
|
||||
* @alternateClassName Bullet
|
||||
*/
|
||||
var Bullet = function () {
|
||||
/**
|
||||
* @method insertOrderedList
|
||||
*
|
||||
* toggle ordered list
|
||||
*
|
||||
* @type command
|
||||
*/
|
||||
this.insertOrderedList = function () {
|
||||
this.toggleList('OL');
|
||||
};
|
||||
|
||||
/**
|
||||
* @method insertUnorderedList
|
||||
*
|
||||
* toggle unordered list
|
||||
*
|
||||
* @type command
|
||||
*/
|
||||
this.insertUnorderedList = function () {
|
||||
this.toggleList('UL');
|
||||
};
|
||||
|
||||
/**
|
||||
* @method indent
|
||||
*
|
||||
* indent
|
||||
*
|
||||
* @type command
|
||||
*/
|
||||
this.indent = function () {
|
||||
var self = this;
|
||||
var rng = range.create().wrapBodyInlineWithPara();
|
||||
|
||||
var paras = rng.nodes(dom.isPara, { includeAncestor: true });
|
||||
var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
|
||||
|
||||
$.each(clustereds, function (idx, paras) {
|
||||
var head = list.head(paras);
|
||||
if (dom.isLi(head)) {
|
||||
self.wrapList(paras, head.parentNode.nodeName);
|
||||
} else {
|
||||
$.each(paras, function (idx, para) {
|
||||
$(para).css('marginLeft', function (idx, val) {
|
||||
return (parseInt(val, 10) || 0) + 25;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rng.select();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method outdent
|
||||
*
|
||||
* outdent
|
||||
*
|
||||
* @type command
|
||||
*/
|
||||
this.outdent = function () {
|
||||
var self = this;
|
||||
var rng = range.create().wrapBodyInlineWithPara();
|
||||
|
||||
var paras = rng.nodes(dom.isPara, { includeAncestor: true });
|
||||
var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
|
||||
|
||||
$.each(clustereds, function (idx, paras) {
|
||||
var head = list.head(paras);
|
||||
if (dom.isLi(head)) {
|
||||
self.releaseList([paras]);
|
||||
} else {
|
||||
$.each(paras, function (idx, para) {
|
||||
$(para).css('marginLeft', function (idx, val) {
|
||||
val = (parseInt(val, 10) || 0);
|
||||
return val > 25 ? val - 25 : '';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rng.select();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method toggleList
|
||||
*
|
||||
* toggle list
|
||||
*
|
||||
* @param {String} listName - OL or UL
|
||||
*/
|
||||
this.toggleList = function (listName) {
|
||||
var self = this;
|
||||
var rng = range.create().wrapBodyInlineWithPara();
|
||||
|
||||
var paras = rng.nodes(dom.isPara, { includeAncestor: true });
|
||||
var bookmark = rng.paraBookmark(paras);
|
||||
var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
|
||||
|
||||
// paragraph to list
|
||||
if (list.find(paras, dom.isPurePara)) {
|
||||
var wrappedParas = [];
|
||||
$.each(clustereds, function (idx, paras) {
|
||||
wrappedParas = wrappedParas.concat(self.wrapList(paras, listName));
|
||||
});
|
||||
paras = wrappedParas;
|
||||
// list to paragraph or change list style
|
||||
} else {
|
||||
var diffLists = rng.nodes(dom.isList, {
|
||||
includeAncestor: true
|
||||
}).filter(function (listNode) {
|
||||
return !$.nodeName(listNode, listName);
|
||||
});
|
||||
|
||||
if (diffLists.length) {
|
||||
$.each(diffLists, function (idx, listNode) {
|
||||
dom.replace(listNode, listName);
|
||||
});
|
||||
} else {
|
||||
paras = this.releaseList(clustereds, true);
|
||||
}
|
||||
}
|
||||
|
||||
range.createFromParaBookmark(bookmark, paras).select();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method wrapList
|
||||
*
|
||||
* @param {Node[]} paras
|
||||
* @param {String} listName
|
||||
* @return {Node[]}
|
||||
*/
|
||||
this.wrapList = function (paras, listName) {
|
||||
var head = list.head(paras);
|
||||
var last = list.last(paras);
|
||||
|
||||
var prevList = dom.isList(head.previousSibling) && head.previousSibling;
|
||||
var nextList = dom.isList(last.nextSibling) && last.nextSibling;
|
||||
|
||||
var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
|
||||
|
||||
// P to LI
|
||||
paras = paras.map(function (para) {
|
||||
return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
|
||||
});
|
||||
|
||||
// append to list(<ul>, <ol>)
|
||||
dom.appendChildNodes(listNode, paras);
|
||||
|
||||
if (nextList) {
|
||||
dom.appendChildNodes(listNode, list.from(nextList.childNodes));
|
||||
dom.remove(nextList);
|
||||
}
|
||||
|
||||
return paras;
|
||||
};
|
||||
|
||||
/**
|
||||
* @method releaseList
|
||||
*
|
||||
* @param {Array[]} clustereds
|
||||
* @param {Boolean} isEscapseToBody
|
||||
* @return {Node[]}
|
||||
*/
|
||||
this.releaseList = function (clustereds, isEscapseToBody) {
|
||||
var releasedParas = [];
|
||||
|
||||
$.each(clustereds, function (idx, paras) {
|
||||
var head = list.head(paras);
|
||||
var last = list.last(paras);
|
||||
|
||||
var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) :
|
||||
head.parentNode;
|
||||
var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, {
|
||||
node: last.parentNode,
|
||||
offset: dom.position(last) + 1
|
||||
}, {
|
||||
isSkipPaddingBlankHTML: true
|
||||
}) : null;
|
||||
|
||||
var middleList = dom.splitTree(headList, {
|
||||
node: head.parentNode,
|
||||
offset: dom.position(head)
|
||||
}, {
|
||||
isSkipPaddingBlankHTML: true
|
||||
});
|
||||
|
||||
paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) :
|
||||
list.from(middleList.childNodes).filter(dom.isLi);
|
||||
|
||||
// LI to P
|
||||
if (isEscapseToBody || !dom.isList(headList.parentNode)) {
|
||||
paras = paras.map(function (para) {
|
||||
return dom.replace(para, 'P');
|
||||
});
|
||||
}
|
||||
|
||||
$.each(list.from(paras).reverse(), function (idx, para) {
|
||||
dom.insertAfter(para, headList);
|
||||
});
|
||||
|
||||
// remove empty lists
|
||||
var rootLists = list.compact([headList, middleList, lastList]);
|
||||
$.each(rootLists, function (idx, rootList) {
|
||||
var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList));
|
||||
$.each(listNodes.reverse(), function (idx, listNode) {
|
||||
if (!dom.nodeLength(listNode)) {
|
||||
dom.remove(listNode, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
releasedParas = releasedParas.concat(paras);
|
||||
});
|
||||
|
||||
return releasedParas;
|
||||
};
|
||||
};
|
||||
|
||||
return Bullet;
|
||||
});
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
define(['summernote/core/range', 'summernote/core/dom'], function (range, dom) { // ODOO: suggest upstream
|
||||
/**
|
||||
* @class editing.History
|
||||
*
|
||||
* Editor History
|
||||
*
|
||||
*/
|
||||
var History = function ($editable) {
|
||||
var stack = [], stackOffset = -1;
|
||||
var editable = $editable[0];
|
||||
|
||||
var makeSnapshot = function () {
|
||||
var rng = range.create();
|
||||
var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
|
||||
|
||||
return {
|
||||
contents: $editable.html(),
|
||||
bookmark: (rng && dom.ancestor(rng.sc, dom.isEditable) ? rng.bookmark(editable) : emptyBookmark)
|
||||
// ODOO: suggest upstream added " && dom.ancestor(rng.sc, dom.isEditable) "
|
||||
};
|
||||
};
|
||||
|
||||
var applySnapshot = function (snapshot) {
|
||||
if (snapshot.contents !== null) {
|
||||
$editable.html(snapshot.contents);
|
||||
}
|
||||
if (snapshot.bookmark !== null) {
|
||||
range.createFromBookmark(editable, snapshot.bookmark).select();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* undo
|
||||
*/
|
||||
this.undo = function () {
|
||||
// Create snap shot if not yet recorded
|
||||
if ($editable.html() !== stack[stackOffset].contents) {
|
||||
this.recordUndo();
|
||||
}
|
||||
|
||||
if (0 < stackOffset) {
|
||||
stackOffset--;
|
||||
applySnapshot(stack[stackOffset]);
|
||||
}
|
||||
};
|
||||
|
||||
/* ODOO: to suggest upstream */
|
||||
this.hasUndo = function () {
|
||||
return 0 < stackOffset;
|
||||
};
|
||||
|
||||
/**
|
||||
* redo
|
||||
*/
|
||||
this.redo = function () {
|
||||
if (stack.length - 1 > stackOffset) {
|
||||
stackOffset++;
|
||||
applySnapshot(stack[stackOffset]);
|
||||
}
|
||||
};
|
||||
|
||||
/* ODOO: to suggest upstream */
|
||||
this.hasRedo = function () {
|
||||
return stack.length - 1 > stackOffset;
|
||||
};
|
||||
|
||||
var last; // ODOO: to suggest upstream (since we may have several editor)
|
||||
/**
|
||||
* recorded undo
|
||||
*/
|
||||
this.recordUndo = function () {
|
||||
// ODOO: method totally rewritten
|
||||
// test event for firefox: remove stack of history because event doesn't exists
|
||||
var key = typeof event !== 'undefined' ? event : false;
|
||||
if (key && !event.metaKey && !event.ctrlKey && !event.altKey && event.type === "keydown") {
|
||||
key = event.type + "-";
|
||||
if (event.which === 8 || event.which === 46) key += 'delete';
|
||||
else if (event.which === 13) key += 'enter';
|
||||
else key += 'other';
|
||||
if (key === last) return;
|
||||
hasUndo = true;
|
||||
}
|
||||
last = key;
|
||||
|
||||
// Wash out stack after stackOffset
|
||||
if (stack.length > stackOffset+1) {
|
||||
stack = stack.slice(0, stackOffset+1);
|
||||
}
|
||||
|
||||
if (stack[stackOffset] && stack[stackOffset].contents === $editable.html()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stackOffset++;
|
||||
|
||||
// Create new snapshot and push it to the end
|
||||
stack.push(makeSnapshot());
|
||||
};
|
||||
|
||||
/* ODOO: to suggest upstream */
|
||||
this.splitNext = function () {
|
||||
last = false;
|
||||
};
|
||||
|
||||
/* ODOO: to suggest upstream */
|
||||
this.reset = function () {
|
||||
last = false;
|
||||
stack = [];
|
||||
stackOffset = -1;
|
||||
this.recordUndo();
|
||||
};
|
||||
|
||||
// Create first undo stack
|
||||
this.recordUndo();
|
||||
};
|
||||
|
||||
return History;
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
define([
|
||||
'summernote/core/agent',
|
||||
'summernote/core/func',
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom'
|
||||
], function (agent, func, list, dom) {
|
||||
/**
|
||||
* @class editing.Style
|
||||
*
|
||||
* Style
|
||||
*
|
||||
*/
|
||||
var Style = function () {
|
||||
/**
|
||||
* @method jQueryCSS
|
||||
*
|
||||
* [workaround] for old jQuery
|
||||
* passing an array of style properties to .css()
|
||||
* will result in an object of property-value pairs.
|
||||
* (compability with version < 1.9)
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $obj
|
||||
* @param {Array} propertyNames - An array of one or more CSS properties.
|
||||
* @return {Object}
|
||||
*/
|
||||
var jQueryCSS = function ($obj, propertyNames) {
|
||||
if (agent.jqueryVersion < 1.9) {
|
||||
var result = {};
|
||||
$.each(propertyNames, function (idx, propertyName) {
|
||||
result[propertyName] = $obj.css(propertyName);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return $obj.css.call($obj, propertyNames);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns style object from node
|
||||
*
|
||||
* @param {jQuery} $node
|
||||
* @return {Object}
|
||||
*/
|
||||
this.fromNode = function ($node) {
|
||||
var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
|
||||
var styleInfo = jQueryCSS($node, properties) || {};
|
||||
styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
|
||||
return styleInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* paragraph level style
|
||||
*
|
||||
* @param {WrappedRange} rng
|
||||
* @param {Object} styleInfo
|
||||
*/
|
||||
this.stylePara = function (rng, styleInfo) {
|
||||
$.each(rng.nodes(dom.isPara, {
|
||||
includeAncestor: true
|
||||
}), function (idx, para) {
|
||||
$(para).css(styleInfo);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* insert and returns styleNodes on range.
|
||||
*
|
||||
* @param {WrappedRange} rng
|
||||
* @param {Object} [options] - options for styleNodes
|
||||
* @param {String} [options.nodeName] - default: `SPAN`
|
||||
* @param {Boolean} [options.expandClosestSibling] - default: `false`
|
||||
* @param {Boolean} [options.onlyPartialContains] - default: `false`
|
||||
* @return {Node[]}
|
||||
*/
|
||||
this.styleNodes = function (rng, options) {
|
||||
rng = rng.splitText();
|
||||
|
||||
var nodeName = options && options.nodeName || 'SPAN';
|
||||
var expandClosestSibling = !!(options && options.expandClosestSibling);
|
||||
var onlyPartialContains = !!(options && options.onlyPartialContains);
|
||||
|
||||
if (rng.isCollapsed()) {
|
||||
return [rng.insertNode(dom.create(nodeName))];
|
||||
}
|
||||
|
||||
var pred = dom.makePredByNodeName(nodeName);
|
||||
var nodes = rng.nodes(dom.isText, {
|
||||
fullyContains: true
|
||||
}).map(function (text) {
|
||||
return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName);
|
||||
});
|
||||
|
||||
if (expandClosestSibling) {
|
||||
if (onlyPartialContains) {
|
||||
var nodesInRange = rng.nodes();
|
||||
// compose with partial contains predication
|
||||
pred = func.and(pred, function (node) {
|
||||
return list.contains(nodesInRange, node);
|
||||
});
|
||||
}
|
||||
|
||||
return nodes.map(function (node) {
|
||||
var siblings = dom.withClosestSiblings(node, pred);
|
||||
var head = list.head(siblings);
|
||||
var tails = list.tail(siblings);
|
||||
$.each(tails, function (idx, elem) {
|
||||
dom.appendChildNodes(head, elem.childNodes);
|
||||
dom.remove(elem);
|
||||
});
|
||||
return list.head(siblings);
|
||||
});
|
||||
} else {
|
||||
return nodes;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* get current style on cursor
|
||||
*
|
||||
* @param {WrappedRange} rng
|
||||
* @return {Object} - object contains style properties.
|
||||
*/
|
||||
this.current = function (rng) {
|
||||
var $cont = $(dom.isText(rng.sc) ? rng.sc.parentNode : rng.sc);
|
||||
var styleInfo = this.fromNode($cont);
|
||||
|
||||
// document.queryCommandState for toggle state
|
||||
styleInfo['font-bold'] = document.queryCommandState('bold') ? 'bold' : 'normal';
|
||||
styleInfo['font-italic'] = document.queryCommandState('italic') ? 'italic' : 'normal';
|
||||
styleInfo['font-underline'] = document.queryCommandState('underline') ? 'underline' : 'normal';
|
||||
styleInfo['font-strikethrough'] = document.queryCommandState('strikeThrough') ? 'strikethrough' : 'normal';
|
||||
styleInfo['font-superscript'] = document.queryCommandState('superscript') ? 'superscript' : 'normal';
|
||||
styleInfo['font-subscript'] = document.queryCommandState('subscript') ? 'subscript' : 'normal';
|
||||
|
||||
// list-style-type to list-style(unordered, ordered)
|
||||
if (!rng.isOnList()) {
|
||||
styleInfo['list-style'] = 'none';
|
||||
} else {
|
||||
var aOrderedType = ['circle', 'disc', 'disc-leading-zero', 'square'];
|
||||
var isUnordered = $.inArray(styleInfo['list-style-type'], aOrderedType) > -1;
|
||||
styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
|
||||
}
|
||||
|
||||
var para = dom.ancestor(rng.sc, dom.isPara);
|
||||
if (para && para.style['line-height']) {
|
||||
styleInfo['line-height'] = para.style.lineHeight;
|
||||
} else {
|
||||
var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
|
||||
styleInfo['line-height'] = lineHeight.toFixed(1);
|
||||
}
|
||||
|
||||
styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
|
||||
styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
|
||||
styleInfo.range = rng;
|
||||
|
||||
return styleInfo;
|
||||
};
|
||||
};
|
||||
|
||||
return Style;
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
define([
|
||||
'summernote/core/dom', 'summernote/core/range', 'summernote/core/list'
|
||||
], function (dom, range, list) {
|
||||
/**
|
||||
* @class editing.Table
|
||||
*
|
||||
* Table
|
||||
*
|
||||
*/
|
||||
var Table = function () {
|
||||
/**
|
||||
* handle tab key
|
||||
*
|
||||
* @param {WrappedRange} rng
|
||||
* @param {Boolean} isShift
|
||||
*/
|
||||
this.tab = function (rng, isShift) {
|
||||
var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
|
||||
var table = dom.ancestor(cell, dom.isTable);
|
||||
var cells = dom.listDescendant(table, dom.isCell);
|
||||
|
||||
var nextCell = list[isShift ? 'prev' : 'next'](cells, cell);
|
||||
if (nextCell) {
|
||||
range.create(nextCell, 0).select();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* create empty table element
|
||||
*
|
||||
* @param {Number} rowCount
|
||||
* @param {Number} colCount
|
||||
* @return {Node}
|
||||
*/
|
||||
this.createTable = function (colCount, rowCount) {
|
||||
var tds = [], tdHTML;
|
||||
for (var idxCol = 0; idxCol < colCount; idxCol++) {
|
||||
tds.push('<td>' + dom.blank + '</td>');
|
||||
}
|
||||
tdHTML = tds.join('');
|
||||
|
||||
var trs = [], trHTML;
|
||||
for (var idxRow = 0; idxRow < rowCount; idxRow++) {
|
||||
trs.push('<tr>' + tdHTML + '</tr>');
|
||||
}
|
||||
trHTML = trs.join('');
|
||||
return $('<table class="table table-bordered">' + trHTML + '</table>')[0];
|
||||
};
|
||||
};
|
||||
return Table;
|
||||
});
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
define([
|
||||
'summernote/core/dom',
|
||||
'summernote/core/range',
|
||||
'summernote/editing/Bullet'
|
||||
], function (dom, range, Bullet) {
|
||||
|
||||
/**
|
||||
* @class editing.Typing
|
||||
*
|
||||
* Typing
|
||||
*
|
||||
*/
|
||||
var Typing = function () {
|
||||
|
||||
// a Bullet instance to toggle lists off
|
||||
var bullet = new Bullet();
|
||||
|
||||
/**
|
||||
* insert tab
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {WrappedRange} rng
|
||||
* @param {Number} tabsize
|
||||
*/
|
||||
this.insertTab = function ($editable, rng, tabsize) {
|
||||
var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR));
|
||||
rng = rng.deleteContents();
|
||||
rng.insertNode(tab, true);
|
||||
|
||||
rng = range.create(tab, tabsize);
|
||||
rng.select();
|
||||
};
|
||||
|
||||
/**
|
||||
* insert paragraph
|
||||
*/
|
||||
this.insertParagraph = function () {
|
||||
var rng = range.create();
|
||||
|
||||
// deleteContents on range.
|
||||
rng = rng.deleteContents();
|
||||
|
||||
// Wrap range if it needs to be wrapped by paragraph
|
||||
rng = rng.wrapBodyInlineWithPara();
|
||||
|
||||
// finding paragraph
|
||||
var splitRoot = dom.ancestor(rng.sc, dom.isPara);
|
||||
|
||||
var nextPara;
|
||||
// on paragraph: split paragraph
|
||||
if (splitRoot) {
|
||||
// if it is an empty line with li
|
||||
if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) {
|
||||
// disable UL/OL and escape!
|
||||
bullet.toggleList(splitRoot.parentNode.nodeName);
|
||||
return;
|
||||
// if new line has content (not a line break)
|
||||
} else {
|
||||
nextPara = dom.splitTree(splitRoot, rng.getStartPoint());
|
||||
|
||||
var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor);
|
||||
emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor));
|
||||
|
||||
$.each(emptyAnchors, function (idx, anchor) {
|
||||
dom.remove(anchor);
|
||||
});
|
||||
}
|
||||
// no paragraph: insert empty paragraph
|
||||
} else {
|
||||
var next = rng.sc.childNodes[rng.so];
|
||||
nextPara = $(dom.emptyPara)[0];
|
||||
if (next) {
|
||||
rng.sc.insertBefore(nextPara, next);
|
||||
} else {
|
||||
rng.sc.appendChild(nextPara);
|
||||
}
|
||||
}
|
||||
|
||||
range.create(nextPara, 0).normalize().select();
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return Typing;
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Super simple wysiwyg editor on Bootstrap v@VERSION
|
||||
* http://summernote.org/
|
||||
*
|
||||
* summernote.js
|
||||
* Copyright 2013-2015 Alan Hong. and other contributors
|
||||
* summernote may be freely distributed under the MIT license./
|
||||
*
|
||||
* Date: @DATE
|
||||
*/
|
||||
(function (factory) {
|
||||
/* global define */
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals: jQuery
|
||||
factory(window.jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
'use strict';
|
||||
@@ -1,169 +0,0 @@
|
||||
define([
|
||||
'summernote/core/list',
|
||||
'summernote/core/agent'
|
||||
], function (list, agent) {
|
||||
/**
|
||||
* @class module.Button
|
||||
*
|
||||
* Button
|
||||
*/
|
||||
var Button = function () {
|
||||
/**
|
||||
* update button status
|
||||
*
|
||||
* @param {jQuery} $container
|
||||
* @param {Object} styleInfo
|
||||
*/
|
||||
this.update = function ($container, styleInfo) {
|
||||
/**
|
||||
* handle dropdown's check mark (for fontname, fontsize, lineHeight).
|
||||
* @param {jQuery} $btn
|
||||
* @param {Number} value
|
||||
*/
|
||||
var checkDropdownMenu = function ($btn, value) {
|
||||
$btn.find('.dropdown-menu .dropdown-item').each(function () {
|
||||
// always compare string to avoid creating another func.
|
||||
var isChecked = ($(this).data('value') + '') === (value + '');
|
||||
this.className = 'dropdown-item' + (isChecked ? ' checked' : '');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* update button state(active or not).
|
||||
*
|
||||
* @private
|
||||
* @param {String} selector
|
||||
* @param {Function} pred
|
||||
*/
|
||||
var btnState = function (selector, pred) {
|
||||
var $btn = $container.find(selector);
|
||||
$btn.toggleClass('active', pred());
|
||||
};
|
||||
|
||||
if (styleInfo.image) {
|
||||
var $img = $(styleInfo.image);
|
||||
|
||||
btnState('button[data-event="imageShape"][data-value="rounded"]', function () {
|
||||
return $img.hasClass('rounded');
|
||||
});
|
||||
btnState('button[data-event="imageShape"][data-value="rounded-circle"]', function () {
|
||||
return $img.hasClass('rounded-circle');
|
||||
});
|
||||
btnState('button[data-event="imageShape"][data-value="img-thumbnail"]', function () {
|
||||
return $img.hasClass('img-thumbnail');
|
||||
});
|
||||
btnState('button[data-event="imageShape"]:not([data-value])', function () {
|
||||
return !$img.is('.rounded, .rounded-circle, .img-thumbnail');
|
||||
});
|
||||
|
||||
var imgFloat = $img.css('float');
|
||||
btnState('button[data-event="floatMe"][data-value="left"]', function () {
|
||||
return imgFloat === 'left';
|
||||
});
|
||||
btnState('button[data-event="floatMe"][data-value="right"]', function () {
|
||||
return imgFloat === 'right';
|
||||
});
|
||||
btnState('button[data-event="floatMe"][data-value="none"]', function () {
|
||||
return imgFloat !== 'left' && imgFloat !== 'right';
|
||||
});
|
||||
|
||||
var style = $img.attr('style');
|
||||
btnState('button[data-event="resize"][data-value="1"]', function () {
|
||||
return !!/(^|\s)(max-)?width\s*:\s*100%/.test(style);
|
||||
});
|
||||
btnState('button[data-event="resize"][data-value="0.5"]', function () {
|
||||
return !!/(^|\s)(max-)?width\s*:\s*50%/.test(style);
|
||||
});
|
||||
btnState('button[data-event="resize"][data-value="0.25"]', function () {
|
||||
return !!/(^|\s)(max-)?width\s*:\s*25%/.test(style);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// fontname
|
||||
var $fontname = $container.find('.note-fontname');
|
||||
if ($fontname.length) {
|
||||
var selectedFont = styleInfo['font-family'];
|
||||
if (!!selectedFont) {
|
||||
|
||||
var list = selectedFont.split(',');
|
||||
for (var i = 0, len = list.length; i < len; i++) {
|
||||
selectedFont = list[i].replace(/[\'\"]/g, '').replace(/\s+$/, '').replace(/^\s+/, '');
|
||||
if (agent.isFontInstalled(selectedFont)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$fontname.find('.note-current-fontname').text(selectedFont);
|
||||
checkDropdownMenu($fontname, selectedFont);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// fontsize
|
||||
var $fontsize = $container.find('.note-fontsize');
|
||||
$fontsize.find('.note-current-fontsize').text(styleInfo['font-size']);
|
||||
checkDropdownMenu($fontsize, parseFloat(styleInfo['font-size']));
|
||||
|
||||
// lineheight
|
||||
var $lineHeight = $container.find('.note-height');
|
||||
checkDropdownMenu($lineHeight, parseFloat(styleInfo['line-height']));
|
||||
|
||||
btnState('button[data-event="bold"]', function () {
|
||||
return styleInfo['font-bold'] === 'bold';
|
||||
});
|
||||
btnState('button[data-event="italic"]', function () {
|
||||
return styleInfo['font-italic'] === 'italic';
|
||||
});
|
||||
btnState('button[data-event="underline"]', function () {
|
||||
return styleInfo['font-underline'] === 'underline';
|
||||
});
|
||||
btnState('button[data-event="strikethrough"]', function () {
|
||||
return styleInfo['font-strikethrough'] === 'strikethrough';
|
||||
});
|
||||
btnState('button[data-event="superscript"]', function () {
|
||||
return styleInfo['font-superscript'] === 'superscript';
|
||||
});
|
||||
btnState('button[data-event="subscript"]', function () {
|
||||
return styleInfo['font-subscript'] === 'subscript';
|
||||
});
|
||||
btnState('button[data-event="justifyLeft"]', function () {
|
||||
return styleInfo['text-align'] === 'left' || styleInfo['text-align'] === 'start';
|
||||
});
|
||||
btnState('button[data-event="justifyCenter"]', function () {
|
||||
return styleInfo['text-align'] === 'center';
|
||||
});
|
||||
btnState('button[data-event="justifyRight"]', function () {
|
||||
return styleInfo['text-align'] === 'right';
|
||||
});
|
||||
btnState('button[data-event="justifyFull"]', function () {
|
||||
return styleInfo['text-align'] === 'justify';
|
||||
});
|
||||
btnState('button[data-event="insertUnorderedList"]', function () {
|
||||
return styleInfo['list-style'] === 'unordered';
|
||||
});
|
||||
btnState('button[data-event="insertOrderedList"]', function () {
|
||||
return styleInfo['list-style'] === 'ordered';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* update recent color
|
||||
*
|
||||
* @param {Node} button
|
||||
* @param {String} eventName
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
this.updateRecentColor = function (button, eventName, value) {
|
||||
var $color = $(button).closest('.note-color');
|
||||
var $recentColor = $color.find('.note-recent-color');
|
||||
var colorInfo = JSON.parse($recentColor.attr('data-value'));
|
||||
colorInfo[eventName] = value;
|
||||
$recentColor.attr('data-value', JSON.stringify(colorInfo));
|
||||
var sKey = eventName === 'backColor' ? 'background-color' : 'color';
|
||||
$recentColor.find('i').css(sKey, value);
|
||||
};
|
||||
};
|
||||
|
||||
return Button;
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
define([
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/key',
|
||||
'summernote/core/agent',
|
||||
'summernote/core/range'
|
||||
], function (list, dom, key, agent, range) {
|
||||
// ODOO override: use 0.8.10 version of this, adapted for the old summernote
|
||||
// version odoo is using
|
||||
var Clipboard = function (handler) {
|
||||
/**
|
||||
* paste by clipboard event
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
var pasteByEvent = function (event) {
|
||||
if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) {
|
||||
// ODOO override: from old summernote version
|
||||
return;
|
||||
}
|
||||
|
||||
var clipboardData = event.originalEvent.clipboardData;
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
var $editable = layoutInfo.editable();
|
||||
|
||||
if (clipboardData && clipboardData.items && clipboardData.items.length) {
|
||||
var item = list.head(clipboardData.items);
|
||||
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
|
||||
handler.insertImages(layoutInfo, [item.getAsFile()]);
|
||||
}
|
||||
handler.invoke('editor.afterCommand', $editable);
|
||||
}
|
||||
};
|
||||
|
||||
this.attach = function (layoutInfo) {
|
||||
layoutInfo.editable().on('paste', pasteByEvent);
|
||||
};
|
||||
};
|
||||
|
||||
return Clipboard;
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
define([
|
||||
'summernote/core/agent',
|
||||
'summernote/core/dom'
|
||||
], function (agent, dom) {
|
||||
|
||||
var CodeMirror;
|
||||
if (agent.hasCodeMirror) {
|
||||
if (agent.isSupportAmd) {
|
||||
require(['CodeMirror'], function (cm) {
|
||||
CodeMirror = cm;
|
||||
});
|
||||
} else {
|
||||
CodeMirror = window.CodeMirror;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Codeview
|
||||
*/
|
||||
var Codeview = function (handler) {
|
||||
|
||||
this.sync = function (layoutInfo) {
|
||||
var isCodeview = handler.invoke('codeview.isActivated', layoutInfo);
|
||||
if (isCodeview && agent.hasCodeMirror) {
|
||||
layoutInfo.codable().data('cmEditor').save();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
* @return {Boolean}
|
||||
*/
|
||||
this.isActivated = function (layoutInfo) {
|
||||
var $editor = layoutInfo.editor();
|
||||
return $editor.hasClass('codeview');
|
||||
};
|
||||
|
||||
/**
|
||||
* toggle codeview
|
||||
*
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.toggle = function (layoutInfo) {
|
||||
if (this.isActivated(layoutInfo)) {
|
||||
this.deactivate(layoutInfo);
|
||||
} else {
|
||||
this.activate(layoutInfo);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* activate code view
|
||||
*
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.activate = function (layoutInfo) {
|
||||
var $editor = layoutInfo.editor(),
|
||||
$toolbar = layoutInfo.toolbar(),
|
||||
$editable = layoutInfo.editable(),
|
||||
$codable = layoutInfo.codable(),
|
||||
$popover = layoutInfo.popover(),
|
||||
$handle = layoutInfo.handle();
|
||||
|
||||
var options = $editor.data('options');
|
||||
|
||||
$codable.val(dom.html($editable, options.prettifyHtml));
|
||||
$codable.height($editable.height());
|
||||
|
||||
handler.invoke('toolbar.updateCodeview', $toolbar, true);
|
||||
handler.invoke('popover.hide', $popover);
|
||||
handler.invoke('handle.hide', $handle);
|
||||
|
||||
$editor.addClass('codeview');
|
||||
|
||||
$codable.focus();
|
||||
|
||||
// activate CodeMirror as codable
|
||||
if (agent.hasCodeMirror) {
|
||||
var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror);
|
||||
|
||||
// CodeMirror TernServer
|
||||
if (options.codemirror.tern) {
|
||||
var server = new CodeMirror.TernServer(options.codemirror.tern);
|
||||
cmEditor.ternServer = server;
|
||||
cmEditor.on('cursorActivity', function (cm) {
|
||||
server.updateArgHints(cm);
|
||||
});
|
||||
}
|
||||
|
||||
// CodeMirror hasn't Padding.
|
||||
cmEditor.setSize(null, $editable.outerHeight());
|
||||
$codable.data('cmEditor', cmEditor);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* deactivate code view
|
||||
*
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.deactivate = function (layoutInfo) {
|
||||
var $holder = layoutInfo.holder(),
|
||||
$editor = layoutInfo.editor(),
|
||||
$toolbar = layoutInfo.toolbar(),
|
||||
$editable = layoutInfo.editable(),
|
||||
$codable = layoutInfo.codable();
|
||||
|
||||
var options = $editor.data('options');
|
||||
|
||||
// deactivate CodeMirror as codable
|
||||
if (agent.hasCodeMirror) {
|
||||
var cmEditor = $codable.data('cmEditor');
|
||||
$codable.val(cmEditor.getValue());
|
||||
cmEditor.toTextArea();
|
||||
}
|
||||
|
||||
var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara;
|
||||
var isChange = $editable.html() !== value;
|
||||
|
||||
$editable.html(value);
|
||||
$editable.height(options.height ? $codable.height() : 'auto');
|
||||
$editor.removeClass('codeview');
|
||||
|
||||
if (isChange) {
|
||||
handler.bindCustomEvent(
|
||||
$holder, $editable.data('callbacks'), 'change'
|
||||
)($editable.html(), $editable);
|
||||
}
|
||||
|
||||
$editable.focus();
|
||||
|
||||
handler.invoke('toolbar.updateCodeview', $toolbar, false);
|
||||
};
|
||||
};
|
||||
|
||||
return Codeview;
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
define([
|
||||
'summernote/core/dom'
|
||||
], function (dom) {
|
||||
var DragAndDrop = function (handler) {
|
||||
var $document = $(document);
|
||||
|
||||
/**
|
||||
* attach Drag and Drop Events
|
||||
*
|
||||
* @param {Object} layoutInfo - layout Informations
|
||||
* @param {Object} options
|
||||
*/
|
||||
this.attach = function (layoutInfo, options) {
|
||||
if (options.airMode || options.disableDragAndDrop) {
|
||||
// prevent default drop event
|
||||
$document.on('drop', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
} else {
|
||||
this.attachDragAndDropEvent(layoutInfo, options);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* attach Drag and Drop Events
|
||||
*
|
||||
* @param {Object} layoutInfo - layout Informations
|
||||
* @param {Object} options
|
||||
*/
|
||||
this.attachDragAndDropEvent = function (layoutInfo, options) {
|
||||
var collection = $(),
|
||||
$editor = layoutInfo.editor(),
|
||||
$dropzone = layoutInfo.dropzone(),
|
||||
$dropzoneMessage = $dropzone.find('.note-dropzone-message');
|
||||
|
||||
// show dropzone on dragenter when dragging a object to document
|
||||
// -but only if the editor is visible, i.e. has a positive width and height
|
||||
$document.on('dragenter', function (e) {
|
||||
var isCodeview = handler.invoke('codeview.isActivated', layoutInfo);
|
||||
var hasEditorSize = $editor.width() > 0 && $editor.height() > 0;
|
||||
if (!isCodeview && !collection.length && hasEditorSize) {
|
||||
$editor.addClass('dragover');
|
||||
$dropzone.width($editor.width());
|
||||
$dropzone.height($editor.height());
|
||||
$dropzoneMessage.text(options.langInfo.image.dragImageHere);
|
||||
}
|
||||
collection = collection.add(e.target);
|
||||
}).on('dragleave', function (e) {
|
||||
collection = collection.not(e.target);
|
||||
if (!collection.length) {
|
||||
$editor.removeClass('dragover');
|
||||
}
|
||||
}).on('drop', function () {
|
||||
collection = $();
|
||||
$editor.removeClass('dragover');
|
||||
});
|
||||
|
||||
// change dropzone's message on hover.
|
||||
$dropzone.on('dragenter', function () {
|
||||
$dropzone.addClass('hover');
|
||||
$dropzoneMessage.text(options.langInfo.image.dropImage);
|
||||
}).on('dragleave', function () {
|
||||
$dropzone.removeClass('hover');
|
||||
$dropzoneMessage.text(options.langInfo.image.dragImageHere);
|
||||
});
|
||||
|
||||
// attach dropImage
|
||||
$dropzone.on('drop', function (event) {
|
||||
|
||||
var dataTransfer = event.originalEvent.dataTransfer;
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
|
||||
if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
layoutInfo.editable().focus();
|
||||
handler.insertImages(layoutInfo, dataTransfer.files);
|
||||
} else {
|
||||
var insertNodefunc = function () {
|
||||
layoutInfo.holder().summernote('insertNode', this);
|
||||
};
|
||||
|
||||
for (var i = 0, len = dataTransfer.types.length; i < len; i++) {
|
||||
var type = dataTransfer.types[i];
|
||||
var content = dataTransfer.getData(type);
|
||||
|
||||
if (type.toLowerCase().indexOf('text') > -1) {
|
||||
layoutInfo.holder().summernote('pasteHTML', content);
|
||||
} else {
|
||||
$(content).each(insertNodefunc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).on('dragover', false); // prevent default dragover event
|
||||
};
|
||||
};
|
||||
|
||||
return DragAndDrop;
|
||||
});
|
||||
@@ -1,865 +0,0 @@
|
||||
define([
|
||||
'summernote/core/agent',
|
||||
'summernote/core/func',
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/range',
|
||||
'summernote/core/async',
|
||||
'summernote/editing/Style',
|
||||
'summernote/editing/Typing',
|
||||
'summernote/editing/Table',
|
||||
'summernote/editing/Bullet'
|
||||
], function (agent, func, list, dom, range, async,
|
||||
Style, Typing, Table, Bullet) {
|
||||
|
||||
var KEY_BOGUS = 'bogus';
|
||||
|
||||
/**
|
||||
* @class editing.Editor
|
||||
*
|
||||
* Editor
|
||||
*
|
||||
*/
|
||||
var Editor = function (handler) {
|
||||
|
||||
var self = this;
|
||||
var style = new Style();
|
||||
var table = new Table();
|
||||
var typing = new Typing();
|
||||
var bullet = new Bullet();
|
||||
|
||||
this.style = style; // ODOO: allow access for override
|
||||
this.table = table; // ODOO: allow access for override
|
||||
this.typing = typing; // ODOO: allow access for override
|
||||
this.bullet = bullet; // ODOO: allow access for override
|
||||
|
||||
/**
|
||||
* @method createRange
|
||||
*
|
||||
* create range
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @return {WrappedRange}
|
||||
*/
|
||||
this.createRange = function ($editable) {
|
||||
this.focus($editable);
|
||||
return range.create();
|
||||
};
|
||||
|
||||
/**
|
||||
* @method saveRange
|
||||
*
|
||||
* save current range
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {Boolean} [thenCollapse=false]
|
||||
*/
|
||||
this.saveRange = function ($editable, thenCollapse) {
|
||||
// ODOO: scroll to top when click on input in editable m (start_modification
|
||||
// this.focus($editable);
|
||||
var r = range.create();
|
||||
if (!r || ($editable[0] !== r.sc && !$.contains($editable[0], r.sc))) {
|
||||
$editable.focus();
|
||||
}
|
||||
// ODOO: end_modication)
|
||||
$editable.data('range', range.create());
|
||||
if (thenCollapse) {
|
||||
range.create().collapse().select();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method saveRange
|
||||
*
|
||||
* save current node list to $editable.data('childNodes')
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.saveNode = function ($editable) {
|
||||
// copy child node reference
|
||||
var copy = [];
|
||||
for (var key = 0, len = $editable[0].childNodes.length; key < len; key++) {
|
||||
copy.push($editable[0].childNodes[key]);
|
||||
}
|
||||
$editable.data('childNodes', copy);
|
||||
};
|
||||
|
||||
/**
|
||||
* @method restoreRange
|
||||
*
|
||||
* restore lately range
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.restoreRange = function ($editable) {
|
||||
var rng = $editable.data('range');
|
||||
if (rng) {
|
||||
rng.select();
|
||||
this.focus($editable);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method restoreNode
|
||||
*
|
||||
* restore lately node list
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.restoreNode = function ($editable) {
|
||||
$editable.html('');
|
||||
var child = $editable.data('childNodes');
|
||||
for (var index = 0, len = child.length; index < len; index++) {
|
||||
$editable[0].appendChild(child[index]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method currentStyle
|
||||
*
|
||||
* current style
|
||||
*
|
||||
* @param {Node} target
|
||||
* @return {Object|Boolean} unfocus
|
||||
*/
|
||||
this.currentStyle = function (target) {
|
||||
var rng = range.create();
|
||||
var styleInfo = rng && rng.isOnEditable() ? style.current(rng.normalize()) : {};
|
||||
if (dom.isImg(target)) {
|
||||
styleInfo.image = target;
|
||||
}
|
||||
return styleInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* style from node
|
||||
*
|
||||
* @param {jQuery} $node
|
||||
* @return {Object}
|
||||
*/
|
||||
this.styleFromNode = function ($node) {
|
||||
return style.fromNode($node);
|
||||
};
|
||||
|
||||
var triggerOnBeforeChange = function ($editable) {
|
||||
var $holder = dom.makeLayoutInfo($editable).holder();
|
||||
handler.bindCustomEvent(
|
||||
$holder, $editable.data('callbacks'), 'before.command'
|
||||
)($editable.html(), $editable);
|
||||
};
|
||||
|
||||
var triggerOnChange = function ($editable) {
|
||||
var $holder = dom.makeLayoutInfo($editable).holder();
|
||||
handler.bindCustomEvent(
|
||||
$holder, $editable.data('callbacks'), 'change'
|
||||
)($editable.html(), $editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @method undo
|
||||
* undo
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.undo = function ($editable) {
|
||||
triggerOnBeforeChange($editable);
|
||||
$editable.data('NoteHistory').undo();
|
||||
triggerOnChange($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @method redo
|
||||
* redo
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.redo = function ($editable) {
|
||||
triggerOnBeforeChange($editable);
|
||||
$editable.data('NoteHistory').redo();
|
||||
triggerOnChange($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @method beforeCommand
|
||||
* before command
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
var beforeCommand = this.beforeCommand = function ($editable) {
|
||||
triggerOnBeforeChange($editable);
|
||||
// keep focus on editable before command execution
|
||||
self.focus($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @method afterCommand
|
||||
* after command
|
||||
* @param {jQuery} $editable
|
||||
* @param {Boolean} isPreventTrigger
|
||||
*/
|
||||
var afterCommand = this.afterCommand = function ($editable, isPreventTrigger) {
|
||||
$editable.data('NoteHistory').recordUndo();
|
||||
if (!isPreventTrigger) {
|
||||
triggerOnChange($editable);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method bold
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method italic
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method underline
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method strikethrough
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method formatBlock
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method superscript
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method subscript
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method justifyLeft
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method justifyCenter
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method justifyRight
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method justifyFull
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method formatBlock
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method removeFormat
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method backColor
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method foreColor
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method insertHorizontalRule
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @method fontName
|
||||
*
|
||||
* change font name
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
|
||||
/* jshint ignore:start */
|
||||
// native commands(with execCommand), generate function for execCommand
|
||||
var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
|
||||
'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
|
||||
'formatBlock', 'removeFormat',
|
||||
'backColor', 'foreColor', 'fontName'];
|
||||
|
||||
for (var idx = 0, len = commands.length; idx < len; idx ++) {
|
||||
this[commands[idx]] = (function (sCmd) {
|
||||
return function ($editable, value) {
|
||||
beforeCommand($editable);
|
||||
|
||||
document.execCommand(sCmd, false, value);
|
||||
|
||||
afterCommand($editable, true);
|
||||
};
|
||||
})(commands[idx]);
|
||||
}
|
||||
/* jshint ignore:end */
|
||||
|
||||
/**
|
||||
* @method tab
|
||||
*
|
||||
* handle tab key
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {Object} options
|
||||
*/
|
||||
this.tab = function ($editable, options) {
|
||||
var rng = this.createRange($editable);
|
||||
if (rng.isCollapsed() && rng.isOnCell()) {
|
||||
table.tab(rng);
|
||||
} else {
|
||||
beforeCommand($editable);
|
||||
typing.insertTab($editable, rng, options.tabsize);
|
||||
afterCommand($editable);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method untab
|
||||
*
|
||||
* handle shift+tab key
|
||||
*
|
||||
*/
|
||||
this.untab = function ($editable) {
|
||||
var rng = this.createRange($editable);
|
||||
if (rng.isCollapsed() && rng.isOnCell()) {
|
||||
table.tab(rng, true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method insertParagraph
|
||||
*
|
||||
* insert paragraph
|
||||
*
|
||||
* @param {Node} $editable
|
||||
*/
|
||||
this.insertParagraph = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
typing.insertParagraph($editable);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @method insertOrderedList
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.insertOrderedList = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
bullet.insertOrderedList($editable);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.insertUnorderedList = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
bullet.insertUnorderedList($editable);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.indent = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
bullet.indent($editable);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.outdent = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
bullet.outdent($editable);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* insert image
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} sUrl
|
||||
*/
|
||||
this.insertImage = function ($editable, sUrl, filename) {
|
||||
async.createImage(sUrl, filename).then(function ($image) {
|
||||
beforeCommand($editable);
|
||||
$image.css({
|
||||
display: '',
|
||||
width: Math.min($editable.width(), $image.width())
|
||||
});
|
||||
range.create().insertNode($image[0]);
|
||||
range.createFromNodeAfter($image[0]).select();
|
||||
afterCommand($editable);
|
||||
}).fail(function () {
|
||||
var $holder = dom.makeLayoutInfo($editable).holder();
|
||||
handler.bindCustomEvent(
|
||||
$holder, $editable.data('callbacks'), 'image.upload.error'
|
||||
)();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @method insertNode
|
||||
* insert node
|
||||
* @param {Node} $editable
|
||||
* @param {Node} node
|
||||
*/
|
||||
this.insertNode = function ($editable, node) {
|
||||
beforeCommand($editable);
|
||||
range.create().insertNode(node);
|
||||
range.createFromNodeAfter(node).select();
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* insert text
|
||||
* @param {Node} $editable
|
||||
* @param {String} text
|
||||
*/
|
||||
this.insertText = function ($editable, text) {
|
||||
beforeCommand($editable);
|
||||
var textNode = range.create().insertNode(dom.createText(text));
|
||||
range.create(textNode, dom.nodeLength(textNode)).select();
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* paste HTML
|
||||
* @param {Node} $editable
|
||||
* @param {String} markup
|
||||
*/
|
||||
this.pasteHTML = function ($editable, markup) {
|
||||
beforeCommand($editable);
|
||||
var contents = range.create().pasteHTML(markup);
|
||||
range.createFromNodeAfter(list.last(contents)).select();
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* formatBlock
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} tagName
|
||||
*/
|
||||
this.formatBlock = function ($editable, tagName) {
|
||||
beforeCommand($editable);
|
||||
// [workaround] for MSIE, IE need `<`
|
||||
tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
|
||||
document.execCommand('FormatBlock', false, tagName);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
this.formatPara = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
this.formatBlock($editable, 'P');
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/* jshint ignore:start */
|
||||
for (var idx = 1; idx <= 6; idx ++) {
|
||||
this['formatH' + idx] = function (idx) {
|
||||
return function ($editable) {
|
||||
this.formatBlock($editable, 'H' + idx);
|
||||
};
|
||||
}(idx);
|
||||
};
|
||||
/* jshint ignore:end */
|
||||
|
||||
/**
|
||||
* fontSize
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} value - px
|
||||
*/
|
||||
this.fontSize = function ($editable, value) {
|
||||
var rng = range.create();
|
||||
|
||||
if (rng.isCollapsed()) {
|
||||
var spans = style.styleNodes(rng);
|
||||
var firstSpan = list.head(spans);
|
||||
|
||||
$(spans).css({
|
||||
'font-size': value + 'px'
|
||||
});
|
||||
|
||||
// [workaround] added styled bogus span for style
|
||||
// - also bogus character needed for cursor position
|
||||
if (firstSpan && !dom.nodeLength(firstSpan)) {
|
||||
firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR;
|
||||
range.createFromNodeAfter(firstSpan.firstChild).select();
|
||||
$editable.data(KEY_BOGUS, firstSpan);
|
||||
}
|
||||
} else {
|
||||
beforeCommand($editable);
|
||||
$(style.styleNodes(rng)).css({
|
||||
'font-size': value + 'px'
|
||||
});
|
||||
afterCommand($editable);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* insert horizontal rule
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.insertHorizontalRule = function ($editable) {
|
||||
beforeCommand($editable);
|
||||
|
||||
var rng = range.create();
|
||||
var hrNode = rng.insertNode($('<HR/>')[0]);
|
||||
if (hrNode.nextSibling) {
|
||||
range.create(hrNode.nextSibling, 0).normalize().select();
|
||||
}
|
||||
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove bogus node and character
|
||||
*/
|
||||
this.removeBogus = function ($editable) {
|
||||
var bogusNode = $editable.data(KEY_BOGUS);
|
||||
if (!bogusNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
var textNode = list.find(list.from(bogusNode.childNodes), dom.isText);
|
||||
|
||||
var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR);
|
||||
if (bogusCharIdx !== -1) {
|
||||
textNode.deleteData(bogusCharIdx, 1);
|
||||
}
|
||||
|
||||
if (dom.isEmpty(bogusNode)) {
|
||||
dom.remove(bogusNode);
|
||||
}
|
||||
|
||||
$editable.removeData(KEY_BOGUS);
|
||||
};
|
||||
|
||||
/**
|
||||
* lineHeight
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} value
|
||||
*/
|
||||
this.lineHeight = function ($editable, value) {
|
||||
beforeCommand($editable);
|
||||
style.stylePara(range.create(), {
|
||||
lineHeight: value
|
||||
});
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* unlink
|
||||
*
|
||||
* @type command
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
this.unlink = function ($editable) {
|
||||
var rng = this.createRange($editable);
|
||||
if (rng.isOnAnchor()) {
|
||||
var anchor = dom.ancestor(rng.sc, dom.isAnchor);
|
||||
rng = range.createFromNode(anchor);
|
||||
rng.select();
|
||||
|
||||
beforeCommand($editable);
|
||||
document.execCommand('unlink');
|
||||
afterCommand($editable);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* create link (command)
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {Object} linkInfo
|
||||
* @param {Object} options
|
||||
*/
|
||||
this.createLink = function ($editable, linkInfo, options) {
|
||||
var linkUrl = linkInfo.url;
|
||||
var linkText = linkInfo.text;
|
||||
var isNewWindow = linkInfo.isNewWindow;
|
||||
var rng = linkInfo.range || this.createRange($editable);
|
||||
var isTextChanged = rng.toString() !== linkText;
|
||||
|
||||
options = options || dom.makeLayoutInfo($editable).editor().data('options');
|
||||
|
||||
beforeCommand($editable);
|
||||
|
||||
if (options.onCreateLink) {
|
||||
linkUrl = options.onCreateLink(linkUrl);
|
||||
}
|
||||
|
||||
var anchors = [];
|
||||
// ODOO: adding this branch to modify existing anchor if it fully contains the range
|
||||
var ancestor_anchor = dom.ancestor(rng.sc, dom.isAnchor);
|
||||
if(ancestor_anchor && ancestor_anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
|
||||
anchors.push($(ancestor_anchor).html(linkText).get(0));
|
||||
} else if (isTextChanged) {
|
||||
// Create a new link when text changed.
|
||||
var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]);
|
||||
anchors.push(anchor);
|
||||
} else {
|
||||
anchors = style.styleNodes(rng, {
|
||||
nodeName: 'A',
|
||||
expandClosestSibling: true,
|
||||
onlyPartialContains: true
|
||||
});
|
||||
}
|
||||
|
||||
$.each(anchors, function (idx, anchor) {
|
||||
$(anchor).attr('href', linkUrl);
|
||||
$(anchor).attr('class', linkInfo.className || null); // ODOO: addition
|
||||
$(anchor).css(linkInfo.style || {}); // ODOO: addition
|
||||
if (isNewWindow) {
|
||||
$(anchor).attr('target', '_blank');
|
||||
} else {
|
||||
$(anchor).removeAttr('target');
|
||||
}
|
||||
});
|
||||
|
||||
var startRange = range.createFromNodeBefore(list.head(anchors));
|
||||
var startPoint = startRange.getStartPoint();
|
||||
var endRange = range.createFromNodeAfter(list.last(anchors));
|
||||
var endPoint = endRange.getEndPoint();
|
||||
|
||||
range.create(
|
||||
startPoint.node,
|
||||
startPoint.offset,
|
||||
endPoint.node,
|
||||
endPoint.offset
|
||||
).select();
|
||||
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns link info
|
||||
*
|
||||
* @return {Object}
|
||||
* @return {WrappedRange} return.range
|
||||
* @return {String} return.text
|
||||
* @return {Boolean} [return.isNewWindow=true]
|
||||
* @return {String} [return.url=""]
|
||||
*/
|
||||
this.getLinkInfo = function ($editable) {
|
||||
this.focus($editable);
|
||||
|
||||
var rng = range.create().expand(dom.isAnchor);
|
||||
|
||||
// Get the first anchor on range(for edit).
|
||||
var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
|
||||
|
||||
return {
|
||||
range: rng,
|
||||
text: rng.toString(),
|
||||
isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : false,
|
||||
url: $anchor.length ? $anchor.attr('href') : ''
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* setting color
|
||||
*
|
||||
* @param {Node} $editable
|
||||
* @param {Object} sObjColor color code
|
||||
* @param {String} sObjColor.foreColor foreground color
|
||||
* @param {String} sObjColor.backColor background color
|
||||
*/
|
||||
this.color = function ($editable, sObjColor) {
|
||||
var oColor = JSON.parse(sObjColor);
|
||||
var foreColor = oColor.foreColor, backColor = oColor.backColor;
|
||||
|
||||
beforeCommand($editable);
|
||||
|
||||
if (foreColor) { document.execCommand('foreColor', false, foreColor); }
|
||||
if (backColor) { document.execCommand('backColor', false, backColor); }
|
||||
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* insert Table
|
||||
*
|
||||
* @param {Node} $editable
|
||||
* @param {String} sDim dimension of table (ex : "5x5")
|
||||
*/
|
||||
this.insertTable = function ($editable, sDim) {
|
||||
var dimension = sDim.split('x');
|
||||
beforeCommand($editable);
|
||||
|
||||
var rng = range.create().deleteContents();
|
||||
rng.insertNode(table.createTable(dimension[0], dimension[1]));
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* float me
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} value
|
||||
* @param {jQuery} $target
|
||||
*/
|
||||
this.floatMe = function ($editable, value, $target) {
|
||||
beforeCommand($editable);
|
||||
// bootstrap
|
||||
$target.removeClass('float-left float-right');
|
||||
if (value && value !== 'none') {
|
||||
$target.addClass('pull-' + value);
|
||||
}
|
||||
|
||||
// fallback for non-bootstrap
|
||||
$target.css('float', value);
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* change image shape
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} value css class
|
||||
* @param {Node} $target
|
||||
*/
|
||||
this.imageShape = function ($editable, value, $target) {
|
||||
beforeCommand($editable);
|
||||
|
||||
$target.removeClass('rounded rounded-circle img-thumbnail');
|
||||
|
||||
if (value) {
|
||||
$target.addClass(value);
|
||||
}
|
||||
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* resize overlay element
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} value
|
||||
* @param {jQuery} $target - target element
|
||||
*/
|
||||
this.resize = function ($editable, value, $target) {
|
||||
beforeCommand($editable);
|
||||
|
||||
$target.css({
|
||||
width: value * 100 + '%',
|
||||
height: ''
|
||||
});
|
||||
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Position} pos
|
||||
* @param {jQuery} $target - target element
|
||||
* @param {Boolean} [bKeepRatio] - keep ratio
|
||||
*/
|
||||
this.resizeTo = function (pos, $target, bKeepRatio) {
|
||||
var imageSize;
|
||||
if (bKeepRatio) {
|
||||
var newRatio = pos.y / pos.x;
|
||||
var ratio = $target.data('ratio');
|
||||
imageSize = {
|
||||
width: ratio > newRatio ? pos.x : pos.y / ratio,
|
||||
height: ratio > newRatio ? pos.x * ratio : pos.y
|
||||
};
|
||||
} else {
|
||||
imageSize = {
|
||||
width: pos.x,
|
||||
height: pos.y
|
||||
};
|
||||
}
|
||||
|
||||
$target.css(imageSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove media object
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} value - dummy argument (for keep interface)
|
||||
* @param {jQuery} $target - target element
|
||||
*/
|
||||
this.removeMedia = function ($editable, value, $target) {
|
||||
beforeCommand($editable);
|
||||
$target.detach();
|
||||
|
||||
handler.bindCustomEvent(
|
||||
$(), $editable.data('callbacks'), 'media.delete'
|
||||
)($target, $editable);
|
||||
|
||||
afterCommand($editable);
|
||||
};
|
||||
|
||||
/**
|
||||
* set focus
|
||||
*
|
||||
* @param $editable
|
||||
*/
|
||||
this.focus = function ($editable) {
|
||||
$editable.focus();
|
||||
|
||||
// [workaround] for firefox bug http://goo.gl/lVfAaI
|
||||
if (agent.isFF) {
|
||||
var rng = range.create();
|
||||
if (!rng || rng.isOnEditable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
range.createFromNode($editable[0])
|
||||
.normalize()
|
||||
.collapse()
|
||||
.select();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns whether contents is empty or not.
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @return {Boolean}
|
||||
*/
|
||||
this.isEmpty = function ($editable) {
|
||||
return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html();
|
||||
};
|
||||
};
|
||||
|
||||
return Editor;
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
define([], function () { // ODOO: remove error from Odoo define
|
||||
var Fullscreen = function (handler) {
|
||||
var $window = $(window);
|
||||
var $scrollbar = $('html, body');
|
||||
|
||||
/**
|
||||
* toggle fullscreen
|
||||
*
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.toggle = function (layoutInfo) {
|
||||
|
||||
var $editor = layoutInfo.editor(),
|
||||
$toolbar = layoutInfo.toolbar(),
|
||||
$editable = layoutInfo.editable(),
|
||||
$codable = layoutInfo.codable();
|
||||
|
||||
var resize = function (size) {
|
||||
$editable.css('height', size.h);
|
||||
$codable.css('height', size.h);
|
||||
if ($codable.data('cmeditor')) {
|
||||
$codable.data('cmeditor').setsize(null, size.h);
|
||||
}
|
||||
};
|
||||
|
||||
$editor.toggleClass('fullscreen');
|
||||
var isFullscreen = $editor.hasClass('fullscreen');
|
||||
if (isFullscreen) {
|
||||
$editable.data('orgheight', $editable.css('height'));
|
||||
|
||||
$window.on('resize', function () {
|
||||
resize({
|
||||
h: $window.height() - $toolbar.outerHeight()
|
||||
});
|
||||
}).trigger('resize');
|
||||
|
||||
$scrollbar.css('overflow', 'hidden');
|
||||
} else {
|
||||
$window.off('resize');
|
||||
resize({
|
||||
h: $editable.data('orgheight')
|
||||
});
|
||||
$scrollbar.css('overflow', 'visible');
|
||||
}
|
||||
|
||||
handler.invoke('toolbar.updateFullscreen', $toolbar, isFullscreen);
|
||||
};
|
||||
};
|
||||
|
||||
return Fullscreen;
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
define([
|
||||
'summernote/core/dom'
|
||||
], function (dom) {
|
||||
/**
|
||||
* @class module.Handle
|
||||
*
|
||||
* Handle
|
||||
*/
|
||||
var Handle = function (handler) {
|
||||
var $document = $(document);
|
||||
|
||||
/**
|
||||
* `mousedown` event handler on $handle
|
||||
* - controlSizing: resize image
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
var hHandleMousedown = function (event) {
|
||||
if (dom.isControlSizing(event.target)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var layoutInfo = dom.makeLayoutInfo(event.target),
|
||||
$handle = layoutInfo.handle(),
|
||||
$popover = layoutInfo.popover(),
|
||||
$editable = layoutInfo.editable(),
|
||||
$editor = layoutInfo.editor();
|
||||
|
||||
var target = $handle.find('.note-control-selection').data('target'),
|
||||
$target = $(target), posStart = $target.offset(),
|
||||
scrollTop = $document.scrollTop();
|
||||
|
||||
var isAirMode = $editor.data('options').airMode;
|
||||
|
||||
$document.on('mousemove', function (event) {
|
||||
handler.invoke('editor.resizeTo', {
|
||||
x: event.clientX - posStart.left,
|
||||
y: event.clientY - (posStart.top - scrollTop)
|
||||
}, $target, !event.shiftKey);
|
||||
|
||||
handler.invoke('handle.update', $handle, {image: target}, isAirMode);
|
||||
handler.invoke('popover.update', $popover, {image: target}, isAirMode);
|
||||
}).one('mouseup', function () {
|
||||
$document.off('mousemove');
|
||||
handler.invoke('editor.afterCommand', $editable);
|
||||
});
|
||||
|
||||
if (!$target.data('ratio')) { // original ratio.
|
||||
$target.data('ratio', $target.height() / $target.width());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.attach = function (layoutInfo) {
|
||||
layoutInfo.handle().on('mousedown', hHandleMousedown);
|
||||
};
|
||||
|
||||
/**
|
||||
* update handle
|
||||
* @param {jQuery} $handle
|
||||
* @param {Object} styleInfo
|
||||
* @param {Boolean} isAirMode
|
||||
*/
|
||||
this.update = function ($handle, styleInfo, isAirMode) {
|
||||
var $selection = $handle.find('.note-control-selection');
|
||||
if (styleInfo.image) {
|
||||
var $image = $(styleInfo.image);
|
||||
var pos = isAirMode ? $image.offset() : $image.position();
|
||||
|
||||
// include margin
|
||||
var imageSize = {
|
||||
w: $image.outerWidth(true),
|
||||
h: $image.outerHeight(true)
|
||||
};
|
||||
|
||||
$selection.css({
|
||||
display: 'block',
|
||||
left: pos.left,
|
||||
top: pos.top,
|
||||
width: imageSize.w,
|
||||
height: imageSize.h
|
||||
}).data('target', styleInfo.image); // save current image element.
|
||||
var sizingText = imageSize.w + 'x' + imageSize.h;
|
||||
$selection.find('.note-control-selection-info').text(sizingText);
|
||||
} else {
|
||||
$selection.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* hide
|
||||
*
|
||||
* @param {jQuery} $handle
|
||||
*/
|
||||
this.hide = function ($handle) {
|
||||
$handle.children().hide();
|
||||
};
|
||||
};
|
||||
|
||||
return Handle;
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
define([], function () { // ODOO: remove error from Odoo define
|
||||
var HelpDialog = function (handler) {
|
||||
/**
|
||||
* show help dialog
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {jQuery} $dialog
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.showHelpDialog = function ($editable, $dialog) {
|
||||
return $.Deferred(function (deferred) {
|
||||
var $helpDialog = $dialog.find('.note-help-dialog');
|
||||
|
||||
$helpDialog.one('hidden.bs.modal', function () {
|
||||
deferred.resolve();
|
||||
}).modal('show');
|
||||
}).promise();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.show = function (layoutInfo) {
|
||||
var $dialog = layoutInfo.dialog(),
|
||||
$editable = layoutInfo.editable();
|
||||
|
||||
handler.invoke('editor.saveRange', $editable, true);
|
||||
this.showHelpDialog($editable, $dialog).then(function () {
|
||||
handler.invoke('editor.restoreRange', $editable);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return HelpDialog;
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
define([
|
||||
'summernote/core/key'
|
||||
], function (key) {
|
||||
var ImageDialog = function (handler) {
|
||||
/**
|
||||
* toggle button status
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $btn
|
||||
* @param {Boolean} isEnable
|
||||
*/
|
||||
var toggleBtn = function ($btn, isEnable) {
|
||||
$btn.toggleClass('disabled', !isEnable);
|
||||
$btn.attr('disabled', !isEnable);
|
||||
};
|
||||
|
||||
/**
|
||||
* bind enter key
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $input
|
||||
* @param {jQuery} $btn
|
||||
*/
|
||||
var bindEnterKey = function ($input, $btn) {
|
||||
$input.on('keypress', function (event) {
|
||||
if (event.keyCode === key.code.ENTER) {
|
||||
$btn.trigger('click');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.show = function (layoutInfo) {
|
||||
var $dialog = layoutInfo.dialog(),
|
||||
$editable = layoutInfo.editable();
|
||||
|
||||
handler.invoke('editor.saveRange', $editable);
|
||||
this.showImageDialog($editable, $dialog).then(function (data) {
|
||||
handler.invoke('editor.restoreRange', $editable);
|
||||
|
||||
if (typeof data === 'string') {
|
||||
// image url
|
||||
handler.invoke('editor.insertImage', $editable, data);
|
||||
} else {
|
||||
// array of files
|
||||
handler.insertImages(layoutInfo, data);
|
||||
}
|
||||
}).fail(function () {
|
||||
handler.invoke('editor.restoreRange', $editable);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* show image dialog
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {jQuery} $dialog
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.showImageDialog = function ($editable, $dialog) {
|
||||
return $.Deferred(function (deferred) {
|
||||
var $imageDialog = $dialog.find('.note-image-dialog');
|
||||
|
||||
var $imageInput = $dialog.find('.note-image-input'),
|
||||
$imageUrl = $dialog.find('.note-image-url'),
|
||||
$imageBtn = $dialog.find('.note-image-btn');
|
||||
|
||||
$imageDialog.one('shown.bs.modal', function () {
|
||||
// Cloning imageInput to clear element.
|
||||
$imageInput.replaceWith($imageInput.clone()
|
||||
.on('change', function () {
|
||||
deferred.resolve(this.files || this.value);
|
||||
$imageDialog.modal('hide');
|
||||
})
|
||||
.val('')
|
||||
);
|
||||
|
||||
$imageBtn.click(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
deferred.resolve($imageUrl.val());
|
||||
$imageDialog.modal('hide');
|
||||
});
|
||||
|
||||
$imageUrl.on('keyup paste', function (event) {
|
||||
var url;
|
||||
|
||||
if (event.type === 'paste') {
|
||||
url = event.originalEvent.clipboardData.getData('text');
|
||||
} else {
|
||||
url = $imageUrl.val();
|
||||
}
|
||||
|
||||
toggleBtn($imageBtn, url);
|
||||
}).val('').trigger('focus');
|
||||
bindEnterKey($imageUrl, $imageBtn);
|
||||
}).one('hidden.bs.modal', function () {
|
||||
$imageInput.off('change');
|
||||
$imageUrl.off('keyup paste keypress');
|
||||
$imageBtn.off('click');
|
||||
|
||||
if (deferred.state() === 'pending') {
|
||||
deferred.reject();
|
||||
}
|
||||
}).modal('show');
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return ImageDialog;
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
define([
|
||||
'summernote/core/key'
|
||||
], function (key) {
|
||||
var LinkDialog = function (handler) {
|
||||
|
||||
/**
|
||||
* toggle button status
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $btn
|
||||
* @param {Boolean} isEnable
|
||||
*/
|
||||
var toggleBtn = function ($btn, isEnable) {
|
||||
$btn.toggleClass('disabled', !isEnable);
|
||||
$btn.attr('disabled', !isEnable);
|
||||
};
|
||||
|
||||
/**
|
||||
* bind enter key
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $input
|
||||
* @param {jQuery} $btn
|
||||
*/
|
||||
var bindEnterKey = function ($input, $btn) {
|
||||
$input.on('keypress', function (event) {
|
||||
if (event.keyCode === key.code.ENTER) {
|
||||
$btn.trigger('click');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show link dialog and set event handlers on dialog controls.
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {jQuery} $dialog
|
||||
* @param {Object} linkInfo
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.showLinkDialog = function ($editable, $dialog, linkInfo) {
|
||||
return $.Deferred(function (deferred) {
|
||||
var $linkDialog = $dialog.find('.note-link-dialog');
|
||||
|
||||
var $linkText = $linkDialog.find('.note-link-text'),
|
||||
$linkUrl = $linkDialog.find('.note-link-url'),
|
||||
$linkBtn = $linkDialog.find('.note-link-btn'),
|
||||
$openInNewWindow = $linkDialog.find('input[type=checkbox]');
|
||||
|
||||
$linkDialog.one('shown.bs.modal', function () {
|
||||
$linkText.val(linkInfo.text);
|
||||
|
||||
$linkText.on('input', function () {
|
||||
toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
|
||||
// if linktext was modified by keyup,
|
||||
// stop cloning text from linkUrl
|
||||
linkInfo.text = $linkText.val();
|
||||
});
|
||||
|
||||
// if no url was given, copy text to url
|
||||
if (!linkInfo.url) {
|
||||
linkInfo.url = linkInfo.text || 'http://';
|
||||
toggleBtn($linkBtn, linkInfo.text);
|
||||
}
|
||||
|
||||
$linkUrl.on('input', function () {
|
||||
toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
|
||||
// display same link on `Text to display` input
|
||||
// when create a new link
|
||||
if (!linkInfo.text) {
|
||||
$linkText.val($linkUrl.val());
|
||||
}
|
||||
}).val(linkInfo.url).trigger('focus').trigger('select');
|
||||
|
||||
bindEnterKey($linkUrl, $linkBtn);
|
||||
bindEnterKey($linkText, $linkBtn);
|
||||
|
||||
$openInNewWindow.prop('checked', linkInfo.isNewWindow);
|
||||
|
||||
$linkBtn.one('click', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
deferred.resolve({
|
||||
range: linkInfo.range,
|
||||
url: $linkUrl.val(),
|
||||
text: $linkText.val(),
|
||||
isNewWindow: $openInNewWindow.is(':checked')
|
||||
});
|
||||
$linkDialog.modal('hide');
|
||||
});
|
||||
}).one('hidden.bs.modal', function () {
|
||||
// detach events
|
||||
$linkText.off('input keypress');
|
||||
$linkUrl.off('input keypress');
|
||||
$linkBtn.off('click');
|
||||
|
||||
if (deferred.state() === 'pending') {
|
||||
deferred.reject();
|
||||
}
|
||||
}).modal('show');
|
||||
}).promise();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} layoutInfo
|
||||
*/
|
||||
this.show = function (layoutInfo) {
|
||||
var $editor = layoutInfo.editor(),
|
||||
$dialog = layoutInfo.dialog(),
|
||||
$editable = layoutInfo.editable(),
|
||||
$popover = layoutInfo.popover(),
|
||||
linkInfo = handler.invoke('editor.getLinkInfo', $editable);
|
||||
|
||||
var options = $editor.data('options');
|
||||
|
||||
handler.invoke('editor.saveRange', $editable);
|
||||
this.showLinkDialog($editable, $dialog, linkInfo).then(function (linkInfo) {
|
||||
handler.invoke('editor.restoreRange', $editable);
|
||||
handler.invoke('editor.createLink', $editable, linkInfo, options);
|
||||
// hide popover after creating link
|
||||
handler.invoke('popover.hide', $popover);
|
||||
}).fail(function () {
|
||||
handler.invoke('editor.restoreRange', $editable);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return LinkDialog;
|
||||
});
|
||||
@@ -1,129 +0,0 @@
|
||||
define([
|
||||
'summernote/core/func',
|
||||
'summernote/core/list',
|
||||
'summernote/module/Button'
|
||||
], function (func, list, Button) {
|
||||
/**
|
||||
* @class module.Popover
|
||||
*
|
||||
* Popover (http://getbootstrap.com/javascript/#popovers)
|
||||
*
|
||||
*/
|
||||
var Popover = function () {
|
||||
var button = new Button();
|
||||
|
||||
this.button = button; // ODOO: allow access for override
|
||||
|
||||
/**
|
||||
* returns position from placeholder
|
||||
*
|
||||
* @private
|
||||
* @param {Node} placeholder
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.isAirMode
|
||||
* @return {Position}
|
||||
*/
|
||||
var posFromPlaceholder = function (placeholder, options) {
|
||||
var isAirMode = options && options.isAirMode;
|
||||
var isLeftTop = options && options.isLeftTop;
|
||||
|
||||
var $placeholder = $(placeholder);
|
||||
var pos = isAirMode ? $placeholder.offset() : $placeholder.position();
|
||||
var height = isLeftTop ? 0 : $placeholder.outerHeight(true); // include margin
|
||||
|
||||
// popover below placeholder.
|
||||
return {
|
||||
left: pos.left,
|
||||
top: pos.top + height
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* show popover
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} popover
|
||||
* @param {Position} pos
|
||||
*/
|
||||
var showPopover = function ($popover, pos) {
|
||||
$popover.css({
|
||||
display: 'block',
|
||||
left: pos.left,
|
||||
top: pos.top
|
||||
});
|
||||
};
|
||||
|
||||
var PX_POPOVER_ARROW_OFFSET_X = 20;
|
||||
|
||||
/**
|
||||
* update current state
|
||||
* @param {jQuery} $popover - popover container
|
||||
* @param {Object} styleInfo - style object
|
||||
* @param {Boolean} isAirMode
|
||||
*/
|
||||
this.update = function ($popover, styleInfo, isAirMode) {
|
||||
button.update($popover, styleInfo);
|
||||
|
||||
var $linkPopover = $popover.find('.note-link-popover');
|
||||
if (styleInfo.anchor) {
|
||||
var $anchor = $linkPopover.find('a');
|
||||
var href = $(styleInfo.anchor).attr('href');
|
||||
var target = $(styleInfo.anchor).attr('target');
|
||||
$anchor.attr('href', href).html(href);
|
||||
if (!target) {
|
||||
$anchor.removeAttr('target');
|
||||
} else {
|
||||
$anchor.attr('target', '_blank');
|
||||
}
|
||||
showPopover($linkPopover, posFromPlaceholder(styleInfo.anchor, {
|
||||
isAirMode: isAirMode
|
||||
}));
|
||||
} else {
|
||||
$linkPopover.hide();
|
||||
}
|
||||
|
||||
var $imagePopover = $popover.find('.note-image-popover');
|
||||
if (styleInfo.image) {
|
||||
showPopover($imagePopover, posFromPlaceholder(styleInfo.image, {
|
||||
isAirMode: isAirMode,
|
||||
isLeftTop: true
|
||||
}));
|
||||
} else {
|
||||
$imagePopover.hide();
|
||||
}
|
||||
|
||||
var $airPopover = $popover.find('.note-air-popover');
|
||||
if (isAirMode && styleInfo.range && !styleInfo.range.isCollapsed()) {
|
||||
var rect = list.last(styleInfo.range.getClientRects());
|
||||
if (rect) {
|
||||
var bnd = func.rect2bnd(rect);
|
||||
showPopover($airPopover, {
|
||||
left: Math.max(bnd.left + bnd.width / 2 - PX_POPOVER_ARROW_OFFSET_X, 0),
|
||||
top: bnd.top + bnd.height
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$airPopover.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node} button
|
||||
* @param {String} eventName
|
||||
* @param {String} value
|
||||
*/
|
||||
this.updateRecentColor = function (button, eventName, value) {
|
||||
button.updateRecentColor(button, eventName, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* hide all popovers
|
||||
* @param {jQuery} $popover - popover container
|
||||
*/
|
||||
this.hide = function ($popover) {
|
||||
$popover.children().hide();
|
||||
};
|
||||
};
|
||||
|
||||
return Popover;
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
define([
|
||||
'summernote/core/dom'
|
||||
], function (dom) {
|
||||
var EDITABLE_PADDING = 24;
|
||||
|
||||
var Statusbar = function () {
|
||||
var $document = $(document);
|
||||
|
||||
this.attach = function (layoutInfo, options) {
|
||||
if (!options.disableResizeEditor) {
|
||||
layoutInfo.statusbar().on('mousedown', hStatusbarMousedown);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* `mousedown` event handler on statusbar
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
var hStatusbarMousedown = function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var $editable = dom.makeLayoutInfo(event.target).editable();
|
||||
var editableTop = $editable.offset().top - $document.scrollTop();
|
||||
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
var options = layoutInfo.editor().data('options');
|
||||
|
||||
$document.on('mousemove', function (event) {
|
||||
var nHeight = event.clientY - (editableTop + EDITABLE_PADDING);
|
||||
|
||||
nHeight = (options.minHeight > 0) ? Math.max(nHeight, options.minHeight) : nHeight;
|
||||
nHeight = (options.maxHeight > 0) ? Math.min(nHeight, options.maxHeight) : nHeight;
|
||||
|
||||
$editable.height(nHeight);
|
||||
}).one('mouseup', function () {
|
||||
$document.off('mousemove');
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return Statusbar;
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
define([
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom',
|
||||
'summernote/module/Button'
|
||||
], function (list, dom, Button) {
|
||||
/**
|
||||
* @class module.Toolbar
|
||||
*
|
||||
* Toolbar
|
||||
*/
|
||||
var Toolbar = function () {
|
||||
var button = new Button();
|
||||
|
||||
this.button = button; // ODOO: allow access for override
|
||||
|
||||
this.update = function ($toolbar, styleInfo) {
|
||||
button.update($toolbar, styleInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node} button
|
||||
* @param {String} eventName
|
||||
* @param {String} value
|
||||
*/
|
||||
this.updateRecentColor = function (buttonNode, eventName, value) {
|
||||
button.updateRecentColor(buttonNode, eventName, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* activate buttons exclude codeview
|
||||
* @param {jQuery} $toolbar
|
||||
*/
|
||||
this.activate = function ($toolbar) {
|
||||
$toolbar.find('button')
|
||||
.not('button[data-event="codeview"]')
|
||||
.removeClass('disabled');
|
||||
};
|
||||
|
||||
/**
|
||||
* deactivate buttons exclude codeview
|
||||
* @param {jQuery} $toolbar
|
||||
*/
|
||||
this.deactivate = function ($toolbar) {
|
||||
$toolbar.find('button')
|
||||
.not('button[data-event="codeview"]')
|
||||
.addClass('disabled');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {jQuery} $container
|
||||
* @param {Boolean} [bFullscreen=false]
|
||||
*/
|
||||
this.updateFullscreen = function ($container, bFullscreen) {
|
||||
var $btn = $container.find('button[data-event="fullscreen"]');
|
||||
$btn.toggleClass('active', bFullscreen);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {jQuery} $container
|
||||
* @param {Boolean} [isCodeview=false]
|
||||
*/
|
||||
this.updateCodeview = function ($container, isCodeview) {
|
||||
var $btn = $container.find('button[data-event="codeview"]');
|
||||
$btn.toggleClass('active', isCodeview);
|
||||
|
||||
if (isCodeview) {
|
||||
this.deactivate($container);
|
||||
} else {
|
||||
this.activate($container);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* get button in toolbar
|
||||
*
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} name
|
||||
* @return {jQuery}
|
||||
*/
|
||||
this.get = function ($editable, name) {
|
||||
var $toolbar = dom.makeLayoutInfo($editable).toolbar();
|
||||
|
||||
return $toolbar.find('[data-name=' + name + ']');
|
||||
};
|
||||
|
||||
/**
|
||||
* set button state
|
||||
* @param {jQuery} $editable
|
||||
* @param {String} name
|
||||
* @param {Boolean} [isActive=true]
|
||||
*/
|
||||
this.setButtonState = function ($editable, name, isActive) {
|
||||
isActive = (isActive === false) ? false : true;
|
||||
|
||||
var $button = this.get($editable, name);
|
||||
$button.toggleClass('active', isActive);
|
||||
};
|
||||
};
|
||||
|
||||
return Toolbar;
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
}));
|
||||
@@ -1,328 +0,0 @@
|
||||
define([
|
||||
'summernote/core/agent',
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/range',
|
||||
'summernote/defaults',
|
||||
'summernote/EventHandler',
|
||||
'summernote/Renderer',
|
||||
'summernote/core/key' // ODOO: change for override
|
||||
], function (agent, list, dom, range,
|
||||
defaults, EventHandler, Renderer, key) {
|
||||
|
||||
// jQuery namespace for summernote
|
||||
/**
|
||||
* @class $.summernote
|
||||
*
|
||||
* summernote attribute
|
||||
*
|
||||
* @mixin defaults
|
||||
* @singleton
|
||||
*
|
||||
*/
|
||||
$.summernote = $.summernote || {};
|
||||
|
||||
// extends default settings
|
||||
// - $.summernote.version
|
||||
// - $.summernote.options
|
||||
// - $.summernote.lang
|
||||
$.extend($.summernote, defaults);
|
||||
|
||||
var renderer = new Renderer();
|
||||
var eventHandler = new EventHandler();
|
||||
|
||||
$.extend($.summernote, {
|
||||
/** @property {Renderer} */
|
||||
renderer: renderer,
|
||||
/** @property {EventHandler} */
|
||||
eventHandler: eventHandler,
|
||||
/**
|
||||
* @property {Object} core
|
||||
* @property {core.agent} core.agent
|
||||
* @property {core.dom} core.dom
|
||||
* @property {core.range} core.range
|
||||
*/
|
||||
core: {
|
||||
agent: agent,
|
||||
list : list,
|
||||
dom: dom,
|
||||
range: range,
|
||||
key: key // ODOO: change for override
|
||||
},
|
||||
/**
|
||||
* @property {Object}
|
||||
* pluginEvents event list for plugins
|
||||
* event has name and callback function.
|
||||
*
|
||||
* ```
|
||||
* $.summernote.addPlugin({
|
||||
* events : {
|
||||
* 'hello' : function(layoutInfo, value, $target) {
|
||||
* console.log('event name is hello, value is ' + value );
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* * event name is data-event property.
|
||||
* * layoutInfo is a summernote layout information.
|
||||
* * value is data-value property.
|
||||
*/
|
||||
pluginEvents: {},
|
||||
|
||||
plugins : []
|
||||
});
|
||||
|
||||
/**
|
||||
* @method addPlugin
|
||||
*
|
||||
* add Plugin in Summernote
|
||||
*
|
||||
* Summernote can make a own plugin.
|
||||
*
|
||||
* ### Define plugin
|
||||
* ```
|
||||
* // get template function
|
||||
* var tmpl = $.summernote.renderer.getTemplate();
|
||||
*
|
||||
* // add a button
|
||||
* $.summernote.addPlugin({
|
||||
* buttons : {
|
||||
* // "hello" is button's namespace.
|
||||
* "hello" : function(lang, options) {
|
||||
* // make icon button by template function
|
||||
* return tmpl.iconButton(options.iconPrefix + 'header', {
|
||||
* // callback function name when button clicked
|
||||
* event : 'hello',
|
||||
* // set data-value property
|
||||
* value : 'hello',
|
||||
* hide : true
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* },
|
||||
*
|
||||
* events : {
|
||||
* "hello" : function(layoutInfo, value) {
|
||||
* // here is event code
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
* ### Use a plugin in toolbar
|
||||
*
|
||||
* ```
|
||||
* $("#editor").summernote({
|
||||
* ...
|
||||
* toolbar : [
|
||||
* // display hello plugin in toolbar
|
||||
* ['group', [ 'hello' ]]
|
||||
* ]
|
||||
* ...
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param {Object} plugin
|
||||
* @param {Object} [plugin.buttons] define plugin button. for detail, see to Renderer.addButtonInfo
|
||||
* @param {Object} [plugin.dialogs] define plugin dialog. for detail, see to Renderer.addDialogInfo
|
||||
* @param {Object} [plugin.events] add event in $.summernote.pluginEvents
|
||||
* @param {Object} [plugin.langs] update $.summernote.lang
|
||||
* @param {Object} [plugin.options] update $.summernote.options
|
||||
*/
|
||||
$.summernote.addPlugin = function (plugin) {
|
||||
|
||||
// save plugin list
|
||||
$.summernote.plugins.push(plugin);
|
||||
|
||||
if (plugin.buttons) {
|
||||
$.each(plugin.buttons, function (name, button) {
|
||||
renderer.addButtonInfo(name, button);
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.dialogs) {
|
||||
$.each(plugin.dialogs, function (name, dialog) {
|
||||
renderer.addDialogInfo(name, dialog);
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.events) {
|
||||
$.each(plugin.events, function (name, event) {
|
||||
$.summernote.pluginEvents[name] = event;
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.langs) {
|
||||
$.each(plugin.langs, function (locale, lang) {
|
||||
if ($.summernote.lang[locale]) {
|
||||
$.extend($.summernote.lang[locale], lang);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.options) {
|
||||
$.extend($.summernote.options, plugin.options);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* extend $.fn
|
||||
*/
|
||||
$.fn.extend({
|
||||
/**
|
||||
* @method
|
||||
* Initialize summernote
|
||||
* - create editor layout and attach Mouse and keyboard events.
|
||||
*
|
||||
* ```
|
||||
* $("#summernote").summernote( { options ..} );
|
||||
* ```
|
||||
*
|
||||
* @member $.fn
|
||||
* @param {Object|String} options reference to $.summernote.options
|
||||
* @return {this}
|
||||
*/
|
||||
summernote: function () {
|
||||
// check first argument's type
|
||||
// - {String}: External API call {{module}}.{{method}}
|
||||
// - {Object}: init options
|
||||
var type = $.type(list.head(arguments));
|
||||
var isExternalAPICalled = type === 'string';
|
||||
var hasInitOptions = type === 'object';
|
||||
|
||||
// extend default options with custom user options
|
||||
var options = hasInitOptions ? list.head(arguments) : {};
|
||||
|
||||
options = $.extend({}, $.summernote.options, options);
|
||||
options.icons = $.extend({}, $.summernote.options.icons, options.icons);
|
||||
|
||||
// Include langInfo in options for later use, e.g. for image drag-n-drop
|
||||
// Setup language info with en-US as default
|
||||
options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
|
||||
|
||||
// override plugin options
|
||||
if (!isExternalAPICalled && hasInitOptions) {
|
||||
for (var i = 0, len = $.summernote.plugins.length; i < len; i++) {
|
||||
var plugin = $.summernote.plugins[i];
|
||||
|
||||
if (options.plugin[plugin.name]) {
|
||||
$.summernote.plugins[i] = $.extend(true, plugin, options.plugin[plugin.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.each(function (idx, holder) {
|
||||
var $holder = $(holder);
|
||||
|
||||
// if layout isn't created yet, createLayout and attach events
|
||||
if (!renderer.hasNoteEditor($holder)) {
|
||||
renderer.createLayout($holder, options);
|
||||
|
||||
var layoutInfo = renderer.layoutInfoFromHolder($holder);
|
||||
$holder.data('layoutInfo', layoutInfo);
|
||||
|
||||
eventHandler.attach(layoutInfo, options);
|
||||
eventHandler.attachCustomEvent(layoutInfo, options);
|
||||
}
|
||||
});
|
||||
|
||||
var $first = this.first();
|
||||
if ($first.length) {
|
||||
var layoutInfo = renderer.layoutInfoFromHolder($first);
|
||||
|
||||
// external API
|
||||
if (isExternalAPICalled) {
|
||||
var moduleAndMethod = list.head(list.from(arguments));
|
||||
var args = list.tail(list.from(arguments));
|
||||
|
||||
// TODO now external API only works for editor
|
||||
var params = [moduleAndMethod, layoutInfo.editable()].concat(args);
|
||||
return eventHandler.invoke.apply(eventHandler, params);
|
||||
} else if (options.focus) {
|
||||
// focus on first editable element for initialize editor
|
||||
layoutInfo.editable().focus();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*
|
||||
* get the HTML contents of note or set the HTML contents of note.
|
||||
*
|
||||
* * get contents
|
||||
* ```
|
||||
* var content = $("#summernote").code();
|
||||
* ```
|
||||
* * set contents
|
||||
*
|
||||
* ```
|
||||
* $("#summernote").code(html);
|
||||
* ```
|
||||
*
|
||||
* @member $.fn
|
||||
* @param {String} [html] - HTML contents(optional, set)
|
||||
* @return {this|String} - context(set) or HTML contents of note(get).
|
||||
*/
|
||||
code: function (html) {
|
||||
// get the HTML contents of note
|
||||
if (html === undefined) {
|
||||
var $holder = this.first();
|
||||
if (!$holder.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var layoutInfo = renderer.layoutInfoFromHolder($holder);
|
||||
var $editable = layoutInfo && layoutInfo.editable();
|
||||
|
||||
if ($editable && $editable.length) {
|
||||
var isCodeview = eventHandler.invoke('codeview.isActivated', layoutInfo);
|
||||
eventHandler.invoke('codeview.sync', layoutInfo);
|
||||
return isCodeview ? layoutInfo.codable().val() :
|
||||
layoutInfo.editable().html();
|
||||
}
|
||||
return dom.value($holder);
|
||||
}
|
||||
|
||||
// set the HTML contents of note
|
||||
this.each(function (i, holder) {
|
||||
var layoutInfo = renderer.layoutInfoFromHolder($(holder));
|
||||
var $editable = layoutInfo && layoutInfo.editable();
|
||||
if ($editable) {
|
||||
$editable.html(html);
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*
|
||||
* destroy Editor Layout and detach Key and Mouse Event
|
||||
*
|
||||
* @member $.fn
|
||||
* @return {this}
|
||||
*/
|
||||
destroy: function () {
|
||||
this.each(function (idx, holder) {
|
||||
var $holder = $(holder);
|
||||
|
||||
if (!renderer.hasNoteEditor($holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var info = renderer.layoutInfoFromHolder($holder);
|
||||
var options = info.editor().data('options');
|
||||
|
||||
eventHandler.detach(info, options);
|
||||
renderer.removeLayout($holder, info, options);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,467 +0,0 @@
|
||||
/* Theme Variables
|
||||
------------------------------------------*/
|
||||
@border-color: #a9a9a9;
|
||||
@background-color: #f5f5f5;
|
||||
|
||||
/* Frame Mode Layout
|
||||
------------------------------------------*/
|
||||
.note-editor {
|
||||
border: 1px solid @border-color;
|
||||
position: relative;
|
||||
/* overflow: hidden; ODOO: removed for embedded editor */
|
||||
|
||||
/* dropzone */
|
||||
@dropzone-color: lightskyblue;
|
||||
@dropzone-active-color: darken(@dropzone-color, 30);
|
||||
.note-dropzone {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
color: @dropzone-color;
|
||||
background-color: white;
|
||||
opacity: 0.95;
|
||||
pointer-event: none;
|
||||
|
||||
.note-dropzone-message {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.hover {
|
||||
color: @dropzone-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.dragover .note-dropzone {
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* codeview mode */
|
||||
&.codeview {
|
||||
.note-editing-area {
|
||||
.note-editable {
|
||||
display: none;
|
||||
}
|
||||
.note-codable {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* fullscreen mode */
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1050; /* bs3 modal-backdrop: 1030, bs2: 1040 */
|
||||
.note-editable {
|
||||
background-color: white;
|
||||
}
|
||||
.note-resizebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-editing-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* editable */
|
||||
.note-editable {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
|
||||
&[contenteditable=true]:empty:not(:focus):before {
|
||||
content:attr(data-placeholder);
|
||||
}
|
||||
&[contenteditable="false"] {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
}
|
||||
|
||||
/* codeable */
|
||||
.note-codable {
|
||||
display: none;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
font-family: Menlo, Monaco, monospace, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
background-color: #222;
|
||||
resize: none;
|
||||
|
||||
/* override BS2 default style */
|
||||
box-sizing: border-box;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* statusbar */
|
||||
.note-statusbar {
|
||||
background-color: @background-color;
|
||||
.note-resizebar {
|
||||
padding-top: 1px;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
cursor: ns-resize;
|
||||
.note-icon-bar {
|
||||
width: 20px;
|
||||
margin: 1px auto;
|
||||
border-top: 1px solid @border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Air Mode Layout
|
||||
------------------------------------------*/
|
||||
.note-air-editor {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Popover
|
||||
------------------------------------------*/
|
||||
.note-popover .popover {
|
||||
max-width: none;
|
||||
.popover-body {
|
||||
a {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; /* for FF */
|
||||
vertical-align: middle; /* for FF */
|
||||
}
|
||||
}
|
||||
& .arrow {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Popover and Toolbar (Button container)
|
||||
------------------------------------------*/
|
||||
.note-popover .popover .popover-body, .panel-heading.note-toolbar {
|
||||
margin: 0;
|
||||
padding: 0 0 5px 5px;
|
||||
|
||||
&>.btn-group {
|
||||
margin-top: 5px;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
.note-table {
|
||||
min-width: 0;
|
||||
padding: 5px;
|
||||
.note-dimension-picker {
|
||||
font-size: 18px;
|
||||
.note-dimension-picker-mousecatcher {
|
||||
position: absolute !important;
|
||||
z-index: 3;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-dimension-picker-unhighlighted {
|
||||
position: relative !important;
|
||||
z-index: 1;
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIj4+Pjp6ekKlAqjAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKhmnaJzPAAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat;
|
||||
}
|
||||
.note-dimension-picker-highlighted {
|
||||
position: absolute !important;
|
||||
z-index: 2;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIjd6vvD2f9LKLW+AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKwNDEVT0AAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-style {
|
||||
h1, h2, h3, h4, h5, h6, blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.note-color {
|
||||
.dropdown-toggle {
|
||||
width: 20px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
min-width: 340px;
|
||||
.btn-group {
|
||||
margin: 0;
|
||||
&:first-child {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.note-palette-title {
|
||||
font-size: 12px;
|
||||
margin: 2px 7px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.note-color-reset {
|
||||
font-size: 11px;
|
||||
margin: 3px;
|
||||
padding: 0 3px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.note-color-row {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.note-color-reset:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-para {
|
||||
.dropdown-menu {
|
||||
min-width: 216px;
|
||||
padding: 5px;
|
||||
&>div:first-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* dropdown-menu for toolbar and popover */
|
||||
.dropdown-menu {
|
||||
min-width: 90px;
|
||||
|
||||
/* dropdown-menu right position */
|
||||
/* http://forrst.com/posts/Bootstrap_right_positioned_dropdown-2KB */
|
||||
&.right {
|
||||
right: 0;
|
||||
left: auto;
|
||||
&::before { right: 9px; left: auto !important; }
|
||||
&::after { right: 10px; left: auto !important; }
|
||||
}
|
||||
/* dropdown-menu for selectbox */
|
||||
&.note-check {
|
||||
li a i {
|
||||
color: deepskyblue;
|
||||
visibility: hidden;
|
||||
}
|
||||
li a.checked i {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.note-fontsize-10 {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* color palette for toolbar and popover */
|
||||
.note-color-palette {
|
||||
line-height: 1;
|
||||
div {
|
||||
.note-color-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
.note-color-btn:hover {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog
|
||||
------------------------------------------*/
|
||||
.note-dialog {
|
||||
&>div {
|
||||
display: none; /* BS2's hide pacth. */
|
||||
}
|
||||
.form-group { /* overwrite BS's form-horizontal minus margins */
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.note-modal-form {
|
||||
margin: 0; /* overwrite BS2's form margin bottom */
|
||||
}
|
||||
.note-image-dialog {
|
||||
.note-dropzone {
|
||||
min-height: 100px;
|
||||
font-size: 30px;
|
||||
line-height: 4; /* vertical-align */
|
||||
color: lightgray;
|
||||
text-align: center;
|
||||
border: 4px dashed lightgray;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.note-help-dialog {
|
||||
@note-shortcut-hl: #dd0;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
background-color: #222 !important;
|
||||
opacity: 0.9;
|
||||
.modal-content {
|
||||
background: transparent;
|
||||
border: 1px solid white;
|
||||
box-shadow: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* BS2's background pacth. */
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: white 1px solid;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 14px;
|
||||
color: @note-shortcut-hl;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.note-shortcut {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&-row {
|
||||
margin-right: -5px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
&-col {
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: @note-shortcut-hl;
|
||||
}
|
||||
|
||||
&-key {
|
||||
font-family: "Courier New";
|
||||
color: @note-shortcut-hl;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Handle
|
||||
------------------------------------------*/
|
||||
.note-handle {
|
||||
/* control selection */
|
||||
.note-control-selection {
|
||||
position: absolute;
|
||||
display: none;
|
||||
border: 1px solid black;
|
||||
&>div { position: absolute; }
|
||||
|
||||
.note-control-selection-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.note-control-handle {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.note-control-holder {
|
||||
.note-control-handle;
|
||||
}
|
||||
|
||||
.note-control-sizing {
|
||||
.note-control-handle;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.note-control-nw {
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.note-control-ne {
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.note-control-sw {
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.note-control-se {
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.note-control-se.note-control-holder {
|
||||
cursor: default;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.note-control-selection-info {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
color: white;
|
||||
background-color: black;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
+93
-16
@@ -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,
|
||||
@@ -0,0 +1,476 @@
|
||||
odoo.define('web_editor.field.html', function (require) {
|
||||
'use strict';
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var core = require('web.core');
|
||||
var Wysiwyg = require('web_editor.wysiwyg');
|
||||
var field_registry = require('web.field_registry');
|
||||
|
||||
var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
*/
|
||||
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();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @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 () {
|
||||
var self = this;
|
||||
if (!this.wysiwyg) {
|
||||
return this._super();
|
||||
}
|
||||
var _super = this._super.bind(this);
|
||||
return this.wysiwyg.save().then(function (isDirty, html) {
|
||||
self._isDirty = isDirty;
|
||||
_super();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
isSet: function () {
|
||||
return this.value && this.value !== "<p><br/></p>" && this.value.match(/\S/);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
return this.$target || $();
|
||||
},
|
||||
/**
|
||||
* Do not re-render this field if it was the origin of the onchange call.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
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.wysiwyg.setValue(value);
|
||||
} else {
|
||||
this.$content.html(value);
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getValue: function () {
|
||||
var value = this.$target.val();
|
||||
if (this.nodeOptions.wrapper) {
|
||||
return this._unWrap(value);
|
||||
}
|
||||
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 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.
|
||||
* The new documents being attached to the email, they will not be erased by the CRON
|
||||
* when closing the wizard.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} attachments
|
||||
*/
|
||||
_onAttachmentChange: function (attachments) {
|
||||
if (!this.fieldNameAttachment) {
|
||||
return;
|
||||
}
|
||||
this.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: _.object([this.fieldNameAttachment], [{
|
||||
operation: 'ADD_M2M',
|
||||
ids: attachments
|
||||
}])
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
var value = this._textToHtml(this.value);
|
||||
if (this.nodeOptions.wrapper) {
|
||||
value = this._wrap(value);
|
||||
}
|
||||
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();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderReadonly: function () {
|
||||
var self = this;
|
||||
var value = this._textToHtml(this.value);
|
||||
if (this.nodeOptions.wrapper) {
|
||||
value = this._wrap(value);
|
||||
}
|
||||
|
||||
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
|
||||
* @param {string} text
|
||||
* @returns {string} the text converted to html
|
||||
*/
|
||||
_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) {
|
||||
if (value.match(/^\s*$/)) {
|
||||
value = '<p><br/></p>';
|
||||
} else {
|
||||
value = "<p>" + value.split(/<br\/?>/).join("<br/></p><p>") + "</p>";
|
||||
value = value
|
||||
.replace(/<p><\/p>/g, '')
|
||||
.replace('<p><p>', '<p>')
|
||||
.replace('<p><p ', '<p ')
|
||||
.replace('</p></p>', '</p>');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* Move HTML contents out of their wrapper.
|
||||
*
|
||||
* @private
|
||||
* @param {string} html content
|
||||
* @returns {string} html content
|
||||
*/
|
||||
_unWrap: function (html) {
|
||||
var $wrapper = $(html).find('#wrapper');
|
||||
return $wrapper.length ? $wrapper.html() : html;
|
||||
},
|
||||
/**
|
||||
* 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');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method called when wysiwyg triggers a change.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
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');
|
||||
}
|
||||
});
|
||||
$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', FieldHtml);
|
||||
|
||||
|
||||
return FieldHtml;
|
||||
});
|
||||
@@ -1,532 +0,0 @@
|
||||
odoo.define('web_editor.backend', function (require) {
|
||||
'use strict';
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
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
|
||||
* improved by odoo.
|
||||
*
|
||||
*/
|
||||
var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
className: 'oe_form_field oe_form_field_html_text',
|
||||
supportedFieldTypes: ['html'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
new SummernoteManager(this);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Summernote 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);
|
||||
}
|
||||
if (this._getValue() !== this.value) {
|
||||
this._isDirty = true;
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
isSet: function () {
|
||||
return this.value && this.value !== "<p><br/></p>" && this.value.match(/\S/);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
return this.$content || this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Do not re-render this field if it was the origin of the onchange call.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
reset: function (record, event) {
|
||||
this._reset(record, event);
|
||||
if (!event || event.target !== this) {
|
||||
if (this.mode === 'edit') {
|
||||
this.$content.html(this._textToHtml(this.value));
|
||||
} else {
|
||||
this._renderReadonly();
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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);
|
||||
}
|
||||
return this.$content.html();
|
||||
},
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
_onUpload: 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);
|
||||
this.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: _.object([this.fieldNameAttachment], [{
|
||||
operation: 'ADD_M2M',
|
||||
ids: attachments
|
||||
}])
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
// 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.$('.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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 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');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} text
|
||||
* @returns {string} the text converted to html
|
||||
*/
|
||||
_textToHtml: function (text) {
|
||||
var value = text || "";
|
||||
try {
|
||||
$(text)[0].innerHTML; // crashes if text isn't html
|
||||
} catch (e) {
|
||||
if (value.match(/^\s*$/)) {
|
||||
value = '<p><br/></p>';
|
||||
} else {
|
||||
value = "<p>" + value.split(/<br\/?>/).join("<br/></p><p>") + "</p>";
|
||||
value = value
|
||||
.replace(/<p><\/p>/g, '')
|
||||
.replace('<p><p>', '<p>')
|
||||
.replace('<p><p ', '<p ')
|
||||
.replace('</p></p>', '</p>');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_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 $();
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
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"];
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set the value when the widget is fully loaded (content + editor).
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
commitChanges: function () {
|
||||
var self = this;
|
||||
var result = this._super.bind(this, arguments);
|
||||
if (this.mode === 'readonly') {
|
||||
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 $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());
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
field_registry
|
||||
.add('html', FieldTextHtmlSimple)
|
||||
.add('html_frame', FieldTextHtml);
|
||||
|
||||
return {
|
||||
FieldTextHtmlSimple: FieldTextHtmlSimple,
|
||||
FieldTextHtml: FieldTextHtml,
|
||||
};
|
||||
});
|
||||
+2
-1
@@ -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,286 +0,0 @@
|
||||
odoo.define('web_editor.editor', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var core = require('web.core');
|
||||
var rte = require('web_editor.rte');
|
||||
var snippetsEditor = require('web_editor.snippet.editor');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var EditorMenuBar = Widget.extend({
|
||||
template: 'web_editor.editorbar',
|
||||
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
|
||||
events: {
|
||||
'click button[data-action=save]': '_onSaveClick',
|
||||
'click button[data-action=cancel]': '_onCancelClick',
|
||||
},
|
||||
custom_events: {
|
||||
request_history_undo_record: '_onHistoryUndoRecordRequest',
|
||||
request_save: '_onSaveRequest',
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes RTE and snippets menu.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent) {
|
||||
var self = this;
|
||||
var res = this._super.apply(this, arguments);
|
||||
this.rte = new rte.Class(this);
|
||||
this.rte.on('rte:start', this, function () {
|
||||
self.trigger('rte:start');
|
||||
});
|
||||
|
||||
// Snippets edition
|
||||
var $editable = this.rte.editable();
|
||||
window.__EditorMenuBar_$editable = $editable; // TODO remove this hack asap
|
||||
this.snippetsMenu = new snippetsEditor.Class(this, $editable);
|
||||
|
||||
return res;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
|
||||
core.bus.on('editor_save_request', this, this.save);
|
||||
core.bus.on('editor_discard_request', this, this.cancel);
|
||||
|
||||
$('.dropdown-toggle').dropdown();
|
||||
|
||||
$(document).on('keyup', function (event) {
|
||||
if ((event.keyCode === 8 || event.keyCode === 46)) {
|
||||
var $target = $(event.target).closest('.o_editable');
|
||||
if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) {
|
||||
$target.empty();
|
||||
}
|
||||
}
|
||||
});
|
||||
$(document).on('click', '.note-editable', function (ev) {
|
||||
ev.preventDefault();
|
||||
});
|
||||
$(document).on('submit', '.note-editable form .btn', function (ev) {
|
||||
ev.preventDefault(); // Disable form submition in editable mode
|
||||
});
|
||||
$(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
|
||||
// Prevent dropdown closing when a contenteditable children is focused
|
||||
if (ev.originalEvent
|
||||
&& $(ev.target).has(ev.originalEvent.target).length
|
||||
&& $(ev.originalEvent.target).is('[contenteditable]')) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.rte.start();
|
||||
|
||||
var flag = false;
|
||||
window.onbeforeunload = function (event) {
|
||||
if (rte.history.getEditableHasUndo().length && !flag) {
|
||||
flag = true;
|
||||
_.defer(function () { flag=false; });
|
||||
return _t('This document is not saved!');
|
||||
}
|
||||
};
|
||||
|
||||
// Snippets menu
|
||||
defs.push(this.snippetsMenu.insertAfter(this.$el));
|
||||
this.rte.editable().find('*').off('mousedown mouseup click');
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
self.trigger_up('edit_mode');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
core.bus.off('editor_save_request', this, this._onSaveRequest);
|
||||
core.bus.off('editor_discard_request', this, this._onDiscardRequest);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asks the user if he really wants to discard its changes (if there are
|
||||
* some of them), then simply reload the page if he wants to.
|
||||
*
|
||||
* @param {boolean} [reload=true]
|
||||
* true if the page has to be reloaded when the user answers yes
|
||||
* (do nothing otherwise but add this to allow class extension)
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
cancel: function (reload) {
|
||||
var self = this;
|
||||
var def = $.Deferred();
|
||||
if (!rte.history.getEditableHasUndo().length) {
|
||||
def.resolve();
|
||||
} else {
|
||||
var confirm = Dialog.confirm(this, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), {
|
||||
confirm_callback: def.resolve.bind(def),
|
||||
});
|
||||
confirm.on('closed', def, def.reject);
|
||||
}
|
||||
return def.then(function () {
|
||||
if (reload !== false) {
|
||||
window.onbeforeunload = null;
|
||||
return self._reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Asks the snippets to clean themself, then saves the page, then reloads it
|
||||
* if asked to.
|
||||
*
|
||||
* @param {boolean} [reload=true]
|
||||
* true if the page has to be reloaded after the save
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
save: function (reload) {
|
||||
var self = this;
|
||||
var defs = [];
|
||||
this.trigger_up('ready_to_save', {defs: defs});
|
||||
return $.when.apply($, defs).then(function () {
|
||||
self.snippetsMenu.cleanForSave();
|
||||
return self._saveCroppedImages();
|
||||
}).then(function () {
|
||||
return self.rte.save();
|
||||
}).then(function () {
|
||||
if (reload !== false) {
|
||||
return self._reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reloads the page in non-editable mode, with the right scrolling.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred} (never resolved, the page is reloading anyway)
|
||||
*/
|
||||
_reload: function () {
|
||||
window.location.hash = 'scrollTop=' + window.document.body.scrollTop;
|
||||
if (window.location.search.indexOf('enable_editor') >= 0) {
|
||||
window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
|
||||
} else {
|
||||
window.location.reload(true);
|
||||
}
|
||||
return $.Deferred();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_saveCroppedImages: function () {
|
||||
var self = this;
|
||||
var defs = _.map(this.rte.editable().find('.o_cropped_img_to_save'), function (croppedImg) {
|
||||
var $croppedImg = $(croppedImg);
|
||||
$croppedImg.removeClass('o_cropped_img_to_save');
|
||||
|
||||
var resModel = $croppedImg.data('crop:resModel');
|
||||
var resID = $croppedImg.data('crop:resID');
|
||||
var cropID = $croppedImg.data('crop:id');
|
||||
var mimetype = $croppedImg.data('crop:mimetype');
|
||||
var originalSrc = $croppedImg.data('crop:originalSrc');
|
||||
|
||||
var datas = $croppedImg.attr('src').split(',')[1];
|
||||
|
||||
if (!cropID) {
|
||||
var name = originalSrc + '.crop';
|
||||
return self._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'create',
|
||||
args: [{
|
||||
res_model: resModel,
|
||||
res_id: resID,
|
||||
name: name,
|
||||
datas_fname: name,
|
||||
datas: datas,
|
||||
mimetype: mimetype,
|
||||
url: originalSrc, // To save the original image that was cropped
|
||||
}],
|
||||
}).then(function (attachmentID) {
|
||||
return self._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'generate_access_token',
|
||||
args: [[attachmentID]],
|
||||
}).then(function (access_token) {
|
||||
$croppedImg.attr('src', '/web/image/' + attachmentID + '?access_token=' + access_token[0]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return self._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'write',
|
||||
args: [[cropID], {datas: datas}],
|
||||
});
|
||||
}
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the "Discard" button is clicked -> discards the changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onCancelClick: function () {
|
||||
this.cancel();
|
||||
},
|
||||
/**
|
||||
* Called when an element askes to record an history undo -> records it.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onHistoryUndoRecordRequest: function (ev) {
|
||||
this.rte.historyRecordUndo(ev.data.$target, ev.data.event);
|
||||
},
|
||||
/**
|
||||
* Called when the "Save" button is clicked -> saves the changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSaveClick: function () {
|
||||
this.save();
|
||||
},
|
||||
/**
|
||||
* Called when a discard request is received -> discard the page content
|
||||
* changes.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onDiscardRequest: function (ev) {
|
||||
this.cancel(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
|
||||
},
|
||||
/**
|
||||
* Called when a save request is received -> saves the page content.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onSaveRequest: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.save(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
Class: EditorMenuBar,
|
||||
};
|
||||
});
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user