[IMP] web_editor, mass_mailing, website: restore the 12.0 editor

This PR reverts the saas-12.2 editor by restoring the 12.0 editor.

The saas-12.2 editor was an intermediate work between the previous
editor of Odoo 12 and the new one slated for Odoo 13. It was unstable
but it was expected to be replaced by the new editor of version 13.

However, since the new editor has been postponed to Odoo 14, the
saas-12.2 one would have been staying in Odoo 13, which would have
been a nightmare to maintain. To avoid this outcome, we chose to
put back the 12.0 editor in its place.

This has the particular advantage that Odoo 13 will share the same
bugfixes as Odoo 12 and 11 as they all run under the same core
editor, while the saas-12.2 one would have been an entirely
different beast to maintain.

Despite its title, this PR is not a revert of PR 29775 per se, as
we are using the wysiwyg abstraction layer that was introduced in
that PR.

The goal of the wysiwyg abstraction layer is to provide an
interface for other modules that want to use the editor feature
such that the editor core itself can be changed without requiring
extensive changes in the other modules.

This PR uses the wysiwyg abstraction layer to restore the 12.0
editor with minimal changes to the modules that use the editor.

closes odoo/odoo#35677

Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Robot Odoo
2019-08-22 09:56:16 +02:00
committed by GitHub
121 changed files with 14723 additions and 28535 deletions
@@ -92,35 +92,6 @@ var MassMailingFieldHtml = FieldHtml.extend({
// 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.
*
@@ -293,12 +264,12 @@ var MassMailingFieldHtml = FieldHtml.extend({
} 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();
$contents = this.$content.find('.o_editable').contents();
}
$newWrapperContent.append($contents);
this._switchImages(themeParams, $newWrapperContent);
this.$content.find('.note-editable').empty().append($newLayout);
this.$content.find('.o_editable').empty().append($newLayout);
$old_layout.remove();
if (firstChoice) {
@@ -326,6 +297,9 @@ var MassMailingFieldHtml = FieldHtml.extend({
if (this._isFromInline) {
this._fromInline();
}
if (this.snippetsLoaded) {
this._onSnippetsLoaded(this.snippetsLoaded);
}
this._super();
},
/**
@@ -334,6 +308,10 @@ var MassMailingFieldHtml = FieldHtml.extend({
*/
_onSnippetsLoaded: function (ev) {
var self = this;
if (!this.$content) {
this.snippetsLoaded = ev;
return;
}
var $snippetsSideBar = ev.data;
var $themes = $snippetsSideBar.find("#email_designer_themes").children();
var $snippets = $snippetsSideBar.find(".oe_snippet");
@@ -341,7 +319,6 @@ var MassMailingFieldHtml = FieldHtml.extend({
if (config.device.isMobile) {
$snippetsSideBar.hide();
console.log(this.$content[0]);
this.$content.attr('style', 'padding-left: 0px !important');
}
@@ -35,17 +35,23 @@
.dropdown-menu {
position: absolute;
bottom: -1px;
bottom: -2px;
top: -3px !important;
left: -5px !important;
right: 3px !important;
right: -2px !important;
margin: 0;
border-radius: 0;
overflow: auto;
background-color: $o-wysiwyg-bg-color;
z-index: 1050;
.dropdown-item {
&.show {
bottom: 31px !important;
top: -2px !important;
right: 3px !important;
}
.dropdown-item {
padding: 10px 10px;
&:first-child {
display: none;
@@ -182,6 +188,13 @@ body.o_force_mail_theme_choice {
overflow: initial;
}
#web_editor-toolbars .popover .note-color {
.note-back-color-preview .dropdown-toggle,
.note-fore-color-preview .dropdown-toggle {
height: $o-wysiwyg-toolbar-height - 1;
}
}
.oe_structure {
width: 100%;
}
@@ -56,7 +56,6 @@ td:not([align]) {
> .o_mail_wrapper {
width: 100%;
margin: 24px 0;
border-collapse: separate; // Allow the first and last td to have the
// same width
@@ -64,7 +64,7 @@ QUnit.module('field html', {
}, function () {
QUnit.test('save arch and html', async function (assert) {
assert.expect(6);
assert.expect(4);
var form = await testUtils.createView({
View: FormView,
@@ -85,19 +85,6 @@ QUnit.test('save arch and html', async function (assert) {
' />'+
'</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);
},
});
var $fieldReadonly = form.$('.oe_form_field[name="body_html"]');
var $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
@@ -113,79 +100,6 @@ QUnit.test('save arch and html', async function (assert) {
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');
await testUtils.nextTick();
await $iframe.data('loadDef');
await testUtils.nextTick();
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
await testUtils.nextTick();
await testUtils.dom.triggerEvents($content.find('.note-toolbar .note-font .note-btn-bold'),['mousedown','click']);
await testUtils.dom.click(form.$buttons.find('.o_form_button_save'));
form.destroy();
});
QUnit.test('save and edit arch wysiwyg', async function (assert) {
assert.expect(4);
testUtils.mock.patch(FieldHtml, {
commitChanges: function () {
assert.step("FieldHtml");
return this._super.apply(this, arguments);
},
});
testUtils.mock.patch(MassMailingFieldHtml, {
commitChanges: function () {
assert.step("MassMailingFieldHtml");
return this._super.apply(this, arguments);
},
});
var form = await testUtils.createView({
View: FormView,
model: 'mailing.mailing',
data: this.data,
arch: '<form>' +
' <header style="min-height:31px;">' +
' <button name="action_put_in_queue" type="object" class="oe_highlight" string="Send Now"/>' +
' </header>' +
' <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,
intercepts: {
execute_action: function (event) {
event.data.on_success();
},
},
});
await testUtils.dom.click($('[name=action_put_in_queue]'));
await testUtils.form.clickEdit(form);
var $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
var $iframe = $fieldEdit.find('iframe');
await $iframe.data('loadDef');
await testUtils.nextTick();
await testUtils.dom.click($('[name=action_put_in_queue]'));
assert.verifySteps(["MassMailingFieldHtml", "FieldHtml", "MassMailingFieldHtml"]);
testUtils.mock.unpatch(FieldHtml);
testUtils.mock.unpatch(MassMailingFieldHtml);
form.destroy();
});
-3
View File
@@ -32,9 +32,6 @@
</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="web_editor.assets_wysiwyg" 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>
+2
View File
@@ -147,6 +147,8 @@ class Web_Editor(http.Controller):
allSelected = False
node = ul.getprevious()
if node is None:
node = ul.getparent().getprevious()
if node is not None and node.tag == 'li':
self._update_checklist_recursive(node, allSelected, ancestors=True)
@@ -1,16 +0,0 @@
.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;
}
@@ -1,292 +0,0 @@
(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'
}
}
}
});
}));
@@ -1,81 +0,0 @@
(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;
};
}
});
}));
@@ -1,312 +0,0 @@
(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 = [
'&quot;', '&amp;', '&lt;', '&gt;', '&iexcl;', '&cent;',
'&pound;', '&curren;', '&yen;', '&brvbar;', '&sect;',
'&uml;', '&copy;', '&ordf;', '&laquo;', '&not;',
'&reg;', '&macr;', '&deg;', '&plusmn;', '&sup2;',
'&sup3;', '&acute;', '&micro;', '&para;', '&middot;',
'&cedil;', '&sup1;', '&ordm;', '&raquo;', '&frac14;',
'&frac12;', '&frac34;', '&iquest;', '&times;', '&divide;',
'&fnof;', '&circ;', '&tilde;', '&ndash;', '&mdash;',
'&lsquo;', '&rsquo;', '&sbquo;', '&ldquo;', '&rdquo;',
'&bdquo;', '&dagger;', '&Dagger;', '&bull;', '&hellip;',
'&permil;', '&prime;', '&Prime;', '&lsaquo;', '&rsaquo;',
'&oline;', '&frasl;', '&euro;', '&image;', '&weierp;',
'&real;', '&trade;', '&alefsym;', '&larr;', '&uarr;',
'&rarr;', '&darr;', '&harr;', '&crarr;', '&lArr;',
'&uArr;', '&rArr;', '&dArr;', '&hArr;', '&forall;',
'&part;', '&exist;', '&empty;', '&nabla;', '&isin;',
'&notin;', '&ni;', '&prod;', '&sum;', '&minus;',
'&lowast;', '&radic;', '&prop;', '&infin;', '&ang;',
'&and;', '&or;', '&cap;', '&cup;', '&int;',
'&there4;', '&sim;', '&cong;', '&asymp;', '&ne;',
'&equiv;', '&le;', '&ge;', '&sub;', '&sup;',
'&nsub;', '&sube;', '&supe;', '&oplus;', '&otimes;',
'&perp;', '&sdot;', '&lceil;', '&rceil;', '&lfloor;',
'&rfloor;', '&loz;', '&spades;', '&clubs;', '&hearts;',
'&diams;'
];
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);
});
};
}
});
}));
@@ -0,0 +1,447 @@
/* 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;
}
@@ -0,0 +1,569 @@
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
@@ -0,0 +1,42 @@
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'
}
});
});
@@ -0,0 +1,170 @@
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;
});
@@ -0,0 +1,68 @@
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
@@ -0,0 +1,130 @@
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;
});
@@ -0,0 +1,96 @@
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;
});
@@ -0,0 +1,191 @@
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;
});
@@ -0,0 +1,790 @@
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;
});
@@ -0,0 +1,416 @@
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;
});
@@ -0,0 +1,235 @@
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;
});
@@ -0,0 +1,118 @@
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;
});
@@ -0,0 +1,161 @@
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;
});
@@ -0,0 +1,52 @@
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;
});
@@ -0,0 +1,86 @@
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;
});
@@ -0,0 +1,21 @@
/**
* 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';
@@ -0,0 +1,169 @@
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;
});
@@ -0,0 +1,41 @@
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;
});
@@ -0,0 +1,137 @@
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;
});
@@ -0,0 +1,98 @@
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;
});
@@ -0,0 +1,865 @@
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;
});
@@ -0,0 +1,51 @@
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;
});
@@ -0,0 +1,101 @@
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;
});
@@ -0,0 +1,35 @@
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;
});
@@ -0,0 +1,110 @@
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;
});
@@ -0,0 +1,129 @@
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;
});
@@ -0,0 +1,129 @@
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;
});
@@ -0,0 +1,44 @@
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;
});
@@ -0,0 +1,101 @@
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;
});
@@ -0,0 +1 @@
}));
@@ -0,0 +1,328 @@
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;
}
});
});
@@ -0,0 +1,467 @@
/* 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
@@ -396,7 +396,7 @@ FieldHtml.include({
* @override
*/
commitChanges: function () {
if (this.nodeOptions['style-inline'] && this.isRendered) {
if (this.nodeOptions['style-inline'] && this.mode === "edit") {
this._toInline();
}
return this._super();
@@ -452,7 +452,7 @@ FieldHtml.include({
* @override
*/
_onLoadWysiwyg: function () {
if (this.nodeOptions['style-inline']) {
if (this.nodeOptions['style-inline'] && this.mode === "edit") {
this._fromInline();
}
this._super();
@@ -467,4 +467,4 @@ return {
attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
linkImgToAttachmentThumbnail: linkImgToAttachmentThumbnail,
};
});
});
@@ -167,7 +167,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
// 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.$editor;
self.$content = self.wysiwyg.$editor.closest('body, odoo-wysiwyg-container');
self._onLoadWysiwyg();
self.isRendered = true;
});
@@ -179,9 +179,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
* @returns {Object}
*/
_getWysiwygOptions: function () {
var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return {
return Object.assign({}, this.nodeOptions, {
recordInfo: {
context: this.record.getContext(this.recordParams),
res_model: this.model,
@@ -192,26 +190,18 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
iframeCssAssets: this.nodeOptions.cssEdit,
snippets: this.nodeOptions.snippets,
tabSize: 0,
keyMap: {
pc: {
'TAB': null,
'SHIFT+TAB': null,
},
mac: {
'ENTER': iPhone ? null : 'insertParagraph',
'TAB': null,
'SHIFT+TAB': null,
},
},
tabsize: 0,
height: 180,
generateOptions: function (options) {
var para = _.find(options.toolbar, function (item) {
return item[0] === 'para';
});
para[1].splice(2, 0, 'checklist');
if (para && para[1] && para[1].indexOf('checklist') === -1) {
para[1].splice(2, 0, 'checklist');
}
return options;
},
};
});
},
/**
* trigger_up 'field_changed' add record into the "ir.attachment" field found in the view.
@@ -469,10 +459,9 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
'font-size': '15px',
position: 'absolute',
right: '+5px',
top: '+5px',
});
var $toolbar = this.$content.find('.note-toolbar');
$toolbar.css('position', 'relative');
$toolbar.append($button);
this.$el.append($button);
},
/**
* @private
+158
View File
@@ -0,0 +1,158 @@
odoo.define('web_editor.base', function (require) {
'use strict';
var ajax = require('web.ajax');
var session = require('web.session');
var domReady = new Promise(function(resolve) {
$(resolve);
});
return {
/**
* Retrieves all the CSS rules which match the given parser (Regex).
*
* @param {Regex} filter
* @returns {Object[]} Array of CSS rules descriptions (objects). A rule is
* defined by 3 values: 'selector', 'css' and 'names'. 'selector'
* is a string which contains the whole selector, 'css' is a string
* which contains the css properties and 'names' is an array of the
* first captured groups for each selector part. E.g.: if the
* filter is set to match .fa-* rules and capture the icon names,
* the rule:
* '.fa-alias1::before, .fa-alias2::before { hello: world; }'
* will be retrieved as
* {
* selector: '.fa-alias1::before, .fa-alias2::before',
* css: 'hello: world;',
* names: ['.fa-alias1', '.fa-alias2'],
* }
*/
cacheCssSelectors: {},
getCssSelectors: function (filter) {
if (this.cacheCssSelectors[filter]) {
return this.cacheCssSelectors[filter];
}
this.cacheCssSelectors[filter] = [];
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
var rules;
try {
// try...catch because Firefox not able to enumerate
// document.styleSheets[].cssRules[] for cross-domain
// stylesheets.
rules = sheets[i].rules || sheets[i].cssRules;
} catch (e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
if (!rules) {
continue;
}
for (var r = 0 ; r < rules.length ; r++) {
var selectorText = rules[r].selectorText;
if (!selectorText) {
continue;
}
var selectors = selectorText.split(/\s*,\s*/);
var data = null;
for (var s = 0; s < selectors.length; s++) {
var match = selectors[s].trim().match(filter);
if (!match) {
continue;
}
if (!data) {
data = {
selector: match[0],
css: rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, ''),
names: [match[1]]
};
} else {
data.selector += (', ' + match[0]);
data.names.push(match[1]);
}
}
if (data) {
this.cacheCssSelectors[filter].push(data);
}
}
}
return this.cacheCssSelectors[filter];
},
/**
* List of font icons to load by editor. The icons are displayed in the media
* editor and identified like font and image (can be colored, spinned, resized
* with fa classes).
* To add font, push a new object {base, parser}
*
* - base: class who appear on all fonts
* - parser: regular expression used to select all font in css stylesheets
*
* @type Array
*/
fontIcons: [{base: 'fa', parser: /\.(fa-(?:\w|-)+)::?before/i}],
/**
* Searches the fonts described by the @see fontIcons variable.
*/
computeFonts: _.once(function () {
var self = this;
_.each(this.fontIcons, function (data) {
data.cssData = self.getCssSelectors(data.parser);
data.alias = _.flatten(_.map(data.cssData, _.property('names')));
});
}),
/**
* If a widget needs to be instantiated on page loading, it needs to wait
* for appropriate resources to be loaded. This function returns a Promise
* which is resolved when the dom is ready, the session is bound
* (translations loaded) and the XML is loaded. This should however not be
* necessary anymore as widgets should not be parentless and should then be
* instantiated (directly or not) by the page main component (webclient,
* website root, editor bar, ...). The DOM will be ready then, the main
* component is in charge of waiting for the session and the XML can be
* lazy loaded thanks to the @see Widget.xmlDependencies key.
*
* @returns {Promise}
*/
ready: function () {
return Promise.all([domReady, session.is_bound, ajax.loadXML()]);
},
};
});
//==============================================================================
odoo.define('web_editor.context', function (require) {
'use strict';
function getContext(context) {
var html = document.documentElement;
return _.extend({
lang: (html.getAttribute('lang') || 'en_US').replace('-', '_'),
}, context || {});
}
function getExtraContext(context) {
var html = document.documentElement;
return _.extend(getContext(), {
editable: !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python
translatable: !!html.dataset.translatable,
edit_translations: !!html.dataset.edit_translations,
}, context || {});
}
return {
get: getContext,
getExtra: getExtraContext,
};
});
//==============================================================================
odoo.define('web_editor.ready', function (require) {
'use strict';
var base = require('web_editor.base');
return base.ready();
});
@@ -0,0 +1,333 @@
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 summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
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, options) {
var self = this;
var res = this._super.apply(this, arguments);
var Editor = options.Editor || rte.Class;
this.rte = new Editor(this, {
getConfig: function ($editable) {
var param = self._getDefaultConfig($editable);
if (options.generateOptions) {
param = options.generateOptions(param);
}
return param;
},
saveElement: options.saveElement,
});
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
if (options.snippets) {
this.snippetsMenu = new snippetsEditor.Class(this, Object.assign({
$el: $editable,
selectorEditableArea: '.o_editable',
}, options));
}
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
if (self.snippetsMenu) {
defs.push(this.snippetsMenu.insertAfter(this.$el));
}
this.rte.editable().find('*').off('mousedown mouseup click');
return Promise.all(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 {Promise}
*/
cancel: function (reload) {
var self = this;
return new Promise(function(resolve, reject) {
if (!rte.history.getEditableHasUndo().length) {
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: resolve,
});
confirm.on('closed', self, reject);
}
}).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 {Promise}
*/
save: function (reload) {
var self = this;
var defs = [];
this.trigger_up('ready_to_save', {defs: defs});
return Promise.all(defs).then(function () {
if (self.snippetsMenu) {
self.snippetsMenu.cleanForSave();
}
return self._saveCroppedImages();
}).then(function () {
return self.rte.save();
}).then(function () {
if (reload !== false) {
return self._reload();
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_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,
};
},
/**
* Reloads the page in non-editable mode, with the right scrolling.
*
* @private
* @returns {Promise} (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 new Promise(function(){});
},
/**
* @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 Promise.all(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,
};
});
@@ -0,0 +1,750 @@
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, params) {
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 = params && params.getConfig || this._getDefaultConfig;
this._saveElement = params && params.saveElement || this._saveElement;
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 {Promise} 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 Promise.all(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
@@ -67,7 +67,7 @@ var SnippetEditor = Widget.extend({
defs.push(this._initializeOptions());
// Initialize move/clone/remove buttons
if (!this.options.isEditableNode(this.$target[0])) {
if (!this.$target.is(':o_editable')) {
this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
} else {
this.dropped = false;
@@ -450,7 +450,7 @@ var SnippetEditor = Widget.extend({
// TODO lot of this is duplicated code of the d&d feature of snippets
if (!this.dropped) {
var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone').first();
var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first();
if ($el.length) {
$el.after(this.$target);
this.dropped = true;
@@ -609,6 +609,8 @@ var SnippetsMenu = Widget.extend({
}
this.$activeSnippet = false;
this.snippetEditors = [];
this.setSelectorEditableArea(options.$el, options.selectorEditableArea);
},
/**
* @override
@@ -677,6 +679,7 @@ var SnippetsMenu = Widget.extend({
// Auto-selects text elements with a specific class and remove this
// on text changes
this.$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
$(ev.target).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
$(ev.target).selectContent();
$(ev.target).removeClass('o_default_snippet_text');
});
@@ -747,7 +750,7 @@ var SnippetsMenu = Widget.extend({
},
});
this.cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;
return this.cacheSnippetTemplate[this.options.snippets];
return this._defLoadSnippets;
},
/**
* Sets the instance variables $editor, $body and selectorEditableArea.
@@ -766,7 +769,8 @@ var SnippetsMenu = Widget.extend({
* @returns {JQuery}
*/
getEditableArea: function () {
return this.$editor.find(this.selectorEditableArea);
return this.$editor.find(this.selectorEditableArea)
.add(this.$editor.filter(this.selectorEditableArea));
},
/**
* Updates the cover dimensions of the current snippet editor.
@@ -1242,6 +1246,10 @@ var SnippetsMenu = Widget.extend({
this.$el.html($html);
this._makeSnippetDraggable(this.$snippets);
this._disableUndroppableSnippets();
this.$el.addClass('o_loaded');
$('body.editor_enable').addClass('editor_has_snippets');
this.trigger_up('snippets_loaded', self.$el);
},
/**
* Creates a snippet editor to associated to the given snippet. If the given
@@ -1268,7 +1276,8 @@ var SnippetsMenu = Widget.extend({
}
return Promise.resolve(def).then(function (parentEditor) {
snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions, self.getEditableArea(), self.options);
let editableArea = self.getEditableArea();
snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions, $snippet.closest('[data-oe-type="html"], .oe_structure').add(editableArea), self.options);
self.snippetEditors.push(snippetEditor);
return snippetEditor.appendTo(self.$snippetEditorArea);
}).then(function () {
@@ -5,8 +5,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');
var FontPlugin = require('web_editor.wysiwyg.plugin.font');
var ColorpickerDialog = require('web.ColorpickerDialog');
var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
var qweb = core.qweb;
var _t = core._t;
@@ -667,19 +666,12 @@ registry.colorpicker = SnippetOption.extend({
}
if (!this.$el.find('.colorpicker').length) {
// Add common colors to palettes if not excluded
var fontPlugin = new FontPlugin({
layoutInfo: {
editable: $('<div/>'),
editingArea: $('<div/>'),
},
options: {},
});
// TODO remove old UI's code that survived
var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
var $clpicker = $(qweb.render('web_editor.colorpicker'));
var $clpicker = fontPlugin.createPalette('backColor').find('.note-color-palette'); // don't use custom color
$clpicker.find('.note-color-reset').remove();
$clpicker.find('h6').each(function () {
$(this).replaceWith($('<div class="mt8"/>').text($(this).text()));
_.each($clpicker.find('.o_colorpicker_section'), function (elem) {
$(elem).prepend("<div class='text-muted mt8'>" + elem.dataset.display + "</div>");
});
// Retrieve excluded palettes list
@@ -689,7 +681,7 @@ registry.colorpicker = SnippetOption.extend({
}
// Apply a custom title if specified
if (this.data.paletteTitle) {
$clpicker.find('.note-palette-title').text(this.data.paletteTitle);
$pt.find('.note-palette-title').text(this.data.paletteTitle);
}
// Remove excluded palettes
@@ -697,29 +689,39 @@ registry.colorpicker = SnippetOption.extend({
$clpicker.find('[data-name="' + exc + '"]').remove();
});
var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
// Add common colors to palettes if not excluded
if (!('common' in excluded)) {
var $commonColorSection = $clpicker.find('[data-name="common"]');
_.each(summernoteCustomColors, function (colorRow, i) {
var $div = $('<div/>', {class: 'clearfix'}).appendTo($commonColorSection);
if (i === 0) {
// Ignore the summernote gray palette and use ours
return;
}
_.each(colorRow, function (color) {
$div.append('<button class="o_custom_color" style="background-color: ' + color + '" />');
});
});
}
$pt.find('.o_colorpicker_section_tabs').append($clpicker);
this.$el.find('.dropdown-menu').append($pt);
}
var bgColor = ColorpickerDialog.formatColor(self.$target.css('background-color'));
var classes = [];
this.$el.find('.colorpicker button').each(function () {
var $color = $(this);
var color = $color.data('color');
if (color) {
$color.addClass('bg-' + color);
var className = self.colorPrefix + color;
if (self.$target.hasClass(className)) {
$color.addClass('selected');
}
classes.push(className);
} else {
color = $color.data('value');
if (bgColor === ColorpickerDialog.formatColor(color)) {
$color.addClass('selected');
}
if (!color) {
return;
}
$color.addClass('bg-' + color);
var className = self.colorPrefix + color;
if (self.$target.hasClass(className)) {
$color.addClass('selected');
}
classes.push(className);
});
this.classes = classes.join(' ');
@@ -753,9 +755,6 @@ registry.colorpicker = SnippetOption.extend({
var color = $(ev.currentTarget).data('color');
if (color) {
this.$target.addClass(this.colorPrefix + color);
} else if ($(ev.currentTarget).data('value')) {
color = $(ev.currentTarget).data('value');
this.$target.css('background-color', color);
} else if ($(ev.target).hasClass('o_custom_color')) {
this.$target
.removeClass(this.classes)
@@ -853,14 +852,13 @@ registry.background = SnippetOption.extend({
var $editable = this.$target.closest('.o_editable');
var _editor = new weWidgets.MediaDialog(this, {
onlyImages: true,
mediaWidth: 1920,
firstFilters: ['background'],
res_model: $editable.data('oe-model'),
res_id: $editable.data('oe-id'),
}, $image[0]).open();
_editor.on('save', this, function (image) {
this._setCustomBackground(image.src);
_editor.on('save', this, function () {
this._setCustomBackground($image.attr('src'));
this.$target.trigger('content_changed');
});
_editor.on('closed', this, function () {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,325 @@
odoo.define('web_editor.tour', function (require) {
'use strict';
var core = require('web.core');
var tour = require('web_tour.tour');
var base = require('web_editor.base');
/**
* Simulates a click event of given type on the given element.
*
* @param {DOMElement} el
* @param {string} type - 'click', 'mouseup', ...
*/
function simulateClickEvent(el, type) {
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
}
tour.register('rte', {
url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
test: true,
wait_for: base.ready(),
}, [{
content: "Change html for this test",
trigger: "#editable_area",
run: function () {
var html = '\n'+
'<section>\n'+
' <div class="container" style="width: 600px;">\n'+
' <div class="row">\n'+
' <div class="col-lg-6 mt16">\n'+
'<h1 id="text_content_id">Batnae municipium in Anthemusia</h1> \n'+
' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
' <p> Quam <img style="width: 25%" src="/web/static/src/img/logo.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
'<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
' </div>\n'+
' <div class="col-lg-6 mt16">\n'+
' <img class="img img-fluid shadow mb16" src="/web/static/src/img/logo.png" alt="Odoo text and image block">\n'+
' </div>\n'+
' </div>\n'+
'</section>\n';
this.$anchor.html(html);
}
}, {
content: "simulate triple click and change text bg-color",
trigger: '#editable_area > section .row > div:first',
run: function () {
var $h1 = $('h1', this.$anchor);
$.summernote.core.range.create($h1[0].firstChild, 0, $('p', this.$anchor)[0], 0).select();
simulateClickEvent($h1[0], 'mouseup');
}
}, {
content: "change text bg-color after triple click",
trigger: '.note-color button.dropdown-toggle:has(.fa-paint-brush)',
extra_trigger: '#editable_area > section .row > div:first',
}, {
content: "change text backColor",
trigger: "button[data-event=backColor]:visible:eq(2)",
}, {
content: "change selection to change text color",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
run: function () {
$.summernote.core.range.create(this.$anchor[0].firstChild, 5, this.$anchor[0].firstChild, 10).select();
simulateClickEvent(this.$anchor[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle:has(.fa-font)",
}, {
content: "change text color",
trigger: "button[data-event=foreColor]:visible:first",
}, {
content: "change selection to change text bg-color again",
trigger: '#editable_area > section .row > div:first h1 font:eq(2)',
run: function () {
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor[0].firstChild, 10).select();
simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle:has(.fa-paint-brush)",
}, {
content: "change text backColor again",
trigger: "button[data-event=backColor]:visible:first",
}, {
content: "change selection (h1 and p) to change text color with class",
trigger: '#editable_area > section .row > div:first h1 font:eq(4)',
run: function () {
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor.parent("h1").next("p")[0].firstChild, 30).select();
simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle:has(.fa-font)",
}, {
content: "change text color again",
trigger: "button[data-event=foreColor]:visible:eq(3)",
}, {
content: "delete selection",
trigger: '.o_editable.note-editable.o_dirty',
extra_trigger: '#editable_area > section .row > div:first p font',
run: "keydown 46", // delete
}, {
content: "create an other selection to delete",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
extra_trigger: '.o_editable.note-editable.o_dirty',
run: function () {
$.summernote.core.range.createFromNode(this.$anchor.next("p")[0]).clean();
$.summernote.core.range.create(this.$anchor.find('font:containsExact(ici)')[0].firstChild, 1, this.$anchor.next().next()[0].firstChild, 5).select();
simulateClickEvent(this.$anchor.find('font:last')[0], 'mouseup');
},
}, {
content: "clean and delete (backspace) an other selection",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
run: "keydown 8", // backspace
}, {
content: "an other selection",
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
extra_trigger: '.o_editable.note-editable.o_dirty',
run: function () {
$.summernote.core.range.create(this.$anchor.find('font:first')[0].firstChild, 3, this.$anchor.next("p")[0].childNodes[2], 8).select();
simulateClickEvent(this.$anchor.find('font:first')[0], 'mouseup');
},
}, {
content: "delete an other selection",
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
run: "keydown 46",
}, {
content: "undo",
trigger: '.note-image-popover button[data-event="undo"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
}, {
content: "undo again",
trigger: '.note-air-popover button[data-event="undo"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
}, {
content: "delete (backspace) after undo",
trigger: '.o_editable.note-editable.o_dirty',
extra_trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
run: "keydown 8", // backspace
}, {
content: "click on image",
trigger: '#editable_area > section .row > div:first img[style*="25%"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
}, {
content: "Click on resize half",
trigger: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
}, {
content: "Click on edit picture",
trigger: '.note-image-popover:visible button[data-event="showImageDialog"]',
extra_trigger: '#editable_area > section .row > div:first img[style*="50%"]',
}, {
content: "Click on pictogram tab",
trigger: 'a[data-toggle="tab"]:contains(Pictogram)',
extra_trigger: '#editor-media-image',
}, {
content: "select a pictogram",
trigger: '#editor-media-icon.active span.fa:first',
}, {
content: "save pictogram",
trigger: '.modal-footer > .btn-primary',
extra_trigger: '#editor-media-icon.active span.o_selected',
}, {
content: "select a size for the pictogram",
trigger: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
}, {
content: "click on float right",
trigger: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
extra_trigger: '#wrapwrap span.fa-3x',
}, {
content: "click on create link",
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
extra_trigger: '#editable_area > section .row > div:first span.fa.float-right',
}, {
content: "insert a link url",
trigger: 'input[name="url"]',
run: "text http://www.odoo.com",
}, {
content: "click on color style",
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.float-right',
}, {
content: "save link",
trigger: '.modal-footer > .btn-primary',
extra_trigger: 'a#link-preview.btn.btn-success span.fa.fa-3x.float-right',
}, {
content: "click on other picture",
trigger: '#editable_area > section .row > div:last img',
extra_trigger: 'body:not(:has(#link-preview)) a.btn[href^="http://"]:has(span.fa.fa-3x.float-right)',
}, {
content: "click on create link again",
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
extra_trigger: '#editable_area > section .row > div:first span.fa.float-right',
}, {
content: "insert an email",
trigger: 'input[name="url"]',
run: "text test@test.test",
}, {
content: "click on color style again",
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
}, {
content: "save link",
trigger: '.modal-footer > .btn-primary',
extra_trigger: 'a#link-preview.btn.btn-success[href="mailto:test@test.test"]:containsRegex(/^<img [^>]+>$/) img',
}, {
content: "select for triple enter then double backspace",
trigger: '#editable_area > section .row > div:first p:eq(2)',
extra_trigger: 'body:not(:has(#link-preview)) #editable_area > section .row > div:eq(1) > a > img',
run: function () {
var p = this.$anchor[0].firstChild;
$.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
simulateClickEvent(p, 'mouseup');
},
}, {
content: "triple enter then double backspace",
trigger: '#editable_area > section .row > div:first p:eq(2)',
run: "keydown 66,13,66,13,13,8,8", // B enter B enter enter backspace backspace
}, {
content: "add ul content",
trigger: '#editable_area > section .row > div:first',
extra_trigger: 'body:not(:has(#editable_area > section .row > div:first p:eq(5), #editable_area > section .row > div:eq(3))) #editable_area > section .row > div:first p:eq(3)',
run: function () {
var html = ' <ul> '+
'\n <li> <p>Batnae municipium. </p></li>'+
'\n <li> Seleucia praeter.</li>'+
'\n <li><p>Et hanc quidem.</p></li>'+
'\n </ul>';
this.$anchor.append(html);
var node = this.$anchor.find('ul li p:last')[0].firstChild;
$.summernote.core.range.create(node, 6).select();
simulateClickEvent(node, 'mouseup');
}
}, {
content: "click on style dropdown",
trigger: '.note-air-popover .note-style button.dropdown-toggle',
extra_trigger: '#editable_area > section .row > div:first ul li p:first',
}, {
content: "select h3",
trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
}, {
content: "select h3",
trigger: '#editable_area > section .row > div:first > ul > li > h3',
run: function () {
var node = this.$anchor[0].firstChild;
$.summernote.core.range.create(node, 0).select();
simulateClickEvent(node, 'mouseup');
}
}, {
content: "double tabulation",
trigger: '#editable_area > section .row > div:first > ul > li > h3',
run: "keydown 9,9", // tabulation
}, {
content: "click on order list",
trigger: '.note-air-popover button[data-event="insertOrderedList"]',
extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
}, {
content: "select for enter in ul",
trigger: '#editable_area > section .row > div:first ul li > p:last',
run: function () {
this.$anchor[0].firstChild.textContent += "";
$.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
simulateClickEvent(this.$anchor[0], 'mouseup');
}
}, {
content: "enter in ul",
trigger: '#editable_area > section .row > div:first ul li > p:last',
run: "keydown 66,13", // enter
}, {
trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
content: "backspace in list",
run: "keydown 8",
}, {
content: "end",
trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
}]);
tour.register('rte_inline', {
url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
test: true,
wait_for: base.ready(),
}, [{
content: "Change html for this test",
trigger: "#editable_area",
run: function () {
var html = '\n'+
'<div>\n'+
' <table cellspacing="0" cellpadding="0" width="100%">\n'+
' <tbody>\n'+
' <tr>\n'+
' <td valign="center" width="270">\n'+
' <img src="/logo.png" alt="Your Logo" class="rounded-circle img-thumbnail">\n'+
' </td>\n'+
' <td valign="center" width="270">\n'+
' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square float-right"></span>\n'+
' </td>\n'+
' </tr>\n'+
' </tbody>\n'+
' </table>\n'+
'</div>';
this.$anchor.html(html);
}
}, {
content: "call clean for save",
trigger: '#wrapwrap table',
run: function () {
core.bus.trigger('snippet_editor_clean_for_save');
}
}, {
content: "check the image style",
trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
}, {
content: "check the font image src",
trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(0,160,157)/28"]',
}, {
content: "check the font class to css",
trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
}, {
content: "check the second font class to css",
trigger: '#wrapwrap img:eq(2)[style*="float: right"],#wrapwrap img:eq(2)[style*="float:right"]',
}]);
});
@@ -1,6 +0,0 @@
odoo.define('web_editor.wysiwyg.options', function (require) {
'use strict';
return $.summernote.options;
});
@@ -1,222 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.abstract', function (require) {
'use strict';
var Class = require('web.Class');
var mixins = require('web.mixins');
var ServicesMixin = require('web.ServicesMixin');
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
var wysiwygOptions = require('web_editor.wysiwyg.options');
//--------------------------------------------------------------------------
// AbstractPlugin for summernote module API
//--------------------------------------------------------------------------
var AbstractPlugin = Class.extend(mixins.EventDispatcherMixin, ServicesMixin).extend({
/**
* Use this prop if you want to extend a summernote plugin.
*/
init: function (context) {
var self = this;
this._super.apply(this, arguments);
this.setParent(context.options.parent);
this.context = context;
if (!this.context.invoke) {
// for use outside of wysiwyg/summernote
this.context.invoke = function () {};
}
this.$editable = context.layoutInfo.editable;
this.editable = this.$editable[0];
this.document = this.editable.ownerDocument;
this.window = this.document.defaultView;
this.summernote = this.window._summernoteSlave || $.summernote; // if the target is in iframe
this.ui = this.summernote.ui;
this.$editingArea = context.layoutInfo.editingArea;
this.options = _.defaults(context.options || {}, wysiwygOptions);
this.lang = wysiwygTranslation;
this._addButtons();
if (this.events) {
this.events = _.clone(this.events);
_.each(_.keys(this.events), function (key) {
var value = self.events[key];
if (typeof value === 'string') {
value = self[value].bind(self);
}
if (key.indexOf('summernote.') === 0) {
self.events[key] = value;
} else {
delete self.events[key];
key = key.split(' ');
if (key.length > 1) {
self.context.layoutInfo.editor.on(key[0], key.slice(1).join(' '), value);
} else {
self.context.layoutInfo.editor.on(key, value);
}
}
});
}
},
//--------------------------------------------------------------------------
// Public summernote module API
//--------------------------------------------------------------------------
shouldInitialize: function () {
return true;
},
initialize: function () {},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Override to add buttons.
*/
_addButtons: function () {},
/**
* Creates a dropdown button with its contents and behavior.
*
* @param {str} optionName
* @param {str} buttonIcon (ex.: 'note-icon-align')
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
* @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
* @param {function} onclick
*/
_createDropdownButton: function (optionName, buttonIcon, buttonTooltip, values, onclick) {
var self = this;
if (!onclick) {
onclick = function (e) {
var classNames = _.map(values, function (item) {
return item.value;
}).join(' ');
var $target = $(self.context.invoke('editor.restoreTarget'));
$target.removeClass(classNames);
if ($(e.target).data('value')) {
$target.addClass($(e.target).data('value'));
}
};
}
if (optionName) {
this.context.memo('button.' + optionName, function () {
return self._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
});
} else {
return this._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
}
},
/**
* Creates a button to toggle a class.
*
* @param {str} optionName
* @param {str} buttonIcon (ex.: 'note-icon-align')
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
* @param {str} className
*/
_createToggleButton: function (optionName, buttonIcon, buttonTooltip, className) {
var self = this;
return this._createButton(optionName, buttonIcon, buttonTooltip, function () {
var $target = $(self.context.invoke('editor.restoreTarget'));
$target.toggleClass(className);
});
},
/**
* Creates a button.
*
* @param {str} optionName
* @param {str} buttonIcon (ex.: 'note-icon-align')
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
* @param {function} onclick
*/
_createButton: function (optionName, buttonIcon, buttonTooltip, onclick) {
var self = this;
if (optionName) {
this.context.memo('button.' + optionName, function () {
return self._renderButton(buttonIcon, buttonTooltip, onclick);
});
} else {
return this._renderButton(buttonIcon, buttonTooltip, onclick);
}
},
/**
* Helper function to _createButton: renders the button.
*
* @param {str} buttonIcon (ex.: 'note-icon-align')
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
* @param {function} onclick
* @returns {JQuery}
*/
_renderButton: function (buttonIcon, buttonTooltip, onclick) {
var self = this;
return this.context.invoke('buttons.button', {
contents: buttonIcon.indexOf('<') === -1 ? this.ui.icon(buttonIcon) : buttonIcon,
tooltip: buttonTooltip,
click: function (e) {
e.preventDefault();
self.context.invoke('editor.beforeCommand');
onclick(e);
self.editable.normalize();
self.context.invoke('editor.saveRange');
self.context.invoke('editor.afterCommand');
},
}).render();
},
/**
* Helper function to _createDropdownButton: renders the dropdown button.
*
* @param {str} buttonIcon (ex.: 'note-icon-align')
* @param {str} buttonTooltip (ex.: 'Align Paragraph')
* @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
* @param {function} onclick
* @param {JQuery}
*/
_renderDropdownButton: function (buttonIcon, buttonTooltip, values, onclick) {
return this.ui.buttonGroup([
this.context.invoke('buttons.button', {
className: 'dropdown-toggle',
contents: buttonIcon.indexOf('<') === -1 ?
this.ui.dropdownButtonContents(this.ui.icon(buttonIcon), this.options) : buttonIcon,
tooltip: buttonTooltip,
data: {
toggle: 'dropdown'
}
}),
this.ui.dropdown({
items: values,
template: function (item) {
return item.string;
},
click: this._wrapCommand(function (e) {
e.preventDefault();
onclick(e);
}),
})
]).render();
},
/**
* Wraps a given function between common actions required
* for history (undo/redo) and the maintenance of the DOM/range.
*
* @param {function} fn
* @returns {any} the return of fn
*/
_wrapCommand: function (fn) {
var self = this;
return function () {
self.context.invoke('editor.restoreRange');
self.context.invoke('editor.beforeCommand');
var res = fn.apply(self, arguments);
self.editable.normalize();
self.context.invoke('editor.saveRange');
self.context.invoke('editor.afterCommand');
return res;
};
},
});
return AbstractPlugin;
});
@@ -1,571 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.bullet', function (require) {
'use strict';
var core = require('web.core');
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var wysiwygOptions = require('web_editor.wysiwyg.options');
var registry = require('web_editor.wysiwyg.plugin.registry');
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
var _t = core._t;
var dom = $.summernote.dom;
wysiwygOptions.icons.checklist = 'fa fa-check-square';
wysiwygOptions.keyMap.pc['CTRL+SHIFT+NUM9'] = 'insertCheckList';
wysiwygOptions.keyMap.mac['CMD+SHIFT+NUM9'] = 'insertCheckList';
wysiwygTranslation.lists.checklist = _t('Checklist');
wysiwygTranslation.help.checklist = _t('Toggle checkbox list');
var BulletPlugin = AbstractPlugin.extend({
events: {
'summernote.mousedown': '_onMouseDown',
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Insert an ordered list, an unordered list or a checklist.
* If already in list, remove the list or convert it to the given type.
*
* @param {string('ol'|'ul'|'checklist')} type the type of list to insert
* @returns {false|Node[]} contents of the ul/ol or content of the converted/removed list
*/
insertList: function (type) {
var range = this.context.invoke('editor.createRange');
if (!range) {
return;
}
var res;
var start = range.getStartPoint();
var end = range.getEndPoint();
var isInList = dom.ancestor(range.sc, dom.isList);
if (isInList) {
res = this._convertList(false, [], start, end, type);
} else {
var ul = this._createList(type);
res = ul ? [].slice.call(ul.children) : [];
}
var startLeaf = this.context.invoke('HelperPlugin.firstLeaf', start.node);
var endLeaf = this.context.invoke('HelperPlugin.firstLeaf', end.node);
range = this.context.invoke('editor.setRange', startLeaf, start.offset, endLeaf, end.offset);
range.select();
this.context.invoke('editor.saveRange');
return res;
},
/**
* Indent a node (list or format node).
*
* @returns {false|Node[]} contents of list/indented item
*/
indent: function () {
return this._indent();
},
/**
* Outdent a node (list or format node).
*
* @returns {false|Node[]} contents of list/outdented item
*/
outdent: function () {
return this._indent(true);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Add checklist buttons.
*/
_addButtons: function () {
var self = this;
this._super();
this.context.memo('help.checklist', this.lang.help.checklist);
this._createButton('checklist', this.options.icons.checklist, this.lang.lists.checklist, function (e) {
e.preventDefault();
self.context.invoke('editor.insertCheckList');
});
},
/**
* Convert ul<->ol or remove ul/ol.
*
* @param {boolean} isWithinElem true if selection already inside the LI
* @param {DOM[]} nodes selected nodes
* @param {Object} startPoint
* @param {Object} endPoint
* @param {boolean} sorted
* @returns {boolean} isWithinElem
*/
_convertList: function (isWithinElem, nodes, startPoint, endPoint, sorted) {
var ol = dom.ancestor(startPoint.node, dom.isList);
var parent = ol.parentNode;
// get selected lis
var lis = [];
var lisBefore = [];
var lisAfter = [];
_.each(ol.children, function (li) {
if (!isWithinElem && (li === startPoint.node || $.contains(li, startPoint.node))) {
isWithinElem = true;
}
if (isWithinElem) {
lis.push(li);
} else if (lis.length) {
lisAfter.push(li);
} else {
lisBefore.push(li);
}
if (isWithinElem && (li === endPoint.node || $.contains(li, endPoint.node))) {
isWithinElem = false;
}
});
var res = lis;
if (lisBefore.length) {
var ulBefore = this.document.createElement(ol.tagName);
ulBefore.className = ol.className;
if (dom.isLi(ol.parentNode)) {
var li = this.document.createElement('li');
li.className = ol.parentNode.className;
$(li).insertBefore(ol.parentNode);
li.appendChild(ulBefore);
} else {
$(ulBefore).insertBefore(ol);
}
$(ulBefore).append(lisBefore);
}
if (lisAfter.length) {
var ulAfter = this.document.createElement(ol.tagName);
ulAfter.className = ol.className;
if (dom.isLi(ol.parentNode)) {
var li = this.document.createElement('li');
li.className = ol.parentNode.className;
$(li).insertAfter(ol.parentNode);
li.appendChild(ulAfter);
} else {
$(ulAfter).insertAfter(ol);
}
$(ulAfter).append(lisAfter);
}
// convert ul<->ol or remove list
var current = ol.tagName === 'UL' && ol.className.indexOf('o_checklist') !== -1 ? 'checklist' : ol.tagName.toLowerCase();
if (current !== sorted) {
// convert ul <-> ol
var ul;
$(ol).removeClass('o_checklist');
if (sorted === 'checklist' && current === "ul") {
ul = ol;
} else if (sorted === 'ul' && current === 'checklist') {
ul = ol;
} else {
ul = this.document.createElement(sorted === "ol" ? "ol" : "ul");
ul.className = ol.className;
$(ul).removeClass('o_checklist');
$(ul).insertBefore(ol).append(lis);
parent.removeChild(ol);
}
if (sorted === 'checklist') {
$(ul).addClass('o_checklist');
}
this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
} else {
// remove ol/ul
if (dom.isLi(parent) || dom.isList(parent)) {
if (dom.isLi(parent)) {
ol = parent;
parent = ol.parentNode;
}
$(lis).insertBefore(ol);
} else {
res = [];
_.each(lis, function (li) {
res.push.apply(res, li.childNodes);
$(li.childNodes).insertBefore(ol);
});
// wrap in p
var hasNode = _.find(res, function (node) {
return node.tagName && node.tagName !== "BR" && !dom.isMedia(node);
});
if (!hasNode) {
var p = this.document.createElement('p');
$(p).insertBefore(ol).append(res);
res = [p];
}
}
parent.removeChild(ol);
}
nodes.push.apply(nodes, res);
return isWithinElem;
},
/**
* Create a list if allowed.
*
* @param {string('ol'|'ul'|'checklist')} type the type of list to insert
* @returns {false|Node} the list, if any
*/
_createList: function (type) {
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
var formatNodes = this._filterEditableFormatNodes(nodes);
if (!formatNodes.length) {
return;
}
var ul = this._createListElement(type);
$(formatNodes[0][0] || formatNodes[0]).before(ul);
this._fillListElementWith(ul, formatNodes);
this._deleteListElementEdges(ul);
return ul;
},
/**
* Create a list element of the given type and return it.
*
* @param {string('ol'|'ul'|'checklist')} type the type of list to insert
* @returns {Node}
*/
_createListElement: function (type) {
var ul = this.document.createElement(type === "ol" ? "ol" : "ul");
if (type === 'checklist') {
ul.className = 'o_checklist';
}
return ul;
},
/**
* Delete a list element's edges if necessary.
*
* @param {Node} ul
*/
_deleteListElementEdges: function (ul) {
this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
this.editable.normalize();
},
/**
* Fill a list element with the nodes passed, wrapped in LIs.
*
* @param {Node} ul
* @param {Node[]} nodes
*/
_fillListElementWith: function (ul, nodes) {
var self = this;
_.each(nodes, function (node) {
var li = self.document.createElement('li');
$(li).append(node);
ul.appendChild(li);
});
},
/**
* Filter the editable format ancestors of the given nodes
* and fill or wrap them if needed for range selection.
*
* @param {Node[]} nodes
* @returns {Node[]}
*/
_filterEditableFormatNodes: function (nodes) {
var self = this;
var formatNodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
formatNodes = _.compact(_.map(formatNodes, function (node) {
var ancestor = (!node.tagName || node.tagName === 'BR') && dom.ancestor(node, dom.isCell);
if (ancestor && self.options.isEditableNode(ancestor)) {
if (!ancestor.childNodes.length) {
var br = self.document.createElement('br');
ancestor.appendChild(br);
}
var p = self.document.createElement('p');
$(p).append(ancestor.childNodes);
ancestor.appendChild(p);
return p;
}
return self.options.isEditableNode(node) && node || null;
}));
return formatNodes;
},
/**
* Indent or outdent a format node.
*
* @param {bool} outdent true to outdent, false to indent
* @returns {false|[]Node} indented nodes
*/
_indent: function (outdent) {
var range = this.context.invoke('editor.createRange');
if (!range) {
return;
}
// list groups shouldn't be outdented like regular lists
if ($(dom.ancestor(range.sc, dom.isList)).hasClass('list-group')) {
return;
}
var self = this;
var nodes = [];
var isWithinElem;
var ancestor = range.commonAncestor() || this.editable;
var $dom = $(ancestor);
if (!dom.isList(ancestor)) {
// to indent a selection, we indent the child nodes of the common
// ancestor that contains this selection
$dom = $(ancestor.tagName ? ancestor : ancestor.parentNode).children();
}
// if selection is inside indented contents and outdent is true, we can outdent this node
var indentedContent = outdent && dom.ancestor(ancestor, function (node) {
var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
return node.tagName && !!parseFloat(node.style[style] || 0);
});
if (indentedContent) {
$dom = $(indentedContent);
} else {
// if selection is inside a list, we indent its list items
$dom = $(dom.ancestor(ancestor, dom.isList));
if (!$dom.length) {
// if the selection is contained in a single HTML node, we indent
// the first ancestor 'content block' (P, H1, PRE, ...) or TD
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
$dom = $(this._filterEditableFormatNodes(nodes));
}
}
// if select tr, take the first td
$dom = $dom.map(function () {
return this.tagName === "TR" ? this.firstElementChild : this;
});
$dom.each(function () {
if (isWithinElem || $.contains(this, range.sc)) {
if (dom.isList(this)) {
if (outdent) {
var type = this.tagName === 'OL' ? 'ol' : (this.className && this.className.indexOf('o_checklist') !== -1 ? 'checklist' : 'ul');
isWithinElem = self._convertList(isWithinElem, nodes, range.getStartPoint(), range.getEndPoint(), type);
} else {
isWithinElem = self._indentUL(isWithinElem, nodes, this, range.sc, range.ec);
}
} else if (self.context.invoke('HelperPlugin.isFormatNode', this) || dom.ancestor(this, dom.isCell)) {
isWithinElem = self._indentFormatNode(outdent, isWithinElem, nodes, this, range.sc, range.ec);
}
}
});
if ($dom.parent().length) {
var $parent = $dom.parent();
// remove text nodes between lists
var $ul = $parent.find('ul, ol');
if (!$ul.length) {
$ul = $(dom.ancestor(range.sc, dom.isList));
}
$ul.each(function () {
var notWhitespace = /\S/;
if (
this.previousSibling &&
this.previousSibling !== this.previousElementSibling &&
!this.previousSibling.textContent.match(notWhitespace)
) {
this.parentNode.removeChild(this.previousSibling);
}
if (
this.nextSibling &&
this.nextSibling !== this.nextElementSibling &&
!this.nextSibling.textContent.match(notWhitespace)
) {
this.parentNode.removeChild(this.nextSibling);
}
});
// merge same ul or ol
$ul.prev('ul, ol').each(function () {
self.context.invoke('HelperPlugin.deleteEdge', this, 'next');
});
}
range.normalize().select();
this.context.invoke('editor.saveRange');
return !!nodes.length && nodes;
},
/**
* Indent several LIs in a list.
*
* @param {bool} isWithinElem true if selection already inside the LI
* @param {Node[]} nodes
* @param {Node} UL
* @param {Node} start
* @param {Node} end
* @returns {bool} isWithinElem
*/
_indentUL: function (isWithinElem, nodes, UL, start, end) {
var next;
var tagName = UL.tagName;
var node = UL.firstChild;
var ul = document.createElement(tagName);
ul.className = UL.className;
var flag;
if (isWithinElem) {
flag = true;
}
// create and fill ul into a li
while (node) {
if (flag || node === start || $.contains(node, start)) {
isWithinElem = true;
node.parentNode.insertBefore(ul, node);
}
next = node.nextElementSibling;
if (isWithinElem) {
ul.appendChild(node);
nodes.push(node);
}
if (node === end || $.contains(node, end)) {
isWithinElem = false;
break;
}
node = next;
}
var temp;
var prev = ul.previousElementSibling;
if (
prev && prev.tagName === "LI" &&
(temp = prev.firstElementChild) && temp.tagName === tagName &&
((prev.firstElementChild || prev.firstChild) !== ul)
) {
$(prev.firstElementChild || prev.firstChild).append($(ul).contents());
$(ul).remove();
ul = prev;
ul.parentNode.removeChild(ul.nextElementSibling);
}
next = ul.nextElementSibling;
if (
next && next.tagName === "LI" &&
(temp = next.firstElementChild) && temp.tagName === tagName &&
(ul.firstElementChild !== next.firstElementChild)
) {
$(ul.firstElementChild).append($(next.firstElementChild).contents());
$(next.firstElementChild).remove();
ul.parentNode.removeChild(ul.nextElementSibling);
}
// wrap in li
var li = this.document.createElement('li');
li.className = 'o_indent';
$(ul).before(li);
li.appendChild(ul);
return isWithinElem;
},
/**
* Outdent a container node.
*
* @param {Node} node
* @returns {float} margin
*/
_outdentContainer: function (node) {
var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
var margin = parseFloat(node.style[style] || 0) - 1.5;
node.style[style] = margin > 0 ? margin + "em" : "";
return margin;
},
/**
* Indent a container node.
*
* @param {Node} node
* @returns {float} margin
*/
_indentContainer: function (node) {
var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
var margin = parseFloat(node.style[style] || 0) + 1.5;
node.style[style] = margin + "em";
return margin;
},
/**
* Indent/outdent a format node.
*
* @param {bool} outdent true to outdent, false to indent
* @param {bool} isWithinElem true if selection already inside the element
* @param {DOM[]} nodes
* @param {DOM} p
* @param {DOM} start
* @param {DOM} end
* @returns {bool} isWithinElem
*/
_indentFormatNode: function (outdent, isWithinElem, nodes, p, start, end) {
if (p === start || $.contains(p, start) || $.contains(start, p)) {
isWithinElem = true;
}
if (isWithinElem) {
nodes.push(p);
if (outdent) {
this._outdentContainer(p);
} else {
this._indentContainer(p);
}
}
if (p === end || $.contains(p, end) || $.contains(end, p)) {
isWithinElem = false;
}
return isWithinElem;
},
//--------------------------------------------------------------------------
// Handle
//--------------------------------------------------------------------------
/**
* @param {SummernoteEvent} se
* @param {jQueryEvent} e
*/
_onMouseDown: function (se, e) {
if (!dom.isLi(e.target) || !$(e.target).parent('ul.o_checklist').length || e.offsetX > 0) {
return;
}
e.preventDefault();
var checked = $(e.target).hasClass('o_checked');
$(e.target).toggleClass('o_checked', !checked);
var $sublevel = $(e.target).next('ul.o_checklist, li:has(> ul.o_checklist)').find('> li, ul.o_checklist > li');
var $parents = $(e.target).parents('ul.o_checklist').map(function () {
return this.parentNode.tagName === 'LI' ? this.parentNode : this;
});
if (checked) {
$sublevel.removeClass('o_checked');
$parents.prev('ul.o_checklist li').removeClass('o_checked');
} else {
$sublevel.addClass('o_checked');
var $lis;
do {
$lis = $parents.not(':has(li:not(.o_checked))').prev('ul.o_checklist li:not(.o_checked)');
$lis.addClass('o_checked');
} while ($lis.length);
}
},
});
registry.add('BulletPlugin', BulletPlugin);
return BulletPlugin;
});
@@ -1,88 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.buttons', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var dom = $.summernote.dom;
Plugins.buttons.include({
/**
* Fix tooltip for 'option' buttons (summernote 0.8.9 bug)
* Remove this once the library is updated to 0.8.10.
*
* @override
*/
addToolbarButtons: function () {
this.options.codeview = this.lang.options.codeview;
this.options.help = this.lang.options.help;
this.options.fullscreen = this.lang.options.fullscreen;
this._super();
},
/**
* Show current style (of start of selection) in magic wand dropdown.
*
* @param {JQuery Object} $container
*/
updateActiveStyleDropdown: function ($container) {
var self = this;
var range = this.context.invoke('editor.createRange');
var el = dom.ancestor(range.sc, function (n) {
return n.tagName && self.options.styleTags.indexOf(n.tagName.toLowerCase()) !== -1;
});
if (el) {
var tagName = el.tagName.toLowerCase();
$container.find('.dropdown-style a').each(function (idx, item) {
var $item = $(item);
// always compare string to avoid creating another func.
var isChecked = ($item.data('value') + '') === (tagName + '');
$item.toggleClass('active', isChecked);
});
} else {
var $item = $container.find('.dropdown-style a.active');
$item.removeClass('active');
}
},
/**
* @override
*/
updateCurrentStyle: function ($container) {
this._super.apply(this, arguments);
this.updateParaAlignIcon($container || this.$toolbar);
this.updateActiveStyleDropdown($container || this.$toolbar);
},
/**
* Show current paragraph alignment (of start of selection) on paragraph alignment dropdown.
*
* @param {JQuery Object} $container
*/
updateParaAlignIcon: function ($container) {
var range = this.context.invoke('editor.createRange');
var $paraIcon = $container.find('.note-para .dropdown-toggle i');
var el = dom.isText(range.sc) ? range.sc.parentNode : range.sc;
if (el) {
$paraIcon.removeClass();
switch ($(el).css('text-align')) {
case 'left':
$paraIcon.addClass('note-icon-align-left');
break;
case 'center':
$paraIcon.addClass('note-icon-align-center');
break;
case 'right':
$paraIcon.addClass('note-icon-align-right');
break;
case 'justify':
$paraIcon.addClass('note-icon-align-justify');
break;
default:
$paraIcon.addClass('note-icon-align-left');
break;
}
}
},
});
return Plugins.buttons;
});
@@ -1,60 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.codeview', function (require) {
'use strict';
var core = require('web.core');
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var _t = core._t;
var CodeviewPlugin = Plugins.codeview.extend({
/**
* @override
*/
activate: function () {
this._super();
if (this.$codable.height() === 0) {
this.$codable.height(180);
}
this.context.invoke('editor.hidePopover');
this.context.invoke('editor.clearTarget');
},
/**
* @override
*/
deactivate: function () {
if (
this.context.invoke('HelperPlugin.hasJinja', this.context.invoke('code')) &&
!this.isBeingDestroyed
) {
var message = _t("Your code contains JINJA conditions.\nRemove them to return to WYSIWYG HTML edition.");
this.do_warn(_t("Cannot edit HTML"), message);
this.$codable.focus();
return;
}
this._super();
this.$editable.css('height', '');
},
/**
* @override
*/
destroy: function () {
this.isBeingDestroyed = true;
this._super();
},
/**
* Force activation of the code view.
*/
forceActivate: function () {
if (!this.isActivated()) {
this.activate();
}
},
});
registry.add('codeview', CodeviewPlugin);
return CodeviewPlugin;
});
@@ -1,229 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.dropzone', function (require) {
'use strict';
var core = require('web.core');
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var utils = require('web.utils');
var _t = core._t;
var dom = $.summernote.dom;
var DropzonePlugin = Plugins.dropzone.extend({
//--------------------------------------------------------------------------
// Public summernote module API
//--------------------------------------------------------------------------
/**
* Disable Summernote's handling of drop events.
*/
attachDragAndDropEvent: function () {
this._super.apply(this, arguments);
this.$dropzone.off('drop');
this.$dropzone.on('dragenter', this._onDragenter.bind(this));
this.$dropzone.on('drop', this._onDrop.bind(this));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Clean up then drops HTML or plain text into the editor.
*
* @private
* @param {String} html
* @param {Boolean} textOnly true to allow only dropping plain text
*/
_dropHTML: function (html, textOnly) {
this.context.invoke('editor.beforeCommand');
// Clean up
var nodes = this.context.invoke('TextPlugin.prepareClipboardData', html);
// Delete selection
this.context.invoke('HelperPlugin.deleteSelection');
// Insert the nodes
this.context.invoke('TextPlugin.pasteNodes', nodes, textOnly);
this.context.invoke('HelperPlugin.normalize');
this.context.invoke('editor.saveRange');
this.context.invoke('editor.afterCommand');
},
/**
* Drop images into the editor: save them as attachments.
*
* @private
* @param {File[]]} files (images only)
*/
_dropImages: function (files) {
var self = this;
this.context.invoke('editor.beforeCommand');
var range = this.context.invoke('editor.createRange');
var spinners = [];
var images = [];
var defs = [];
_.each(files, function (file) {
// Add spinner
var spinner = $('<span class="fa fa-spinner fa-spin">').attr('data-filename', file.name)[0];
self.context.invoke('editor.hidePopover');
if (range.sc.tagName) {
if (range.so >= dom.nodeLength(range.sc)) {
$(range.sc).append(spinner);
} else {
$(range.sc.childNodes[range.so]).before(spinner);
}
} else {
range.sc.splitText(range.so);
$(range.sc).after(spinner);
}
spinners.push(spinner);
// save images as attachments
var def = new Promise(function (resolve) {
// Get image's Base64 string
utils.getDataURLFromFile(file).then(function (result) {
self._uploadImage(result, file.name).then(function (attachment) {
// Make the HTML
var image = self.document.createElement('img');
image.setAttribute('style', 'width: 100%;');
image.src = '/web/content/' + attachment.id + '/' + attachment.name;
image.alt = attachment.name;
$(spinner).replaceWith(image);
images.push(image);
resolve(image);
$(image).trigger('dropped');
});
});
});
defs.push(def);
});
this.trigger_up('drop_images', {
spinners: spinners,
promises: defs,
});
Promise.all(defs).then(function () {
var defs = [];
$(images).each(function () {
if (!this.height) {
var def = new Promise(function (resolve) {
$(this).one('load error abort', resolve);
});
defs.push(def);
}
});
Promise.all(defs).then(function () {
if (images.length === 1) {
range = self.context.invoke('editor.setRange', _.last(images), 0);
range.select();
self.context.invoke('editor.saveRange');
self.context.invoke('editor.afterCommand');
self.context.invoke('MediaPlugin.updatePopoverAfterEdit', images[0]);
} else {
self.context.invoke('editor.afterCommand');
}
});
});
},
/**
* Upload an image from its Base64 representation.
*
* @private
* @param {String} imageBase64
* @param {String} fileName
* @returns {Promise}
*/
_uploadImage: function (imageBase64, fileName) {
var options = {};
this.trigger_up('getRecordInfo', {
recordInfo: options,
type: 'media',
callback: function (recordInfo) {
_.defaults(options, recordInfo);
},
});
return this._rpc({
route: '/web_editor/attachment/add_data',
params: {
'data': imageBase64.split(';base64,').pop(),
'name': fileName,
'res_id': options.res_id || 0,
'res_model': options.res_model,
},
});
},
/**
* @private
* @param {JQueryEvent} e
*/
_onDragenter: function (e) {
var range = this.context.invoke('editor.createRange');
if (!this.editable.contains(range.sc)) {
var node = this.editable;
if (node && node.firstChild) {
node = node.firstChild;
}
range = this.context.invoke('editor.setRange', node, 0, node, 0);
range.select();
}
},
/**
* @private
* @param {JQueryEvent} e
*/
_onDrop: function (e) {
e.preventDefault();
this.$editor.removeClass('dragover');
if (this.options.disableDragAndDrop) {
return;
}
var dataTransfer = e.originalEvent.dataTransfer;
if (!this._canDropHere()) {
this.context.invoke('HelperPlugin.notify', _t("Not a dropzone"), _t("Dropping is prohibited in this area."));
return;
}
if (dataTransfer.getData('text/html')) {
this._dropHTML(dataTransfer.getData('text/html'));
return;
}
if (dataTransfer.files.length) {
var images = [];
_.each(dataTransfer.files, function (file) {
if (file.type.indexOf('image') !== -1) {
images.push(file);
}
});
if (!images.length || images.length < dataTransfer.files.length) {
this.context.invoke('HelperPlugin.notify', _t("Unsupported file type"), _t("Images are the only file types that can be dropped."));
}
if (images.length) {
this._dropImages(images);
}
}
},
/**
* Return true if dropping is allowed at the current range.
*
* @private
* @returns {Boolean}
*/
_canDropHere: function () {
var range = this.context.invoke('editor.createRange');
return this.options.isEditableNode(range.sc);
},
});
registry.add('dropzone', DropzonePlugin);
return DropzonePlugin;
});
@@ -1,189 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.editor', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var TablePlugin = require('web_editor.wysiwyg.plugin.table');
var NewSummernoteEditor = Plugins.editor.extend({
//--------------------------------------------------------------------------
// Public summernote module API
//--------------------------------------------------------------------------
initialize: function () {
var self = this;
this.history = this.context.modules.HistoryPlugin;
this.dropzone = this.context.modules.DropzonePlugin;
this.insertTable = this.wrapCommand(this._insertTable.bind(this));
this.insertOrderedList = this.wrapCommand(this._insertOrderedList.bind(this));
this.insertUnorderedList = this.wrapCommand(this._insertUnorderedList.bind(this));
this.insertCheckList = this.wrapCommand(this._insertCheckList.bind(this));
this.indent = this.wrapCommand(this._indent.bind(this));
this.outdent = this.wrapCommand(this._outdent.bind(this));
this.table = new TablePlugin(this.context);
this.hasFocus = this._hasFocus.bind(this);
this._makeTextCommand('formatBlock');
this._makeTextCommand('removeFormat');
_.each('bold,italic,underline,strikethrough,superscript,subscript'.split(','), function (sCmd) {
self._makeTextCommand('formatText', sCmd);
});
_.each('justifyLeft,justifyCenter,justifyRight,justifyFull'.split(','), function (sCmd) {
self._makeTextCommand('formatBlockStyle', sCmd);
});
this._super();
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Hide all popovers.
*/
hidePopover: function () {
this.context.invoke('MediaPlugin.hidePopovers');
this.context.invoke('LinkPopover.hide');
},
/*
* Focus the editor.
*/
focus: function () {
// [workaround] Screen will move when page is scolled in IE.
// - do focus when not focused
if (!this.hasFocus()) {
var range = $.summernote.range.create();
if (range) {
$(range.sc).closest('[contenteditable]').focus();
range.select();
} else {
this.$editable.focus();
}
}
},
/**
* Fix double-undo (CTRL-Z) issue with Odoo integration.
*
* @override
*/
undo: function () {
this.createRange();
this._super();
},
/**
* Set the range at the given nodes and offsets.
* If no `ec` is specified, the range is collapsed on start.
* Note: Does NOT select the range.
*
* @param {Node} sc
* @param {Number} so
* @param {Node} [ec]
* @param {Number} [eo]
* @returns {Object} range
*/
setRange: function (sc, so, ec, eo) {
var range = this.createRange();
range.sc = sc;
range.so = so;
range.ec = ec || sc;
range.eo = eo || so;
return range;
},
/**
* Remove a link and preserve its text contents.
*/
unlink: function () {
this.beforeCommand();
this.context.invoke('LinkPlugin.unlink');
this.afterCommand();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns true if editable area has focus.
*
* @private
* @returns {Boolean}
*/
_hasFocus: function () {
return this.$editable.is(':focus') || !!this.$editable.find('[contenteditable]:focus').length;
},
/**
* Indent a list or a format node.
*
* @private
* @returns {false|Node[]} contents of list/indented item
*/
_indent: function () {
return this.context.invoke('BulletPlugin.indent');
},
/**
* Insert a checklist.
*
* @private
*/
_insertCheckList: function () {
this.context.invoke('BulletPlugin.insertList', 'checklist');
},
/**
* Insert an ordered list.
*
* @private
*/
_insertOrderedList: function () {
this.context.invoke('BulletPlugin.insertList', 'ol');
},
/**
* Insert table (respecting unbreakable node rules).
*
* @private
* @param {string} dim (eg: 3x3)
*/
_insertTable: function (dim) {
this.context.invoke('TablePlugin.insertTable', dim);
},
/**
* Insert an ordered list.
*
* @private
*/
_insertUnorderedList: function () {
this.context.invoke('BulletPlugin.insertList', 'ul');
},
/**
* Adds a TextPlugin command to the editor object.
* The command will exist in the editor object under the name
* arg if specified, or commandName otherwise.
*
* @param {String} commandName
* @param {String} arg
*/
_makeTextCommand: function (commandName, arg) {
var self = this;
this[arg || commandName] = self.wrapCommand(function (value) {
self.context.invoke('TextPlugin.' + commandName, arg || value, value);
});
},
/**
* Outdent a list or a format node.
*
* @private
* @returns {false|Node[]} contents of list/outdented item
*/
_outdent: function () {
return this.context.invoke('BulletPlugin.outdent');
},
});
// Override Summernote default editor
registry.add('editor', NewSummernoteEditor);
return NewSummernoteEditor;
});
@@ -1,567 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.font', function (require) {
'use strict';
var core = require('web.core');
var ColorpickerDialog = require('web.ColorpickerDialog');
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var registry = require('web_editor.wysiwyg.plugin.registry');
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
var wysiwygOptions = require('web_editor.wysiwyg.options');
var QWeb = core.qweb;
var _t = core._t;
var dom = $.summernote.dom;
//--------------------------------------------------------------------------
// Font (colorpicker & font-size)
//--------------------------------------------------------------------------
dom.isFont = function (node) {
return node && node.tagName === "FONT" || dom.isIcon(node);
};
var FontPlugin = AbstractPlugin.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Creates both ColorPicker buttons (fore- and backcolors) containing their palettes.
*
* @returns {jQuery} the buttons' div.btn-group container
*/
createColorPickerButtons: function () {
var self = this;
var $container = $('<div class="note-btn-group btn-group note-color"/>');
// create and append the buttons
this._createForeColorButton().appendTo($container);
this._createBackColorButton().appendTo($container);
// add event
$container.on('mousedown', function () {
self.context.invoke('editor.saveRange');
});
return $container;
},
/**
* Takes the font size button created by summernote and override its
* events so as to customize its behavior.
*
* @param {jQuery} $button the button, as created by summernote
* @returns {jQuery} the overridden button
*/
overrideFontSizeButton: function ($button) {
var self = this;
$button.click(function (e) {
e.preventDefault();
});
$button.find('.dropdown-menu').off('click').on('mousedown', function (e) {
e.preventDefault();
self.context.invoke('editor.createRange');
self.context.invoke('editor.beforeCommand');
var $target = $(e.target);
self.context.invoke('FontPlugin.changeFontSize', $target.closest('[data-value]').data('value'), $target);
self.context.invoke('buttons.updateCurrentStyle');
self.context.invoke('editor.saveRange');
self.context.invoke('editor.afterCommand');
});
return $button;
},
/**
* Creates a (fore- or back-) color palette.
*
* @param {string('foreColor'|'backColor')} eventName
* @returns {jQuery}
*/
createPalette: function (eventName) {
var self = this;
var colors = _.clone(this.options.colors);
colors.splice(0, 1); // Ignore the summernote gray palette and use ours
var $palette = $(QWeb.render('wysiwyg.plugin.font.colorPalette', {
colors: colors,
eventName: eventName,
lang: this.lang,
}));
if (this.options.tooltip) {
$palette.find('.note-color-btn').tooltip({
container: this.options.container,
trigger: 'hover',
placement: 'bottom'
});
}
// custom structure for the color picker and custom colorsin XML template
var $clpicker = $(QWeb.render('web_editor.colorpicker'));
var $buttons = $(_.map($clpicker.children(), function (group) {
var $contents = $(group).contents();
if (!$contents.length) {
return '';
}
var $row = $("<div/>", {
"class": "note-color-row mb8 clearfix",
'data-group': $(group).data('name'),
}).append($contents);
var $after_breaks = $row.find(".o_small + :not(.o_small)");
if ($after_breaks.length === 0) {
$after_breaks = $row.find(":nth-child(8n+9)");
}
$after_breaks.addClass("o_clear");
return $row[0].outerHTML;
}).join(""));
$buttons.find('button').each(function () {
var color = $(this).data('color');
$(this).addClass('note-color-btn bg-' + color).attr('data-value', (eventName === 'backColor' ? 'bg-' : 'text-') + color);
});
$palette.find('.o_theme_color_placeholder').prepend($buttons.filter('[data-group="theme"]'));
$palette.find('.o_transparent_color_placeholder').prepend($buttons.filter('[data-group="transparent_grayscale"]'));
$palette.find('.o_common_color_placeholder').prepend($buttons.filter('[data-group="common"]'));
$palette.off('click').on('mousedown', '.note-color-btn', function (e) {
e.preventDefault();
self.context.invoke('editor.createRange');
self.context.invoke('editor.beforeCommand');
var method = eventName === 'backColor' ? 'changeBgColor' : 'changeForeColor';
var $target = $(e.target);
self.context.invoke('FontPlugin.' + method, $target.closest('[data-value]').data('value'), $target);
self.context.invoke('buttons.updateCurrentStyle');
self.editable.normalize();
self.context.invoke('editor.saveRange');
self.context.invoke('editor.afterCommand');
});
$palette.on('mousedown', '.note-custom-color', this._onCustomColor.bind(this, eventName));
return $palette;
},
/**
* Change the selection's fore color.
*
* @param {string} color (hexadecimal or class name)
*/
changeForeColor: function (color) {
this._applyFont(color || 'text-undefined', null, null);
},
/**
* Change the selection's background color.
*
* @param {string} color (hexadecimal or class name)
*/
changeBgColor: function (color) {
this._applyFont(null, color || 'bg-undefined', null);
},
/**
* Change the selection's font size.
*
* @param {integer} fontsize
*/
changeFontSize: function (fontsize) {
this._applyFont(null, null, fontsize || 'inherit');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Applies the given styles (fore- or backcolor, font size)
* to a given <font> node.
*
* @private
* @param {Node} node
* @param {string} color (hexadecimal or class name)
* @param {string} bgcolor (hexadecimal or class name)
* @param {integer} size
* @returns {Node} the <font> node
*/
_applyStylesToFontNode: function (node, color, bgcolor, size) {
var className = node.className.split(this.context.invoke('HelperPlugin.getRegex', 'space'));
var k;
if (color) {
for (k = 0; k < className.length; k++) {
if (className[k].length && className[k].slice(0, 5) === "text-") {
className.splice(k, 1);
k--;
}
}
if (color === 'text-undefined') {
node.className = className.join(" ");
node.style.color = "inherit";
} else if (color.indexOf('text-') !== -1) {
node.className = className.join(" ") + " " + color;
node.style.color = "inherit";
} else {
node.className = className.join(" ");
node.style.color = color;
}
}
if (bgcolor) {
for (k = 0; k < className.length; k++) {
if (className[k].length && className[k].slice(0, 3) === "bg-") {
className.splice(k, 1);
k--;
}
}
if (bgcolor === 'bg-undefined') {
node.className = className.join(" ");
node.style.backgroundColor = "inherit";
} else if (bgcolor.indexOf('bg-') !== -1) {
node.className = className.join(" ") + " " + bgcolor;
node.style.backgroundColor = "inherit";
} else {
node.className = className.join(" ");
node.style.backgroundColor = bgcolor;
}
}
if (size) {
node.style.fontSize = "inherit";
if (!isNaN(size) && Math.abs(parseInt(this.window.getComputedStyle(node).fontSize, 10) - size) / size > 0.05) {
node.style.fontSize = size + "px";
}
}
return node;
},
/**
* Applies the given styles (fore- or backcolor, font size) to the selection.
* If no text is selected, apply to the current text node, if any.
*
* @private
* @param {string} color (hexadecimal or class name)
* @param {string} bgcolor (hexadecimal or class name)
* @param {integer} fontsize
*/
_applyFont: function (color, bgcolor, size) {
var self = this;
var r = this.context.invoke('editor.createRange');
if (!r || !this.$editable.has(r.sc).length || !this.$editable.has(r.ec).length) {
return;
}
var target;
var font;
if (r.isCollapsed()) {
if (dom.isIcon(r.sc)) {
target = dom.lastAncestor(r.sc, dom.isIcon);
} else {
target = this.context.invoke('editor.restoreTarget');
if (target && dom.isIcon(target)) {
r = this.context.invoke('editor.setRange', target, 0);
} else if (dom.isText(r.sc)) {
font = dom.create("font");
font.appendChild(this.document.createTextNode('\u200B'));
var fontParent = dom.ancestor(r.sc, function (n) {
return n.tagName === 'FONT';
});
var right;
if (fontParent) {
right = this.context.invoke('HelperPlugin.splitTree', fontParent, r.getStartPoint());
} else {
right = r.sc.splitText(r.so);
}
$(right).before(font);
font = this._applyStylesToFontNode(font, color, bgcolor, size);
r = this.context.invoke('editor.setRange', font, 1);
r.select();
return;
}
}
}
var startPoint = r.getStartPoint();
var endPoint = r.getEndPoint();
if (startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset]) {
startPoint.node = startPoint.node.childNodes[startPoint.offset];
startPoint.offset = 0;
}
if (endPoint.node.tagName && endPoint.node.childNodes[endPoint.offset]) {
endPoint.node = endPoint.node.childNodes[endPoint.offset];
endPoint.offset = 0;
}
// get first and last point
var ancestor;
var node;
if (!r.isCollapsed()) {
if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
ancestor = dom.lastAncestor(endPoint.node, dom.isFont) || endPoint.node;
dom.splitTree(ancestor, endPoint);
}
if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
ancestor = dom.lastAncestor(startPoint.node, dom.isFont) || startPoint.node;
node = dom.splitTree(ancestor, startPoint);
if (endPoint.node === startPoint.node) {
endPoint.node = node;
endPoint.offset = dom.nodeLength(node);
}
startPoint.node = node;
startPoint.offset = 0;
}
}
// get list of nodes to change
var nodes = [];
dom.walkPoint(startPoint, endPoint, function (point) {
var node = point.node;
if (((dom.isText(node) && self.context.invoke('HelperPlugin.isVisibleText', node)) || dom.isIcon(node)) &&
(node !== endPoint.node || endPoint.offset)) {
nodes.push(point.node);
}
});
nodes = _.unique(nodes);
// if fontawesome
if (r.isCollapsed()) {
nodes.push(startPoint.node);
}
// apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
var $font;
var fonts = [];
var style;
var className;
var i;
if (color || bgcolor || size) {
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
font = dom.lastAncestor(node, dom.isFont);
if (!font) {
if (node.textContent.match(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace'))) {
node.textContent = node.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace', 'g'), '\u00A0');
}
font = dom.create("font");
node.parentNode.insertBefore(font, node);
font.appendChild(node);
}
fonts.push(font);
this._applyStylesToFontNode(font, color, bgcolor, size);
}
}
// remove empty values
// we must remove the value in 2 steps (applay inherit then remove) because some
// browser like chrome have some time an error for the rendering and/or keep inherit
for (i = 0; i < fonts.length; i++) {
font = fonts[i];
if (font.style.color === "inherit") {
font.style.color = "";
}
if (font.style.backgroundColor === "inherit") {
font.style.backgroundColor = "";
}
if (font.style.fontSize === "inherit") {
font.style.fontSize = "";
}
$font = $(font);
if (font.style.color === '' && font.style.backgroundColor === '' && font.style.fontSize === '') {
$font.removeAttr("style");
}
if (!font.className.length) {
$font.removeAttr("class");
}
}
// target the deepest node
if (startPoint.node.tagName && !startPoint.offset) {
startPoint.node = this.context.invoke('HelperPlugin.firstLeaf', startPoint.node.childNodes[startPoint.offset] || startPoint.node);
startPoint.offset = 0;
}
if (endPoint.node.tagName && !endPoint.offset) {
endPoint.node = this.context.invoke('HelperPlugin.firstLeaf', endPoint.node.childNodes[endPoint.offset] || endPoint.node);
endPoint.offset = 0;
}
// select nodes to clean (to remove empty font and merge same nodes)
nodes = [];
dom.walkPoint(startPoint, endPoint, function (point) {
nodes.push(point.node);
});
nodes = _.unique(nodes);
// remove node without attributes (move content), and merge the same nodes
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
if (dom.isText(node) && !this.context.invoke('HelperPlugin.isVisibleText', node)) {
continue;
}
font = dom.lastAncestor(node, dom.isFont);
node = font || dom.ancestor(node, dom.isSpan);
if (!node) {
continue;
}
$font = $(node);
className = this.context.invoke('HelperPlugin.orderClass', node);
style = this.context.invoke('HelperPlugin.orderStyle', node);
if (!className && !style) {
$(node).before($(node).contents());
if (endPoint.node === node) {
endPoint = dom.prevPointUntil(endPoint, function (point) {
return point.node !== node;
});
}
$(node).remove();
nodes.splice(i, 1);
i--;
continue;
}
var prev = font && font.previousSibling;
while (prev && !font.tagName && !this.context.invoke('HelperPlugin.isVisibleText', prev)) {
prev = prev.previousSibling;
}
if (prev &&
font.tagName === prev.tagName &&
className === this.context.invoke('HelperPlugin.orderClass', prev) && style === this.context.invoke('HelperPlugin.orderStyle', prev)) {
$(prev).append($(font).contents());
if (endPoint.node === font) {
endPoint = dom.prevPointUntil(endPoint, function (point) {
return point.node !== font;
});
}
$(font).remove();
nodes.splice(i, 1);
i--;
continue;
}
}
// restore selection
r = this.context.invoke('editor.setRange', startPoint.node, startPoint.offset, endPoint.node, endPoint.offset);
r.normalize().select();
if (target) {
this.context.invoke('MediaPlugin.updatePopoverAfterEdit', target);
}
},
/**
* Creates the backcolor button containing its palette.
*
* @private
* @returns {jQuery} the backcolor button
*/
_createBackColorButton: function () {
var $bgPalette = this.createPalette('backColor');
$bgPalette.find("button:not(.note-color-btn)")
.addClass("note-color-btn")
.attr('data-event', 'backColor')
.each(function () {
var $el = $(this);
var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color');
$el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className);
});
var $bgContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
className: 'note-bg-color',
icon: this.options.icons.bg,
}));
$bgContainer.find('.dropdown-menu').append($bgPalette);
return $bgContainer;
},
/**
* Creates the forecolor button containing its palette.
*
* @private
* @returns {jQuery} the forecolor button
*/
_createForeColorButton: function () {
var $forePalette = this.createPalette('foreColor');
$forePalette.find("button:not(.note-color-btn)")
.addClass("note-color-btn")
.attr('data-event', 'foreColor')
.each(function () {
var $el = $(this);
var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color');
$el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color'));
});
var $foreContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
className: 'note-fore-color',
icon: this.options.icons.fore,
}));
$foreContainer.find('.dropdown-menu').append($forePalette);
return $foreContainer;
},
/**
* Method called on custom color button click :
* opens the color picker dialog and saves the chosen color on save.
*
* @private
* @param {string} targetColor
* @param {jQuery Event} ev
*/
_onCustomColor: function (targetColor, ev) {
ev.preventDefault();
ev.stopPropagation();
var self = this;
var $button = $(ev.target).next('button');
var target = this.context.invoke('editor.restoreTarget');
var colorPickerDialog = new ColorpickerDialog(this, {});
this.context.invoke('editor.saveRange');
colorPickerDialog.on('colorpicker:saved', this, this._wrapCommand(function (ev) {
$button = $button.clone().appendTo($button.parent());
$button.show();
$button.css('background-color', ev.data.cssColor);
$button.attr('data-value', ev.data.cssColor);
$button.data('value', ev.data.cssColor);
$button.attr('title', ev.data.cssColor);
self.context.invoke('editor.saveTarget', target);
self.context.invoke('editor.restoreRange');
$button.mousedown();
}));
colorPickerDialog.open();
this.context.invoke('MediaPlugin.hidePopovers');
},
});
_.extend(wysiwygOptions.icons, {
fore: 'fa fa-font',
bg: 'fa fa-paint-brush',
});
_.extend(wysiwygTranslation.color, {
customColor: _t('Custom color'),
fore: _t('Color'),
bg: _t('Background color'),
});
registry.add('FontPlugin', FontPlugin);
registry.addXmlDependency('/web_editor/static/src/xml/wysiwyg_colorpicker.xml');
registry.addJob(function (wysiwyg) {
if ('web_editor.colorpicker' in QWeb.templates) {
return;
}
if (wysiwyg.isDestroyed()) {
throw new Error('The Wysiwyg are destroyed before this loading');
}
var options = {};
wysiwyg.trigger_up('getRecordInfo', {
recordInfo: options,
callback: function (recordInfo) {
_.defaults(options, recordInfo);
},
});
return wysiwyg._rpc({
model: 'ir.ui.view',
method: 'read_template',
args: ['web_editor.colorpicker'],
kwargs: {
context: options.context,
},
}).then(function (template) {
if (!/^<templates>/.test(template)) {
template = _.str.sprintf('<templates>%s</templates>', template);
}
QWeb.add_template(template);
});
});
return FontPlugin;
});
@@ -1,31 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.font.buttons', function (require) {
'use strict';
var ButtonsPlugin = require('web_editor.wysiwyg.plugin.buttons');
//--------------------------------------------------------------------------
// override the ColorPicker button into the Toolbar and the font size button
//--------------------------------------------------------------------------
ButtonsPlugin.include({
/**
* @override
*/
addToolbarButtons: function () {
var self = this;
this._super();
this.context.memo('button.colorpicker', function () {
return self.context.invoke('FontPlugin.createColorPickerButtons');
});
var fontsizeFunction = this.context.memo('button.fontsize');
this.context.memo('button.fontsize', function () {
return self.context.invoke('FontPlugin.overrideFontSizeButton', fontsizeFunction());
});
},
});
return ButtonsPlugin;
});
@@ -1,27 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.HelpDialog', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
/**
* Allows to customize link content and style.
*/
var HelpDialog = Plugins.helpDialog.extend({
/**
* Restore the hidden close button.
*/
showHelpDialog: function () {
var self = this;
return this._super().then(function () {
self.$dialog.find('button.close span').attr('aria-hidden', 'false');
});
},
});
registry.add('helpDialog', HelpDialog);
return {
HelpDialog: HelpDialog,
};
});
File diff suppressed because it is too large Load Diff
@@ -1,139 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.hint', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var dom = $.summernote.dom;
var HintPlugin = Plugins.hintPopover.extend({
init: function (context) {
context.options.hint = (context.options.hint || []).concat(this._hints());
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public summernote module API
//--------------------------------------------------------------------------
/**
* Replace the current hint.
*/
replace: function () {
var self = this;
var $item = this.$content.find('.note-hint-item.active');
if ($item.length) {
this.lastWordRange.select();
this.context.invoke('HelperPlugin.deleteSelection');
var range = this.context.invoke('editor.createRange');
this.nodeFromItem($item).each(function () {
$(range.sc).after(this);
range = self.context.invoke('editor.setRange', this, dom.nodeLength(this));
});
range.select();
this.context.invoke('editor.saveRange');
this.lastWordRange = null;
this.hide();
this.context.triggerEvent('change', this.$editable.html(), this.$editable[0]);
this.context.invoke('editor.focus');
}
},
/**
* @param {JQueryEvent} e
*/
handleKeyup: function (e) {
var self = this;
if ([13, 38, 40].indexOf(e.keyCode) === -1) { // enter, up, down
var wordRange = this.context.invoke('editor.createRange');
var keyword_1 = wordRange.sc.textContent.slice(0, wordRange.so);
if (this.hints.length && keyword_1) {
this.$content.empty();
this.$popover.hide();
this.lastWordRange = wordRange;
var hasMatch = false;
// test all hints
this.hints.forEach(function (hint, idx) {
var match = keyword_1.match(hint.match);
if (match) {
hasMatch = true;
wordRange.so = wordRange.eo - match[0].length;
self.createGroup(idx, match[0]).appendTo(self.$content);
}
});
if (!hasMatch) {
return;
}
// select first .note-hint-item
this.$content.find('.note-hint-item:first').addClass('active');
// set position for popover after group is created
var rect = wordRange.getClientRects()[0];
var bnd = {
top: rect.top + $(this.document).scrollTop(),
left: rect.left + $(this.document).scrollLeft(),
};
this.$popover.css({
left: bnd.left,
top: bnd.top + (this.direction === 'top' ? -this.$popover.outerHeight() : (rect.bottom - rect.top)) - 5,
});
} else {
this.hide();
}
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Get hint objects.
*
* @private
* @returns {Object[]} hints
*/
_hints: function () {
var self = this;
return [{
className: 'o_hint_partner',
match: /\B@(\w+(\s\w*)?)$/,
search: function (keyword, callback) {
self._rpc({
model: 'res.partner',
method: "search_read",
fields: ['id', 'name', 'email'],
domain: [
['name', 'ilike', keyword],
],
limit: 10,
}).then(callback);
},
template: function (partner) {
return partner.name + (partner.email ? ' <i style="color: #999;">(' + partner.email + ')</i>' : '');
},
content: function (item) {
return $(self.document.createTextNode('@' + item.name + '\u00A0'));
},
},
{
className: 'fa',
match: /:([\-+\w]+)$/,
search: function () {},
template: function () {
return '<span class="fa fa-star">\u200B</span>';
},
content: function () {}
},
];
},
});
registry.add('hintPopover', null);
return HintPlugin;
});
@@ -1,87 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.history', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var HistoryPlugin = Plugins.history.extend({
/**
* Apply a snapshot.
*
* @override
*/
applySnapshot: function () {
this.context.invoke('MediaPlugin.hidePopovers');
try {
this._super.apply(this, arguments);
} catch (e) {
console.error(e);
}
this.context.invoke('editor.focus');
var newRange = this.context.invoke('editor.createRange');
if (!this.options.isEditableNode(newRange.sc)) {
var firstEditableSel = '[contenteditable="true"]:not(":hidden")';
var sc = this.$editable.find(firstEditableSel)[0];
this.context.invoke('editor.setRange', sc, 0, sc, 0).select();
}
},
/**
* Clear the history.
*
* @override
*/
clear: function () {
this.stack = [];
this.stackOffset = -1;
this.recordUndo();
},
/**
* Get the current history stack and the current offset.
*
* @returns {Object} {stack: Object, stackOffset: Integer}
*/
getHistoryStep: function () {
return {
stack: this.stack,
stackOffset: this.stackOffset,
};
},
/**
* Prevent errors with first snapshot.
*
* @override
*/
makeSnapshot: function () {
var rng = $.summernote.range.create(this.editable);
var snapshot = this._super();
if (rng.sc === this.editable || $(rng.sc).has(this.editable).length) {
snapshot.bookmark.s.path = snapshot.bookmark.e.path = [0];
snapshot.bookmark.s.offset = snapshot.bookmark.e.offset = 0;
}
return snapshot;
},
/**
* @override
*/
recordUndo: function () {
if (!this.stack[this.stackOffset] || this.$editable.html() !== this.stack[this.stackOffset].contents) {
this._super();
}
},
/**
* @override
*/
undo: function () {
if (this.stackOffset > 0) {
this.stackOffset--;
}
this.applySnapshot(this.stack[this.stackOffset]);
},
});
registry.add('HistoryPlugin', HistoryPlugin);
return HistoryPlugin;
});
File diff suppressed because it is too large Load Diff
@@ -1,493 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.link', function (require) {
'use strict';
var core = require('web.core');
var LinkDialog = require('wysiwyg.widgets.LinkDialog');
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var _t = core._t;
var dom = $.summernote.dom;
dom.isAnchor = function (node) {
return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
!$(node).hasClass('fa') && !$(node).hasClass('o_image');
};
//--------------------------------------------------------------------------
// link
//--------------------------------------------------------------------------
var LinkPlugin = Plugins.linkDialog.extend({
events: {
'dblclick .note-editable': '_onDblclick',
},
//--------------------------------------------------------------------------
// Public summernote module API
//--------------------------------------------------------------------------
/**
* @param {Object} layoutInfo
*/
show: function () {
var self = this;
var linkInfo = this.context.invoke('editor.getLinkInfo');
this.context.invoke('editor.saveRange');
return this.showLinkDialog(linkInfo)
.then(function (linkInfo) {})
.guardedCatch(function () {});
},
/**
* @override
* @param {Object} linkInfo
* @returns {Promise}
*/
showLinkDialog: function (linkInfo) {
var self = this;
this.context.invoke('editor.hidePopover');
var media = this.context.invoke('editor.restoreTarget');
if (linkInfo.range) {
var r = linkInfo.range.getPoints();
// move carret in icon, video...
if (!media && (dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon))) {
media = dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon);
}
// if select a text content in anchor then click on image then click on anchor button
if (dom.isImg(media) || dom.isIcon(media)) {
r.sc = media.parentNode;
r.so = [].indexOf.call(media.parentNode.childNodes, media);
r.ec = media;
r.eo = dom.nodeLength(media);
linkInfo.range.sc = r.sc;
linkInfo.range.so = r.so;
linkInfo.range.ec = r.ec;
linkInfo.range.eo = r.eo;
}
linkInfo.isAnchor = linkInfo.range.isOnAnchor();
linkInfo.className = linkInfo.isAnchor ? dom.ancestor(r.sc, dom.isAnchor).className : '';
linkInfo.url = linkInfo.isAnchor ? ($(dom.ancestor(r.sc, dom.isAnchor)).attr('href') || '').replace(window.location.origin, '') : '';
var nodes = [];
if (linkInfo.isAnchor) {
nodes = dom.ancestor(r.sc, dom.isAnchor).childNodes;
} else if (!linkInfo.range.isCollapsed()) {
if (dom.isImg(media) || dom.isIcon(media)) {
nodes.push(media);
} else {
if (r.sc.tagName) {
r.sc = (r.so ? r.sc.childNodes[r.so] : r.sc).firstChild || r.sc;
r.so = 0;
} else if (r.so !== r.sc.textContent.length) {
if (r.sc === r.ec) {
r.ec = r.sc = r.sc.splitText(r.so);
r.eo -= r.so;
} else {
r.sc = r.sc.splitText(r.so);
}
r.so = 0;
}
if (r.ec.tagName) {
r.ec = (r.eo ? r.ec.childNodes[r.eo - 1] : r.ec).lastChild || r.ec;
r.eo = r.ec.textContent.length;
} else if (r.eo !== r.ec.textContent.length) {
r.ec.splitText(r.eo);
}
// browsers can't target a picture or void node
if (dom.isVoid(r.sc) || dom.isImg(r.sc)) {
r.so = dom.listPrev(r.sc).length - 1;
r.sc = r.sc.parentNode;
}
if (dom.isBR(r.ec)) {
r.eo = dom.listPrev(r.ec).length - 1;
r.ec = r.ec.parentNode;
} else if (dom.isVoid(r.ec) || dom.isImg(r.sc)) {
r.eo = dom.listPrev(r.ec).length;
r.ec = r.ec.parentNode;
}
linkInfo.range.sc = r.sc;
linkInfo.range.so = r.so;
linkInfo.range.ec = r.ec;
linkInfo.range.eo = r.eo;
linkInfo.range.select();
this.context.invoke('editor.saveRange');
linkInfo.range = this.context.invoke('editor.createRange');
// search nodes to insert in the anchor
var startPoint = this.context.invoke('HelperPlugin.makePoint', r.sc, r.so);
var endPoint = this.context.invoke('HelperPlugin.makePoint', r.ec, r.eo);
dom.walkPoint(startPoint, endPoint, function (point) {
var node = point.node.childNodes && point.node.childNodes[point.offset] || point.node;
nodes.push(node);
});
nodes = _.filter(_.uniq(nodes), function (node) {
return nodes.indexOf(node.parentNode) === -1;
});
}
}
if (nodes.length > 0) {
var text = "";
linkInfo.images = [];
for (var i = 0; i < nodes.length; i++) {
if (dom.ancestor(nodes[i], dom.isImg)) {
text += dom.ancestor(nodes[i], dom.isImg).outerHTML;
} else if (dom.ancestor(nodes[i], dom.isIcon)) {
text += dom.ancestor(nodes[i], dom.isIcon).outerHTML;
} else if (!linkInfo.isAnchor && nodes[i].nodeType === 1) {
// just use text nodes from listBetween
} else if (!linkInfo.isAnchor && i === 0) {
text += nodes[i].textContent;
} else if (!linkInfo.isAnchor && i === nodes.length - 1) {
text += nodes[i].textContent;
} else {
text += nodes[i].textContent;
}
}
linkInfo.text = text.replace(this.context.invoke('HelperPlugin.getRegex', 'space', 'g'), ' ');
}
linkInfo.needLabel = !linkInfo.text.length;
}
var def = new Promise(function (resolve, reject) {
var linkDialog = new LinkDialog(self.options.parent, {
onClose: function () {
setTimeout(function () {
self.context.invoke('editor.focus');
});
}
},
_.omit(linkInfo, 'range')
);
linkDialog.on('save', self, self._wrapCommand(function (newLinkInfo) {
var isCollapsed = linkInfo.range.isCollapsed();
linkInfo.range.select();
newLinkInfo.range = linkInfo.range;
var $anchor;
if (linkInfo.isAnchor) {
$anchor = $(dom.ancestor(r.sc, dom.isAnchor));
$anchor.css(newLinkInfo.style || {});
if (newLinkInfo.isNewWindow) {
$anchor.attr('target', '_blank');
} else {
$anchor.removeAttr('target');
}
} else {
self.context.invoke('editor.saveRange');
self.context.invoke('editor.createLink', newLinkInfo);
var range = self.context.invoke('editor.createRange');
var anchor = dom.ancestor(range.sc.childNodes[range.so] || range.sc, dom.isAnchor);
$anchor = $(anchor);
if (isCollapsed) {
// move the range just after the link
var point = dom.nextPoint({
node: anchor,
offset: dom.nodeLength(anchor),
});
range = self.context.invoke('editor.setRange', point.node, point.offset);
range.select();
} else {
$anchor.selectContent();
}
}
if ((dom.isImg(media) || dom.isIcon(media)) && !$anchor.find(media).length) {
$(media).remove();
}
$anchor.attr('class', newLinkInfo.className);
$anchor.attr('href', newLinkInfo.url);
self.context.invoke('editor.saveRange');
self.context.invoke('editor.saveTarget', $anchor[0]);
self.context.triggerEvent('focusnode', $anchor[0]);
resolve(linkInfo);
}));
linkDialog.on('closed', self, function () {
this.context.invoke('editor.restoreRange');
this.context.invoke('LinkPopover.update');
reject();
});
linkDialog.open();
});
return def;
},
/**
* Remove the current link, keep its contents.
*
* @override
*/
unlink: function () {
var rng = this.context.invoke('editor.createRange');
var anchor = rng.sc;
while (anchor && anchor.tagName !== 'A') {
anchor = anchor.parentElement;
}
if (!anchor) {
this.context.invoke('editor.hidePopover');
return;
}
var startAndEndInvisible = this.context.invoke('HelperPlugin.getRegex', 'startAndEndInvisible', 'g');
anchor.innerHTML = anchor.innerHTML.replace(startAndEndInvisible, '');
var $contents = $(anchor).contents();
$(anchor).before($contents).remove();
this.context.invoke('editor.hidePopover');
var start = $contents[0];
var end = $contents.last()[0];
rng = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
rng.select();
this.editable.normalize();
this.context.invoke('editor.saveRange');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_addButtons: function () {
var self = this;
this._super();
this.context.memo('help.LinkPlugin.show', this.options.langInfo.help['linkDialog.show']);
this.context.memo('button.linkPlugin', function () {
return self.context.invoke('buttons.button', {
contents: self.ui.icon(self.options.icons.link),
tooltip: self.lang.link.link + self.context.invoke('buttons.representShortcut', 'LinkPlugin.show'),
click: self.context.createInvokeHandler('LinkPlugin.show')
}).render();
});
},
/**
* @param {jQueryEvent} e
*/
_onDblclick: function (e) {
if (dom.ancestor(e.target, dom.isAnchor) && !dom.isMedia(e.target)) {
this.show();
}
},
});
var LinkPopover = Plugins.linkPopover.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Prevent links without text: replace empty text with the word "Label".
*
* @param {DOM} anchor
* @param {bool} select
*/
fillEmptyLink: function (anchor, select) {
if (dom.isAnchor(anchor)) {
var text = _t('Label');
if (this.context.invoke('HelperPlugin.getRegex', 'onlyEmptySpace').test(anchor.innerHTML)) {
$(anchor).contents().remove();
$(anchor).append(this.document.createTextNode(text));
}
var rng = this.context.invoke('editor.createRange');
if (select && anchor.innerHTML === text) {
this._cleanLastLink();
rng.sc = rng.ec = anchor.firstChild;
rng.so = 0;
rng.eo = dom.nodeLength(rng.sc);
rng.select();
this.context.invoke('editor.saveRange');
}
if (select) {
this.lastAnchor = anchor;
}
}
},
/**
* @override
*/
hide: function () {
this._cleanLastLink();
this._super();
},
/**
* @override
*/
update: function () {
var rng = this.context.invoke('editor.createRange');
var anchor = dom.ancestor(rng.sc, dom.isAnchor);
if (anchor && anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
anchor = dom.ancestor(rng.sc, dom.isAnchor);
if (!$(anchor).is(':o_editable')) {
this.hide();
return;
}
} else {
anchor = false;
}
if ($(anchor).data('toggle') === 'tab') {
anchor = false;
}
if (!this.options.displayPopover(anchor)) {
anchor = false;
}
if (anchor !== this.lastAnchor) {
this._cleanLastLink();
}
if (!anchor) {
this.hide();
return;
}
if (dom.isAnchor(anchor)) {
this.lastAnchor = anchor;
}
var $target = $(anchor);
if (!$target.data('show_tooltip') && this.context.options.tooltip) {
$target.data('show_tooltip', true);
setTimeout(function () {
$target.tooltip({
title: _t('Double-click to edit'),
trigger: 'manual',
container: this.document.body,
placement: 'top'
}).tooltip('show');
setTimeout(function () {
$target.tooltip('dispose');
}, 2000);
}, 400);
}
var innerHTML = anchor.innerHTML.replace('&nbsp;', '\u00A0').replace('&#8203;', '\u200B');
// prevent links without text
this.fillEmptyLink(anchor, true);
// add invisible char to prevent the carret to leave the link at the begin or end
if (innerHTML[0] !== '\u200B') {
var before = this.document.createTextNode('\u200B');
$(anchor).prepend(before);
}
if (innerHTML[innerHTML.length - 1] !== '\u200B') {
var after = this.document.createTextNode('\u200B');
$(anchor).append(after);
}
anchor.normalize();
var href = $(anchor).attr('href');
this.$popover.find('a').attr('href', href).html(href);
var pos = $(anchor).offset();
var posContainer = $(this.options.container).offset();
pos.left = pos.left - posContainer.left + 10;
pos.top = pos.top - posContainer.top + $(anchor).outerHeight();
this.$popover.css({
display: 'block',
left: pos.left,
top: pos.top,
});
this.context.layoutInfo.editor.after(this.$popover);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Clean the last link and prevent links without text.
*
* @private
*/
_cleanLastLink: function () {
if (this.lastAnchor) {
this.fillEmptyLink(this.lastAnchor);
var range = this.context.invoke('editor.createRange');
var rangeChange;
var innerHTML = this.lastAnchor.innerHTML.replace('&nbsp;', '\u00A0').replace('&#8203;', '\u200B');
// prevent links without text
if (
this.$editable.has(this.lastAnchor).length &&
this.context.invoke('HelperPlugin.getRegexBlank', {
space: true,
invisible: true,
nbsp: true,
}).test(innerHTML)
) {
$(this.lastAnchor).contents().remove();
$(this.lastAnchor).append(this.document.createTextNode(_t('Label')));
range.sc = range.ec = this.lastAnchor.firstChild;
range.so = 0;
range.eo = dom.nodeLength(range.sc);
rangeChange = true;
}
var firstChild = this.context.invoke('HelperPlugin.firstLeaf', this.lastAnchor);
if (
!firstChild.tagName &&
this.context.invoke('HelperPlugin.getRegex', 'startInvisible').test(firstChild.textContent)
) {
firstChild.textContent = firstChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startInvisible'), '');
if (range.sc === firstChild && range.so) {
range.so -= 1;
rangeChange = true;
}
if (range.ec === firstChild && range.eo) {
range.eo -= 1;
rangeChange = true;
}
}
var lastChild = this.context.invoke('HelperPlugin.lastLeaf', this.lastAnchor);
if (
lastChild.textContent.length > 1 &&
!lastChild.tagName &&
this.context.invoke('HelperPlugin.getRegex', 'endInvisible').test(lastChild.textContent)
) {
lastChild.textContent = lastChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'endInvisible'), '');
if (range.sc === lastChild && range.so > dom.nodeLength(lastChild)) {
range.so = dom.nodeLength(lastChild);
rangeChange = true;
}
if (range.ec === lastChild && range.eo > dom.nodeLength(lastChild)) {
range.eo = dom.nodeLength(lastChild);
rangeChange = true;
}
}
if (rangeChange) {
range.select();
this.context.invoke('editor.saveRange');
}
this.lastAnchor = null;
}
},
});
registry.add('LinkPlugin', LinkPlugin)
.add('LinkPopover', LinkPopover)
.add('linkDialog', null)
.add('linkPopover', null)
.add('autoLink', null);
return {
LinkPlugin: LinkPlugin,
LinkPopover: LinkPopover,
};
});
File diff suppressed because it is too large Load Diff
@@ -1,78 +0,0 @@
odoo.define('web_editor.wysiwyg.plugins', function (require) {
'use strict';
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var registry = require('web_editor.wysiwyg.plugin.registry');
var wysiwygOptions = require('web_editor.wysiwyg.options');
var plugins = _.mapObject(wysiwygOptions.modules, function (Module, pluginName) {
var prototype = {
init: function () {
var self = this;
this._super.apply(this, arguments);
var events = _.clone(this.events);
this.summernote.options.modules[pluginName].apply(this, arguments);
_.each(events, function (value, key) {
self.events[key] = value;
});
},
};
_.each(Module.prototype, function (prop, name) {
if (typeof prop === 'function') {
prototype[name] = function () {
return this.summernote.options.modules[pluginName].prototype[name].apply(this, arguments);
};
} else {
prototype[name] = prop;
}
});
var Plugin = AbstractPlugin.extend(prototype).extend({
destroy: function () {
if (this.shouldInitialize()) {
this._super();
}
},
});
// override summernote default buttons
registry.add(pluginName, Plugin);
return Plugin;
});
// export table plugin to convert it in module (see editor)
var $textarea = $('<textarea>');
$('body').append($textarea);
$textarea.summernote();
var summernote = $textarea.data('summernote');
_.each(['style', 'table', 'typing', 'bullet', 'history'], function (name) {
var prototype = {};
for (var k in summernote.modules.editor[name]) {
prototype[k] = summernote.modules.editor[name][k];
}
plugins[name] = AbstractPlugin.extend(prototype);
});
var History = summernote.modules.editor.history.constructor;
plugins.history.include({
init: function (context) {
this._super(context);
History.call(this, this.$editable);
},
});
try {
$textarea.summernote('destroy');
} catch (e) {
summernote.layoutInfo.editor.remove();
}
$textarea.remove();
return plugins;
});
@@ -1,244 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.table', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var dom = $.summernote.dom;
var TablePlugin = Plugins.table.extend({
initialize: function () {
this._super.apply(this, arguments);
var self = this;
// We need setTimeout to make sure to initialize after HelperPlugin and HistoryPlugin
setTimeout(function () {
// contentEditable fail for image and font in table
// user must use right arrow the number of space but without feedback
self.$editable.find('td:has(img, span.fa)').each(function () {
if (this.firstChild && !this.firstChild.tagName) {
var startSpace = self.context.invoke('HelperPlugin.getRegex', 'startSpace');
this.firstChild.textContent = this.firstChild.textContent.replace(startSpace, ' ');
}
if (this.lastChild && !this.lastChild.tagName) {
var endSpace = self.context.invoke('HelperPlugin.getRegex', 'endSpace');
this.lastChild.textContent = this.lastChild.textContent.replace(endSpace, ' ');
}
});
self.context.invoke('HistoryPlugin.clear');
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Add a new col and
* wrap contents of the new cells in p elements.
*
* @see summernote library
*
* @override
* @param {WrappedRange} rng
* @param {String('left'|'right')} position
*/
addCol: function (rng, position) {
this._super.apply(this, arguments);
var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
var table = dom.ancestor(cell, function (n) {
return n.tagName === 'TABLE';
});
var newColIndex = $(cell)[position === 'right' ? 'next' : 'prev']('td').index() + 1;
$(table).find('td:nth-child(' + newColIndex + ')').contents().wrap('<p></p>');
},
/**
* Add a new row and
* wrap contents of the new cells in p elements.
*
* @see summernote library
*
* @override
* @param {WrappedRange} rng
* @param {String('top'|'bottom')} position
*/
addRow: function (rng, position) {
this._super.apply(this, arguments);
var row = dom.ancestor(rng.commonAncestor(), dom.isCell).parentElement;
$(row)[position === 'bottom' ? 'next' : 'prev']('tr').find('td').contents().wrap('<p></p>');
},
/**
* Create empty table element and
* wrap the contents of all cells in p elements.
*
* @see summernote library
*
* @override
* @param {Number} rowCount
* @param {Number} colCount
* @returns {Node} table
*/
createTable: function () {
var table = this._super.apply(this, arguments);
$(table).find('td').contents().wrap('<p></p>');
return table;
},
/**
* @see summernote library
*
* @override
*/
deleteCol: function () {
var range = this.context.invoke('editor.createRange');
// Delete the last remaining column === delete the table
var cell = dom.ancestor(range.commonAncestor(), dom.isCell);
if (cell && !cell.previousElementSibling && !cell.nextElementSibling) {
return this.deleteTable();
}
var neighbor = cell.previousElementSibling || cell.nextElementSibling;
this._super.apply(this, arguments);
// Put the range back on the previous or next cell after deleting
// to allow chain-removing
range = this.context.invoke('editor.createRange');
if (range.sc.tagName !== 'TD' && neighbor) {
range = this.context.invoke('editor.setRange', neighbor, 0);
range.normalize().select();
}
},
/**
* @see summernote library
*
* @override
*/
deleteRow: function () {
var range = this.context.invoke('editor.createRange');
// Delete the last remaining row === delete the table
var row = dom.ancestor(range.commonAncestor(), function (n) {
return n.tagName === 'TR';
});
if (row && !row.previousElementSibling && !row.nextElementSibling) {
return this.deleteTable();
}
var neighbor = row.previousElementSibling || row.nextElementSibling;
this._super.apply(this, arguments);
// Put the range back on the previous or next row after deleting
// to allow chain-removing
range = this.context.invoke('editor.createRange');
if (range.sc.tagName !== 'TR' && neighbor) {
range = this.context.invoke('editor.setRange', neighbor, 0);
range.normalize().select();
}
},
/**
* Delete the table in range.
*/
deleteTable: function () {
var range = this.context.invoke('editor.createRange');
var cell = dom.ancestor(range.commonAncestor(), dom.isCell);
var table = $(cell).closest('table')[0];
var point = this.context.invoke('HelperPlugin.removeBlockNode', table);
range = this.context.invoke('editor.setRange', point.node, point.offset);
range.normalize().select();
},
/**
* Insert a table.
* Note: adds <p><br></p> before/after the table if the table
* has nothing brefore/after it, so as to allow the carret to move there.
*
* @param {String} dim dimension of table (ex : "5x5")
*/
insertTable: function (dim) {
var dimension = dim.split('x');
var table = this.createTable(dimension[0], dimension[1], this.options);
this.context.invoke('HelperPlugin.insertBlockNode', table);
var p;
if (!table.previousElementSibling) {
p = this.document.createElement('p');
$(p).append(this.document.createElement('br'));
$(table).before(p);
}
if (!table.nextElementSibling) {
p = this.document.createElement('p');
$(p).append(this.document.createElement('br'));
$(table).after(p);
}
var range = this.context.invoke('editor.setRange', $(table).find('td')[0], 0);
range.normalize().select();
this.context.invoke('editor.saveRange');
},
});
var TablePopover = Plugins.tablePopover.extend({
events: _.defaults({
'summernote.scroll': '_onScroll',
}, Plugins.tablePopover.prototype.events),
/**
* Update the table's popover and its position.
*
* @override
* @param {Node} target
* @returns {false|Node} the selected cell (on which to display the popover)
*/
update: function (target) {
if (!target || this.context.isDisabled()) {
return false;
}
var cell = dom.ancestor(target, dom.isCell);
if (!!cell && this.options.isEditableNode(cell)) {
var pos = $(cell).offset();
var posContainer = $(this.options.container).offset();
pos.left = pos.left - posContainer.left + 10;
pos.top = pos.top - posContainer.top + $(cell).outerHeight() - 4;
this.lastPos = this.context.invoke('HelperPlugin.makePoint', target, $(target).offset());
this.$popover.css({
display: 'block',
left: pos.left,
top: pos.top,
});
} else {
this.hide();
}
return cell;
},
/**
* Update the target table and its popover.
*
* @private
*/
_onScroll: function () {
var range = this.context.invoke('editor.createRange');
var target = dom.ancestor(range.sc, dom.isCell);
if (!target || target === this.editable) {
return;
}
if (this.lastPos && this.lastPos.target === target && $(target).offset()) {
var newTop = $(target).offset().top;
var movement = this.lastPos.offset.top - newTop;
if (movement && this.mousePosition) {
this.mousePosition.pageY -= movement;
}
}
return this.update(target);
},
});
registry.add('TablePlugin', TablePlugin);
registry.add('TablePopover', TablePopover);
registry.add('tablePopover', null);
return TablePlugin;
});
@@ -1,811 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.text', function (require) {
'use strict';
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var registry = require('web_editor.wysiwyg.plugin.registry');
var dom = $.summernote.dom;
dom.isAnchor = function (node) {
return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
!$(node).hasClass('fa') && !$(node).hasClass('o_image');
};
var TextPlugin = AbstractPlugin.extend({
events: {
'summernote.paste': '_onPaste',
},
// See: https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#removeformat-candidate
formatTags: [
'abbr',
'acronym',
'b',
'bdi',
'bdo',
'big',
'blink',
'cite',
'code',
'dfn',
'em',
'font',
'i',
'ins',
'kbd',
'mark',
'nobr',
'q',
's',
'samp',
'small',
'span',
'strike',
'strong',
'sub',
'sup',
'tt',
'u',
'var',
],
tab: '\u00A0\u00A0\u00A0\u00A0',
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Insert a Horizontal Rule element (hr).
*/
insertHR: function () {
var self = this;
var hr = this.document.createElement('hr');
this.context.invoke('HelperPlugin.insertBlockNode', hr);
var point = this.context.invoke('HelperPlugin.makePoint', hr, 0);
point = dom.nextPointUntil(point, function (pt) {
return pt.node !== hr && !self.options.isUnbreakableNode(pt.node);
}) || point;
var range = $.summernote.range.create(point.node, point.offset);
range.select();
},
/**
* Insert a TAB (4 non-breakable spaces).
*/
insertTab: function () {
this.context.invoke('HelperPlugin.insertTextInline', this.tab);
},
/**
* Paste nodes or their text content into the editor.
*
* @param {Node[]} nodes
* @param {Boolean} textOnly true to allow only dropping plain text
*/
pasteNodes: function (nodes, textOnly) {
if (!nodes.length) {
return;
}
nodes = textOnly ? this.document.createTextNode($(nodes).text()) : nodes;
nodes = textOnly ? nodes : this._mergeAdjacentULs(nodes);
var point = this._getPastePoint();
// Prevent pasting HTML within a link:
point = textOnly ? point : dom.nextPointUntil(point, this._isPointInAnchor.bind(this));
this._insertNodesAt(nodes, point);
var start = nodes[nodes.length - 1];
this.context.invoke('editor.setRange', start, dom.nodeLength(start)).normalize().select();
},
/**
* Prepare clipboard data for safe pasting into the editor.
*
* @see clipboardWhitelist
* @see clipboardBlacklist
*
* @param {DOMString} clipboardData
* @returns {Node[]}
*/
prepareClipboardData: function (clipboardData) {
var $html = $('<wrapper>' + clipboardData + '</wrapper>').contents();
var $clipboardData = this._removeIllegalClipboardElements($html);
var $all = $clipboardData.find('*').addBack();
$all.filter('table').addClass('table table-bordered');
this._wrapTDContents($all.filter('td'));
this._fillEmptyBlocks($all);
this._removeIllegalClipboardAttributes($all);
$all.filter('a').removeClass();
$all.filter('img').css('max-width', '100%');
return $clipboardData.toArray();
},
/**
* Format a 'format' block: change its tagName (eg: p -> h1).
*
* @param {string} tagName
* P, H1, H2, H3, H4, H5, H6, BLOCKQUOTE, PRE
*/
formatBlock: function (tagName) {
var self = this;
var r = this.context.invoke('editor.createRange');
if (
!r ||
!this.$editable.has(r.sc).length ||
!this.$editable.has(r.ec).length ||
this.options.isUnbreakableNode(r.sc)
) {
return;
}
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
nodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
if (!nodes.length) {
var node = this.context.invoke('editor.createRange').sc;
if (node.tagName === 'BR' || dom.isText(node)) {
node = node.parentNode;
}
nodes = [node];
}
var changedNodes = [];
_.each(nodes, function (node) {
var newNode = self.document.createElement(tagName);
$(newNode).append($(node).contents());
var attributes = $(node).prop("attributes");
_.each(attributes, function (attr) {
$(newNode).attr(attr.name, attr.value);
});
$(node).replaceWith(newNode);
changedNodes.push(newNode);
});
// Select all formatted nodes
if (changedNodes.length) {
var lastNode = changedNodes[changedNodes.length - 1];
var startNode = changedNodes[0].firstChild || changedNodes[0];
var endNode = lastNode.lastChild || lastNode;
var range = this.context.invoke('editor.setRange', startNode, 0, endNode, dom.nodeLength(endNode));
range.select();
}
},
/**
* Change the paragraph alignment of a 'format' block.
*
* @param {string} style
* justifyLeft, justifyCenter, justifyRight, justifyFull
*/
formatBlockStyle: function (style) {
var self = this;
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
nodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
var align = style === 'justifyLeft' ? 'left' :
style === 'justifyCenter' ? 'center' :
style === 'justifyRight' ? 'right' : 'justify';
_.each(nodes, function (node) {
if (dom.isText(node)) {
return;
}
var textAlign = self.window.getComputedStyle(node).textAlign;
if (align !== textAlign) {
if (align !== self.window.getComputedStyle(node.parentNode).textAlign) {
$(node).css('text-align', align);
} else {
$(node).css('text-align', '');
}
}
});
this.editable.normalize();
},
/**
* (Un-)format text: make it bold, italic, ...
*
* @param {string} tagName
* bold, italic, underline, strikethrough, superscript, subscript
*/
formatText: function (tagName) {
var self = this;
var tag = {
bold: 'B',
italic: 'I',
underline: 'U',
strikethrough: 'S',
superscript: 'SUP',
subscript: 'SUB',
} [tagName];
if (!tag) {
throw new Error(tagName);
}
var range = this.context.invoke('editor.createRange');
if (!range || !this.$editable.has(range.sc).length || !this.$editable.has(range.ec).length) {
return;
}
if (range.isCollapsed()) {
var br;
if (range.sc.tagName === 'BR') {
br = range.sc;
} else if (range.sc.firstChild && range.sc.firstChild.tagName === 'BR') {
br = range.sc.firstChild;
}
if (br) {
var emptyText = this.document.createTextNode('\u200B');
$(br).before(emptyText).remove();
range = this.context.invoke('editor.setRange', emptyText, 0, emptyText, 1);
} else {
var offset = range.so;
this.context.invoke('HelperPlugin.insertTextInline', '\u200B');
range = this.context.invoke('editor.createRange');
range.so = offset;
range.eo = offset + 1;
}
range.select();
}
var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
var texts = this.context.invoke('HelperPlugin.filterLeafChildren', nodes);
var formatted = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
var start = this.context.invoke('HelperPlugin.firstLeaf', nodes[0]);
var end = this.context.invoke('HelperPlugin.lastLeaf', nodes[nodes.length - 1]);
function containsOnlySelectedText(node) {
return _.all(node.childNodes, function (n) {
return _.any(texts, function (t) {
return n === t && !(dom.isText(n) && n.textContent === '');
}) && containsOnlySelectedText(n);
});
}
function containsAllSelectedText(node) {
return _.all(texts, function (t) {
return _.any(node.childNodes, function (n) {
return n === t && !(dom.isText(n) && n.textContent === '') || containsAllSelectedText(n);
});
});
}
var nodeAlreadyStyled = [];
var toStyled = [];
var notStyled = _.filter(texts, function (text, index) {
if (toStyled.indexOf(text) !== -1 || nodeAlreadyStyled.indexOf(text) !== -1) {
return;
}
nodeAlreadyStyled.push(text);
end = text;
var styled = dom.ancestor(text, function (node) {
return node.tagName === tag;
});
if (styled) {
if (
!/^\u200B$/.test(text.textContent) &&
containsAllSelectedText(styled) &&
containsOnlySelectedText(styled)
) {
// Unwrap all contents
nodes = $(styled).contents();
$(styled).before(nodes).remove();
nodeAlreadyStyled.push.apply(nodeAlreadyStyled, nodes);
end = _.last(nodeAlreadyStyled);
} else {
var options = {
isSkipPaddingBlankHTML: true,
isNotSplitEdgePoint: true,
};
if (
nodeAlreadyStyled.indexOf(text.nextSibling) === -1 &&
!dom.isRightEdgeOf(text, styled)
) {
options.nextText = false;
var point = self.context.invoke('HelperPlugin.makePoint', text, dom.nodeLength(text));
if (dom.isMedia(text)) {
point = dom.nextPoint(point);
}
var next = self.context.invoke('HelperPlugin.splitTree', styled, point, options);
nodeAlreadyStyled.push(next);
}
if (
nodeAlreadyStyled.indexOf(text.previousSibling) === -1 &&
!dom.isLeftEdgeOf(text, styled)
) {
options.nextText = true;
var textPoint = self.context.invoke('HelperPlugin.makePoint', text, 0);
text = self.context.invoke('HelperPlugin.splitTree', styled, textPoint, options);
nodeAlreadyStyled.push(text);
if (index === 0) {
start = text;
}
end = text;
}
var toRemove = dom.ancestor(text, function (n) {
return n.tagName === tag;
});
if (toRemove) {
// Remove generated empty elements
if (
toRemove.nextSibling &&
self.context.invoke('HelperPlugin.isBlankNode', toRemove.nextSibling)
) {
$(toRemove.nextSibling).remove();
}
if (
toRemove.previousSibling &&
self.context.invoke('HelperPlugin.isBlankNode', toRemove.previousSibling)
) {
$(toRemove.previousSibling).remove();
}
// Unwrap the element
nodes = $(toRemove).contents();
$(toRemove).before(nodes).remove();
nodeAlreadyStyled.push.apply(nodeAlreadyStyled, nodes);
end = _.last(nodeAlreadyStyled);
}
}
}
if (dom.ancestor(text, function (node) {
return toStyled.indexOf(node) !== -1;
})) {
return;
}
var node = text;
while (
node && node.parentNode &&
formatted.indexOf(node) === -1 &&
formatted.indexOf(node.parentNode) === -1
) {
node = node.parentNode;
}
if (node !== text) {
if (containsAllSelectedText(node)) {
toStyled.push.apply(toStyled, texts);
node = text;
} else if (!containsOnlySelectedText(node)) {
node = text;
}
}
if (toStyled.indexOf(node) === -1) {
toStyled.push(node);
}
return !styled;
});
toStyled = _.uniq(toStyled);
if (notStyled.length) {
nodes = [];
var toMerge = [];
_.each(toStyled, function (node) {
var next = true;
if (node.nextSibling && node.nextSibling.tagName === tag) {
$(node.nextSibling).prepend(node);
next = false;
}
if (node.previousSibling && node.previousSibling.tagName === tag) {
$(node.previousSibling).append(node);
}
if (node.parentNode && node.parentNode.tagName !== tag) {
var styled = self.document.createElement(tag);
if (node.tagName) {
$(styled).append(node.childNodes);
$(node).append(styled);
} else {
$(node).before(styled);
styled.appendChild(node);
}
}
// Add adjacent nodes with same tagName to list of nodes to merge
if (
node.parentNode && node.parentNode[next ? 'nextSibling' : 'previousSibling'] &&
node.parentNode.tagName === node.parentNode[next ? 'nextSibling' : 'previousSibling'].tagName
) {
toMerge.push(next ? node.parentNode : node.parentNode.previousSibling);
}
});
// Merge what needs merging
while (toMerge.length) {
this.context.invoke('HelperPlugin.deleteEdge', toMerge.pop(), 'next');
}
}
range = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
if (range.sc === range.ec && range.sc.textContent === '\u200B') {
range.so = range.eo = 1;
}
range.select();
this.editable.normalize();
},
/**
* Remove format on the current range.
*
* @see _isParentRemoveFormatCandidate
*/
removeFormat: function () {
this._selectCurrentIfCollapsed();
var selectedText = this.context.invoke('HelperPlugin.getSelectedText');
var selectedIcons = this.context.invoke('HelperPlugin.getSelectedNodes');
selectedIcons = _.filter(selectedIcons, dom.isIcon);
if (!selectedText.length && !selectedIcons.length) {
return;
}
_.each(selectedIcons, this._removeIconFormat.bind(this));
_.each(selectedText, this._removeTextFormat.bind(this));
var startNode = selectedText[0];
var endNode = selectedText[selectedText.length - 1];
this.context.invoke('editor.setRange', startNode, 0, endNode, dom.nodeLength(endNode)).select();
this.editable.normalize();
this.context.invoke('editor.saveRange');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Remove the non-whitelisted or blacklisted
* top level elements from clipboard data.
*
* @see clipboardWhitelist
* @see clipboardBlacklist
*
* @private
* @param {JQuery} $clipboardData
* @returns {Object} {$clipboardData: JQuery, didRemoveNodes: Boolean}
*/
_cleanClipboardRoot: function ($clipboardData) {
var didRemoveNodes = false;
var whiteList = this._clipboardWhitelist();
var blackList = this._clipboardBlacklist();
var $fakeParent = $(this.document.createElement('div'));
_.each($clipboardData, function (node) {
var isWhitelisted = dom.isText(node) || $(node).filter(whiteList.join(',')).length;
var isBlacklisted = $(node).filter(blackList.join(',')).length;
if (!isWhitelisted || isBlacklisted) {
$fakeParent.append(node.childNodes);
didRemoveNodes = true;
} else {
$fakeParent.append(node);
}
});
return {
$clipboardData: $fakeParent.contents(),
didRemoveNodes: didRemoveNodes,
};
},
/**
* Return a list of jQuery selectors for prohibited nodes on paste.
*
* @private
* @returns {String[]}
*/
_clipboardBlacklist: function () {
return ['.Apple-interchange-newline'];
},
/**
* Return a list of jQuery selectors for exclusively authorized nodes on paste.
*
* @private
* @returns {String[]}
*/
_clipboardWhitelist: function () {
var listSels = ['ul', 'ol', 'li'];
var styleSels = ['i', 'b', 'u', 'em', 'strong'];
var tableSels = ['table', 'th', 'tbody', 'tr', 'td'];
var miscSels = ['img', 'br', 'a', '.fa'];
return this.options.styleTags.concat(listSels, styleSels, tableSels, miscSels);
},
/**
* Return a list of attribute names that are exclusively authorized on paste.
*
* @private
* @returns {String[]}
*/
_clipboardWhitelistAttr: function () {
return ['class', 'href', 'src'];
},
/**
* Fill up empty block elements with BR elements so the carret can enter them.
*
* @private
* @param {JQuery} $els
*/
_fillEmptyBlocks: function ($els) {
var self = this;
$els.filter(function (i, n) {
return self.context.invoke('HelperPlugin.isNodeBlockType', n) && !n.childNodes;
}).append(this.document.createElement('br'));
},
/**
* Get all non-whitelisted or blacklisted elements from clipboard data.
*
* @private
* @param {JQuery} $clipboardData
* @returns {JQuery}
*/
_filterIllegalClipboardElements: function ($clipboardData) {
return $clipboardData.find('*').addBack()
.not(this._clipboardWhitelist().join(','))
.addBack(this._clipboardBlacklist().join(','))
.filter(function () {
return !dom.isText(this);
});
},
/**
* Get a legal point to paste at, from the current range's start point.
*
* @private
* @returns {Object} {node: Node, offset: Number}
*/
_getPastePoint: function () {
var point = this.context.invoke('editor.createRange').getStartPoint();
var offsetChild = point.node.childNodes[point.offset];
point = offsetChild ? this.context.invoke('HelperPlugin.makePoint', offsetChild, 0) : point;
return dom.nextPointUntil(point, this._isPastePointLegal.bind(this));
},
/**
* Insert nodes at a point. Insert them inline if the first node is inline
* and pasting inline is legal at that point.
*
* @private
* @param {Node[]} nodes
* @param {Object} point {node: Node, offset: Number}
*/
_insertNodesAt: function (nodes, point) {
var canInsertInline = dom.isText(point.node) || point.node.tagName === 'BR' || dom.isMedia(point.node);
var $fakeParent = $(this.document.createElement('div'));
$fakeParent.append(nodes);
if (dom.isInline(nodes[0]) && canInsertInline) {
point.node = point.node.tagName ? point.node : point.node.splitText(point.offset);
$(point.node).before($fakeParent.contents());
} else {
this.context.invoke('HelperPlugin.insertBlockNode', $fakeParent[0]);
}
$fakeParent.contents().unwrap();
},
/**
* Return true if the parent of the given node is a removeFormat candidate:
* - It is a removeFormat candidate as defined by W3C
* - It is contained within the editable area
* - It is not unbreakable
*
* @see formatTags the list of removeFormat candidates as defined by W3C
*
* @private
* @param {Node} node
*/
_isParentRemoveFormatCandidate: function (node) {
var parent = node.parentNode;
if (!parent) {
return false;
}
var isEditableOrAbove = parent && (parent === this.editable || $.contains(parent, this.editable));
var isUnbreakable = parent && this.options.isUnbreakableNode(parent);
var isRemoveFormatCandidate = parent && parent.tagName && this.formatTags.indexOf(parent.tagName.toLowerCase()) !== -1;
return parent && !isEditableOrAbove && !isUnbreakable && isRemoveFormatCandidate;
},
/**
* Return true if it's legal to paste nodes at the given point:
* if the point is not within a void node and the point is not unbreakable.
*
* @private
* @param {Object} point {node: Node, offset: Number}
* @returns {Boolean}
*/
_isPastePointLegal: function (point) {
var node = point.node;
var isWithinVoid = false;
if (node.parentNode) {
isWithinVoid = dom.isVoid(node.parentNode) || $(node.parentNode).filter('.fa').length;
}
return !isWithinVoid && !this.options.isUnbreakableNode(point.node);
},
/**
* @private
* @param {Object} point {node: Node, offset: Number}
* @returns {Boolean}
*/
_isPointInAnchor: function (point) {
var ancestor = dom.ancestor(point.node, dom.isAnchor);
return !ancestor || ancestor === this.editable;
},
/**
* Check a list of nodes and merges all adjacent ULs together:
* [ul, ul, p, ul, ul] will return [ul, p, ul], with the li's of
* nodes[1] and nodes[4] appended to nodes[0] and nodes[3].
*
* @private
* @param {Node[]} nodes
* @return {Node[]} the remaining, merged nodes
*/
_mergeAdjacentULs: function (nodes) {
var res = [];
var prevNode;
_.each(nodes, function (node) {
prevNode = res[res.length - 1];
if (prevNode && node.tagName === 'UL' && prevNode.tagName === 'UL') {
$(prevNode).append(node.childNodes);
} else {
res.push(node);
}
});
return res;
},
/**
* Remove an icon's format (colors, font size).
*
* @private
* @param {Node} icon
*/
_removeIconFormat: function (icon) {
$(icon).css({
color: '',
backgroundColor: '',
fontSize: '',
});
var reColorClasses = /(^|\s+)(bg|text)-\S*|/g;
icon.className = icon.className.replace(reColorClasses, '').trim();
},
/**
* Remove non-whitelisted attributes from clipboard.
*
* @private
* @param {JQuery} $els
*/
_removeIllegalClipboardAttributes: function ($els) {
var self = this;
$els.each(function () {
var $node = $(this);
_.each(_.pluck(this.attributes, 'name'), function (attribute) {
if (self._clipboardWhitelistAttr().indexOf(attribute) === -1) {
$node.removeAttr(attribute);
}
});
}).removeClass('o_editable o_not_editable');
},
/**
* Remove non-whitelisted and blacklisted elements from clipboard data.
*
* @private
* @param {JQuery} $clipboardData
* @returns {JQuery}
*/
_removeIllegalClipboardElements: function ($clipboardData) {
var root = true;
$clipboardData = $clipboardData.not('meta').not('style').not('script');
var $badNodes = this._filterIllegalClipboardElements($clipboardData);
do {
if (root) {
root = false;
var cleanData = this._cleanClipboardRoot($clipboardData);
$clipboardData = cleanData.$clipboardData;
root = cleanData.didRemoveNodes;
} else {
this._removeNodesPreserveContents($badNodes);
}
$badNodes = this._filterIllegalClipboardElements($clipboardData);
} while ($badNodes.length);
return $clipboardData;
},
/**
* Remove nodes from the DOM while preserving their contents if any.
*
* @private
* @param {JQuery} $nodes
*/
_removeNodesPreserveContents: function ($nodes) {
var $contents = $nodes.contents();
if ($contents.length) {
$contents.unwrap();
} else {
$nodes.remove();
}
},
/**
* Remove a text node's format: remove its style parents (b, i, u, ...).
*
* @private
* @param {Node} textNode
*/
_removeTextFormat: function (textNode) {
var node = textNode;
while (this._isParentRemoveFormatCandidate(node)) {
this.context.invoke('HelperPlugin.splitAtNodeEnds', node);
$(node.parentNode).before(node).remove();
}
},
/**
* Select the whole current node if the range is collapsed
*
* @private
*/
_selectCurrentIfCollapsed: function () {
var range = this.context.invoke('editor.createRange');
if (!range.isCollapsed()) {
return;
}
this.context.invoke('editor.setRange', range.sc, 0, range.sc, dom.nodeLength(range.sc)).select();
this.context.invoke('editor.saveRange');
},
/**
* Prevent inline nodes directly in TDs by wrapping them in P elements.
*
* @private
* @param {JQuery} $tds
*/
_wrapTDContents: function ($tds) {
var self = this;
var $inlinesInTD = $tds.contents().filter(function () {
return !self.context.invoke('HelperPlugin.isNodeBlockType', this);
});
var parentsOfInlinesInTD = [];
_.each($inlinesInTD, function (n) {
parentsOfInlinesInTD.push(self.context.invoke('HelperPlugin.firstBlockAncestor', n));
});
$($.unique(parentsOfInlinesInTD)).wrapInner(this.document.createElement('p'));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Handle paste events to permit cleaning/sorting of the data before pasting.
*
* @private
* @param {SummernoteEvent} se
* @param {jQueryEvent} e
*/
_onPaste: function (se, e) {
var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
if (isSafari && !iPhone) {
return;
}
se.preventDefault();
se.stopImmediatePropagation();
e.preventDefault();
e.stopImmediatePropagation();
this.context.invoke('editor.beforeCommand');
// Clean up
var clipboardData = e.originalEvent.clipboardData.getData('text/html');
if (clipboardData) {
clipboardData = this.prepareClipboardData(clipboardData);
} else {
clipboardData = e.originalEvent.clipboardData.getData('text/plain');
// get that text as an array of text nodes separated by <br> where needed
var allNewlines = /\n/g;
clipboardData = _.str.trim(clipboardData)
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(allNewlines, '<br>');
clipboardData = $('<p>' + clipboardData + '</p>').contents().toArray();
}
// Delete selection
this.context.invoke('HelperPlugin.deleteSelection');
// Insert the nodes
this.pasteNodes(clipboardData);
this.context.invoke('HelperPlugin.normalize');
this.context.invoke('editor.saveRange');
this.context.invoke('editor.afterCommand');
},
});
registry.add('TextPlugin', TextPlugin);
return TextPlugin;
});
@@ -1,57 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.toolbar', function (require) {
'use strict';
var Plugins = require('web_editor.wysiwyg.plugins');
var registry = require('web_editor.wysiwyg.plugin.registry');
var dom = $.summernote.dom;
var ToolbarPlugin = Plugins.toolbar.extend({
events: {
'summernote.mouseup': 'update',
'summernote.keyup': 'update',
'summernote.change': 'update',
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
initialize: function () {
this._super();
this.update();
},
/**
* Update the toolbar (enabled/disabled).
*/
update: function () {
var $btn = this.$toolbar.children().not('.note-history, .note-view').find('button');
var target = this.context.invoke('editor.restoreTarget') || this.context.invoke('editor.createRange').sc;
if (!target || !$.contains(this.editable, target) || !this.options.isEditableNode(target)) {
$btn.addClass('o_disabled');
return;
}
$btn.removeClass('o_disabled');
if (!target || !this.options.displayPopover(target)) {
$btn.addClass('o_disabled');
}
if (dom.ancestor(target, dom.isMedia)) {
this.$toolbar.children('.note-style, .note-font, .note-para, .note-table').addClass('o_disabled');
$btn.find('.fa-file-image-o').parent().removeClass('o_disabled');
if (dom.ancestor(target, dom.isFont)) {
this.$toolbar.children('.note-color').removeClass('o_disabled');
}
}
},
});
registry.add('toolbar', ToolbarPlugin);
});
@@ -1,85 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.transform', function (require) {
'use strict';
var core = require('web.core');
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var registry = require('web_editor.wysiwyg.plugin.registry');
var wysiwygTranslation = require('web_editor.wysiwyg.translation');
var wysiwygOptions = require('web_editor.wysiwyg.options');
var _t = core._t;
var TransformPlugin = AbstractPlugin.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Manages transformations on a media.
*/
transform: function () {
var $image = $(this.context.invoke('editor.restoreTarget'));
if ($image.data('transfo-destroy')) {
$image.removeData('transfo-destroy');
return;
}
$image.transfo(); // see web_editor/static/lib/jQuery.transfo.js
var mouseup = function () {
$('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]'));
};
$(document).on('mouseup', mouseup);
var mousedown = this._wrapCommand(function (event) {
if (!$(event.target).closest('.transfo-container').length) {
$image.transfo('destroy');
$(document).off('mousedown', mousedown).off('mouseup', mouseup);
}
if ($(event.target).closest('.note-popover').length) {
var transformStyles = self.context.invoke('HelperPlugin.getRegex', '', 'g', '[^;]*transform[\\w:]*;?');
$image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(transformStyles, ''));
}
});
$(document).on('mousedown', mousedown);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Adds the transform buttons.
*
* @override
*/
_addButtons: function () {
var self = this;
this._super();
this.context.memo('button.transform', function () {
return self.context.invoke('buttons.button', {
contents: self.ui.icon(self.options.icons.transform),
tooltip: self.lang.image.transform,
click: self.context.createInvokeHandler('TransformPlugin.transform'),
}).render();
});
},
});
_.extend(wysiwygOptions.icons, {
transform: 'fa fa-object-ungroup',
});
_.extend(wysiwygTranslation.image, {
transform: _t('Transform the picture (click twice to reset transformation)'),
});
registry.add('TransformPlugin', TransformPlugin);
return TransformPlugin;
});
@@ -1,364 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.unbreakable', function (require) {
'use strict';
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var registry = require('web_editor.wysiwyg.plugin.registry');
var dom = $.summernote.dom;
//--------------------------------------------------------------------------
// unbreakable node preventing editing
//--------------------------------------------------------------------------
var Unbreakable = AbstractPlugin.extend({
events: {
'wysiwyg.range .note-editable': '_onRange',
'summernote.mouseup': '_onMouseUp',
'summernote.keyup': '_onKeyup',
'summernote.keydown': '_onKeydown',
// 'summernote.focusnode': '_onFocusnode', => add this event to summernote.
},
initialize: function () {
var self = this;
this._super.apply(this, arguments);
setTimeout(function () {
self.secureArea();
self.context.invoke('HistoryPlugin.clear');
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Change the selection if it breaks an unbreakable node:
*
* .. code-block:: xml
*
<unbreakable id="a">
content_1
<unbreakable id="b">content_2</unbreakable>
<allow id="c">
content_3
<unbreakable id="d">content_4</unbreakable>
<unbreakable id="e">
content_5
<allow id="f">content_6</allow>
content_7
</unbreakable>
content_8
</allow>
<unbreakable id="f">content_9</unbreakable>
<allow id="g">
content_10
<unbreakable id="h">content_11</unbreakable>
content_12
</allow>
</unbreakable>
* .. code-block: none
START END RESIZE START RESIZE END
content_1 content_1 content_3 content_3 (find the first allowed node)
content_1 content_2 content_3 content_3
content_1 content_3 content_3 -
content_3 content_3 - - (nothing to do)
content_3 content_8 - - (can remove unbreakable node)
content_3 content_4 - content_3
content_3 content_5 - #d (can select the entire unbreakable node)
content_5 content_8 content_6 content_6
content_5 content_7 #e #e (select the entire unbreakable node)
content_6 content_8 - content_6
content_7 content_8 - content_8
content_9 content_12 content_10 -
*
* @returns {WrappedRange}
*/
secureRange: function () {
var self = this;
var range = this.context.invoke('editor.createRange');
var isCollapsed = range.isCollapsed();
var needReselect = false;
var startPoint = range.getStartPoint();
var endPoint = range.getEndPoint();
// don't change the selection if the carret is just after a media in editable area
var prev;
if (
isCollapsed && startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset] &&
(prev = dom.prevPoint(startPoint)) && dom.isMedia(prev.node) &&
this.options.isEditableNode(prev.node.parentNode)
) {
return range;
}
// move the start selection to an allowed node
var target = startPoint.node.childNodes[startPoint.offset] || startPoint.node;
if (startPoint.offset && startPoint.offset === dom.nodeLength(startPoint.node)) {
startPoint.node = this.context.invoke('HelperPlugin.lastLeaf', startPoint.node);
startPoint.offset = dom.nodeLength(startPoint.node);
}
if (!dom.isMedia(target) || !this.options.isEditableNode(target)) {
var afterEnd = false;
startPoint = dom.nextPointUntil(startPoint, function (point) {
if (point.node === endPoint.node && point.offset === endPoint.offset) {
afterEnd = true;
}
return self.options.isEditableNode(point.node) && dom.isVisiblePoint(point) || !point.node;
});
if (!startPoint || !startPoint.node) { // no allowed node, search the other way
afterEnd = false;
startPoint = dom.prevPointUntil(range.getStartPoint(), function (point) {
return self.options.isEditableNode(point.node) && dom.isVisiblePoint(point) || !point.node;
});
}
if (startPoint && !startPoint.node) {
startPoint = null;
}
if (afterEnd) {
isCollapsed = true;
}
}
if (startPoint && (startPoint.node !== range.sc || startPoint.offset !== range.so)) {
needReselect = true;
range.sc = startPoint.node;
range.so = startPoint.offset;
if (isCollapsed) {
range.ec = range.sc;
range.eo = range.so;
}
}
if (startPoint && !isCollapsed) { // mouse selection or key selection with shiftKey
var point = endPoint;
endPoint = false;
// if the start point was moved after the end point
var toCollapse = !dom.prevPointUntil(point, function (point) {
return point.node === range.sc && point.offset === range.so;
});
if (!toCollapse) {
// find the first allowed ancestor
var commonUnbreakableParent = dom.ancestor(range.sc, function (node) {
return !dom.isMedia(node) && self.options.isUnbreakableNode(node);
});
if (!commonUnbreakableParent) {
commonUnbreakableParent = this.editable;
}
var lastCheckedNode;
if (point.offset === dom.nodeLength(point.node)) {
point = dom.nextPoint(point);
}
// move the end selection to an allowed node in the first allowed ancestor
endPoint = dom.prevPointUntil(point, function (point) {
if (point.node === range.sc && point.offset === range.so) {
return true;
}
if (lastCheckedNode === point.node) {
return false;
}
// select the entirety of the unbreakable node
if (
point.node.tagName && point.offset &&
$.contains(commonUnbreakableParent, point.node) &&
self.options.isUnbreakableNode(point.node)
) {
return true;
}
var unbreakableParent = dom.ancestor(point.node, function (node) {
return !dom.isMedia(node) && self.options.isUnbreakableNode(node);
});
if (!unbreakableParent) {
unbreakableParent = self.editable;
}
if (commonUnbreakableParent !== unbreakableParent) {
lastCheckedNode = point.node;
return false;
}
lastCheckedNode = point.node;
if (!self.options.isEditableNode(point.node)) {
return false;
}
if (
(/\S|\u200B|\u00A0/.test(point.node.textContent) ||
dom.isMedia(point.node)) &&
dom.isVisiblePoint(point)
) {
return true;
}
if (dom.isText(point.node)) {
lastCheckedNode = point.node;
}
return false;
});
}
if (!endPoint) {
endPoint = range.getStartPoint();
}
if (endPoint.node !== range.ec || endPoint.offset !== range.eo) {
needReselect = true;
range.ec = endPoint.node;
range.eo = endPoint.offset;
}
}
if (needReselect) {
range = range.select();
this.context.invoke('editor.saveRange');
}
return range;
},
/**
* Apply contentEditable false on all media.
*
* @param {DOM} [node] default is editable area
*/
secureArea: function (node) {
this.$editable.find('o_not_editable').attr('contentEditable', 'false');
var medias = (function findMedia(node) {
var medias = [];
if (node.tagName !== 'IMG' && dom.isMedia(node)) {
medias.push(node);
} else {
$(node.childNodes).each(function () {
if (this.tagName) {
medias.push.apply(medias, findMedia(this));
}
});
}
return medias;
})(node || this.editable);
$(medias).addClass('o_fake_not_editable').attr('contentEditable', 'false');
$(medias).each(function () {
if (dom.isVideo(this) && !$(this).children('.o_fake_editable').length) {
// allow char insertion
$(this).prepend('<div class="o_fake_editable o_wysiwyg_to_remove" style="position: absolute;" contentEditable="true"/>');
$(this).append('<div class="o_fake_editable o_wysiwyg_to_remove" style="position: absolute;" contentEditable="true"/>');
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Trigger a focusnode event when the focus enters another node.
*
* @param {DOM} node
*/
_focusNode: function (node) {
if (!node.tagName) {
node = node.parentNode;
}
if (this._focusedNode !== node) {
this._focusedNode = node;
this.context.triggerEvent('focusnode', node);
}
},
//--------------------------------------------------------------------------
// Handler
//--------------------------------------------------------------------------
/**
* Method called on wysiwyg.range event on the editable: secures the range, refocuses.
*/
_onRange: function () {
var range = this.secureRange();
this._focusNode(range.sc);
},
/**
* Method called on mouseup event: secures the range, refocuses.
*/
_onMouseUp: function () {
var range = this.secureRange();
this._focusNode(range.ec);
},
/**
* Method called on keydown event: prevents changes to unbreakable nodes.
*
* @param {SummernoteEvent} se
* @param {jQueryEvent} e
*/
_onKeydown: function (se, e) {
if (!e.key || (e.key.length !== 1 && e.keyCode !== 8 && e.keyCode !== 46)) {
return;
}
var range;
// for test tour, to trigger Keydown on target (instead of use Wysiwyg.setRange)
if (
e.target !== this._focusedNode &&
(this.editable === e.target || $.contains(this.editable, e.target))
) {
range = this.context.invoke('editor.createRange');
if (!$.contains(e.target, range.sc) && !$.contains(e.target, range.ec)) {
range = this.context.invoke('editor.setRange', e.target, 0);
range = range.normalize().select();
this.context.invoke('editor.saveRange');
this._focusNode(range.ec);
}
}
// rerange to prevent some edition.
// eg: if the user select with arraw and shifKey and keypress an other char
range = this.secureRange();
var target = range.getStartPoint();
if (e.keyCode === 8) { // backspace
if (!target || this.options.isUnbreakableNode(target.node)) {
e.preventDefault();
}
} else if (e.keyCode === 46) { // delete
target = dom.nextPointUntil(dom.nextPoint(target), dom.isVisiblePoint);
if (!target || this.options.isUnbreakableNode(target.node)) {
e.preventDefault();
}
}
},
/**
* Method called on keyup event: prevents selection of unbreakable nodes.
*
* @param {SummernoteEvent} se
* @param {jQueryEvent} se
*/
_onKeyup: function (se, e) {
if (e.keyCode < 37 || e.keyCode > 40) {
return;
}
var range;
if (e.keyCode === 37) { // left
range = this.secureRange();
this._focusNode(range.sc);
} else if (e.keyCode === 39) { // right
range = this.secureRange();
this._focusNode(range.ec);
} else if (e.keyCode === 38) { // up
range = this.secureRange();
this._focusNode(range.sc);
} else { // down
range = this.secureRange();
this._focusNode(range.ec);
}
},
});
registry.add('UnbreakablePlugin', Unbreakable);
return Unbreakable;
});
@@ -1,43 +0,0 @@
odoo.define('web_editor.wysiwyg.plugin.registry', function (require) {
"use strict";
var ajax = require('web.ajax');
var core = require('web.core');
var Registry = require('web.Registry');
var WysiwygRegistry = Registry.extend({
init: function () {
this._super();
this._jobs = [];
this._xmlDependencies = [];
this._loadingDef = Promise.resolve();
},
start: function (wysiwyg) {
var fn;
var defs = [this._loadingDef];
while ((fn = this._jobs.shift())) {
defs.push(fn(wysiwyg));
}
var xmlPath;
while ((xmlPath = this._xmlDependencies.shift())) {
defs.push(ajax.loadXML(xmlPath, core.qweb));
}
this._loadingDef = Promise.all(defs);
return this._loadingDef;
},
addJob: function (job) {
this._jobs.push(job);
},
addXmlDependency: function (xmlDependency) {
this._xmlDependencies.push(xmlDependency);
},
plugins: function () {
return _.clone(this.map);
},
});
return new WysiwygRegistry();
});
@@ -47,7 +47,7 @@ var WysiwygRoot = Widget.extend({
});
return instance.attachTo($target).then(function () {
self.$editor = instance.$el;
self.$editor = instance.$editor || instance.$el;
});
});
},
@@ -1,161 +0,0 @@
odoo.define('web_editor.wysiwyg.translation', function (require) {
'use strict';
var core = require('web.core');
var _t = core._t;
$.summernote.lang.odoo = {
font: {
bold: _t('Bold'),
italic: _t('Italic'),
underline: _t('Underline'),
clear: _t('Remove Font Style'),
height: _t('Line Height'),
name: _t('Font Family'),
strikethrough: _t('Strikethrough'),
subscript: _t('Subscript'),
superscript: _t('Superscript'),
size: _t('Font Size')
},
image: {
image: _t('Picture'),
insert: _t('Insert Image'),
resizeFull: _t('Resize Full'),
resizeHalf: _t('Resize Half'),
resizeQuarter: _t('Resize Quarter'),
floatLeft: _t('Float Left'),
floatRight: _t('Float Right'),
floatNone: _t('Float None'),
shapeRounded: _t('Shape: Rounded'),
shapeCircle: _t('Shape: Circle'),
shapeThumbnail: _t('Shape: Thumbnail'),
shapeNone: _t('Shape: None'),
dragImageHere: _t('Drag image or text here'),
dropImage: _t('Drop image or Text'),
selectFromFiles: _t('Select from files'),
maximumFileSize: _t('Maximum file size'),
maximumFileSizeError: _t('Maximum file size exceeded.'),
url: _t('Image URL'),
remove: _t('Remove Image'),
original: _t('Original')
},
video: {
video: _t('Video'),
videoLink: _t('Video Link'),
insert: _t('Insert Video'),
url: _t('Video URL'),
providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
},
link: {
link: _t('Link'),
insert: _t('Insert Link'),
unlink: _t('Unlink'),
edit: _t('Edit'),
textToDisplay: _t('Text to display'),
url: _t('To what URL should this link go?'),
openInNewWindow: _t('Open in new window')
},
table: {
table: _t('Table'),
addRowAbove: _t('Add row above'),
addRowBelow: _t('Add row below'),
addColLeft: _t('Add column left'),
addColRight: _t('Add column right'),
delRow: _t('Delete row'),
delCol: _t('Delete column'),
delTable: _t('Delete table')
},
hr: {
insert: _t('Insert Horizontal Rule')
},
style: {
style: _t('Style'),
p: _t('Normal'),
blockquote: _t('Quote'),
pre: _t('Code'),
h1: _t('Header 1'),
h2: _t('Header 2'),
h3: _t('Header 3'),
h4: _t('Header 4'),
h5: _t('Header 5'),
h6: _t('Header 6')
},
lists: {
unordered: _t('Unordered list'),
ordered: _t('Ordered list')
},
options: {
help: _t('Help'),
fullscreen: _t('Full Screen'),
codeview: _t('Code View')
},
paragraph: {
paragraph: _t('Paragraph'),
outdent: _t('Outdent'),
indent: _t('Indent'),
left: _t('Align left'),
center: _t('Align center'),
right: _t('Align right'),
justify: _t('Justify full')
},
color: {
recent: _t('Recent Color'),
more: _t('More Color'),
background: _t('Background Color'),
foreground: _t('Foreground Color'),
transparent: _t('Transparent'),
setTransparent: _t('Set transparent'),
reset: _t('Reset'),
resetToDefault: _t('Reset to default')
},
shortcut: {
shortcuts: _t('Keyboard shortcuts'),
close: _t('Close'),
textFormatting: _t('Text formatting'),
action: _t('Action'),
paragraphFormatting: _t('Paragraph formatting'),
documentStyle: _t('Document Style'),
extraKeys: _t('Extra keys')
},
help: {
insertParagraph: _t('Insert Paragraph'),
undo: _t('Undoes the last command'),
redo: _t('Redoes the last command'),
tab: _t('Tab'),
untab: _t('Outdent (when at the start of a line)'),
bold: _t('Set a bold style'),
italic: _t('Set a italic style'),
underline: _t('Set a underline style'),
strikethrough: _t('Set a strikethrough style'),
removeFormat: _t('Clean a style'),
justifyLeft: _t('Set left align'),
justifyCenter: _t('Set center align'),
justifyRight: _t('Set right align'),
justifyFull: _t('Set full align'),
insertUnorderedList: _t('Toggle unordered list'),
insertOrderedList: _t('Toggle ordered list'),
outdent: _t('Outdent current paragraph'),
indent: _t('Indent current paragraph'),
formatPara: _t('Change current block\'s format as a paragraph(P tag)'),
formatH1: _t('Change current block\'s format as H1'),
formatH2: _t('Change current block\'s format as H2'),
formatH3: _t('Change current block\'s format as H3'),
formatH4: _t('Change current block\'s format as H4'),
formatH5: _t('Change current block\'s format as H5'),
formatH6: _t('Change current block\'s format as H6'),
insertHorizontalRule: _t('Insert horizontal rule'),
'linkDialog.show': _t('Show Link Dialog')
},
history: {
undo: _t('Undo'),
redo: _t('Redo')
},
specialChar: {
specialChar: _t('SPECIAL CHARACTERS'),
select: _t('Select Special characters')
}
};
return $.summernote.lang.odoo;
});
@@ -1076,8 +1076,11 @@ var VideoWidget = MediaWidget.extend({
}
}
var allVideoClasses = /(^|\s)media_iframe_video(\s|$)/g;
this.media.className = this.media.className && this.media.className.replace(allVideoClasses, ' ');
this.media.innerHTML = '';
var isVideo = this.media.className && this.media.className.match(allVideoClasses);
if (isVideo) {
this.media.className = this.media.className.replace(allVideoClasses, ' ');
this.media.innerHTML = '';
}
},
/**
* Creates a video node according to the given URL and options. If not
@@ -38,11 +38,13 @@ var MediaDialog = Dialog.extend({
*/
init: function (parent, options, media) {
var $media = $(media);
media = $media[0];
options = _.extend({}, options);
options.noDocuments = options.onlyImages || options.noDocuments;
options.noIcons = options.onlyImages || options.noIcons;
options.noVideos = options.onlyImages || options.noVideos;
var onlyImages = options.onlyImages || this.multiImages || (media && ($media.parent().data('oeField') === 'image' || $media.parent().data('oeType') === 'image'));
options.noDocuments = onlyImages || options.noDocuments;
options.noIcons = onlyImages || options.noIcons;
options.noVideos = onlyImages || options.noVideos;
this._super(parent, _.extend({}, {
title: _t("Select a Media"),
@@ -92,19 +94,6 @@ var MediaDialog = Dialog.extend({
var promises = [this._super.apply(this, arguments)];
this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
if (this.imageWidget) {
this.imageWidget.clear();
}
if (this.documentWidget) {
this.documentWidget.clear();
}
if (this.iconWidget) {
this.iconWidget.clear();
}
if (this.videoWidget) {
this.videoWidget.clear();
}
if (this.imageWidget) {
promises.push(this.imageWidget.appendTo(this.$("#editor-media-image")));
}
@@ -170,8 +159,32 @@ var MediaDialog = Dialog.extend({
var _super = this._super;
var args = arguments;
return this.activeWidget.save().then(function (data) {
self._clearWidgets();
self.final_data = data;
return _super.apply(self, args);
_super.apply(self, args);
$(data).trigger('content_changed');
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Call clear on all the widgets except the activeWidget.
* We clear because every widgets are modifying the "media" element.
* All widget have the responsibility to clear a previous element that
* was created from them.
*/
_clearWidgets: function () {
[ this.imageWidget,
this.documentWidget,
this.iconWidget,
this.videoWidget
].forEach( (widget) => {
if (widget !== this.activeWidget) {
widget && widget.clear();
}
});
},
@@ -1,55 +1,56 @@
odoo.define('web_editor.wysiwyg', function (require) {
'use strict';
var Widget = require('web.Widget');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var modulesRegistry = require('web_editor.wysiwyg.plugin.registry');
var wysiwygOptions = require('web_editor.wysiwyg.options');
var _t = core._t;
var SummernoteManager = require('web_editor.rte.summernote');
var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
var id = 0;
// core.bus
// media_dialog_demand
var Wysiwyg = Widget.extend({
xmlDependencies: [
'/web_editor/static/src/xml/wysiwyg.xml',
],
custom_events: {
getRecordInfo: '_onGetRecordInfo',
wysiwyg_blur: '_onWysiwygBlur',
},
defaultOptions: {
codeview: config.isDebug(),
'focus': false,
'toolbar': [
['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',
'colors': summernoteCustomColors,
recordInfo: {
context: {},
},
},
/**
* @params {Object} params
* @params {Object} params.recordInfo
* @params {Object} params.recordInfo.context
* @params {String} [params.recordInfo.context]
* @params {integer} [params.recordInfo.res_id]
* @params {String} [params.recordInfo.data_res_model]
* @params {integer} [params.recordInfo.data_res_id]
* @options {Object} options
* @options {Object} options.recordInfo
* @options {Object} options.recordInfo.context
* @options {String} [options.recordInfo.context]
* @options {integer} [options.recordInfo.res_id]
* @options {String} [options.recordInfo.data_res_model]
* @options {integer} [options.recordInfo.data_res_id]
* @see _onGetRecordInfo
* @see _getAttachmentsDomain in /wysiwyg/widgets/media.js
* @params {Object} params.attachments
* @options {Object} options.attachments
* @see _onGetRecordInfo
* @see _getAttachmentsDomain in /wysiwyg/widgets/media.js (for attachmentIDs)
* @params {function} params.generateOptions
* @options {function} options.generateOptions
* called with the summernote configuration object used before sending to summernote
* @see _editorOptions
**/
init: function (parent, params) {
init: function (parent, options) {
this._super.apply(this, arguments);
this.options = _.extend({}, this.defaultOptions, params);
this.attachments = this.options.attachments || [];
this.hints = [];
this.$el = null;
this._dirty = false;
this.id = _.uniqueId('wysiwyg_');
this.id = ++id;
this.options = options;
},
/**
* Load assets and color picker template then call summernote API
@@ -58,91 +59,49 @@ var Wysiwyg = Widget.extend({
* @override
**/
willStart: function () {
var self = this;
new SummernoteManager(this);
this.$target = this.$el;
this.$el = null; // temporary null to avoid hidden error, setElement when start
return this._super()
.then(function () {
return modulesRegistry.start(self).then(function () {
return self._loadInstance();
});
});
return this._super();
},
/**
*
* @override
*/
start: function () {
var value = this._summernote.code();
this._value = value;
if (this._summernote.invoke('HelperPlugin.hasJinja', value)) {
this._summernote.invoke('codeview.forceActivate');
}
return Promise.resolve();
this.$target.wrap('<odoo-wysiwyg-container>');
this.$el = this.$target.parent();
var options = this._editorOptions();
this.$target.summernote(options);
this.$editor = this.$('.note-editable:first');
this.$editor.data('wysiwyg', this);
this.$editor.data('oe-model', options.recordInfo.res_model);
this.$editor.data('oe-id', options.recordInfo.res_id);
$(document).on('mousedown', this._blur);
this._value = this.$target.html() || this.$target.val();
return this._super.apply(this, arguments).then(() => {
this.$editor.trigger('mouseup');
});
},
/**
* @override
*/
destroy: function () {
if (this._summernote) {
// prevents the replacement of the target by the content of summernote
// (in order to be able to cancel)
var removeLayout = $.summernote.ui.removeLayout;
$.summernote.ui.removeLayout = function ($note, layoutInfo) {
layoutInfo.editor.remove();
$note.show();
};
this._summernote.destroy();
$.summernote.ui.removeLayout = removeLayout;
$(document).off('mousedown', this._blur);
if (this.$target && this.$target.is('textarea') && this.$target.next('.note-editor').length) {
this.$target.summernote('destroy');
}
this.$target.removeAttr('data-wysiwyg-id');
this.$target.removeData('wysiwyg');
$(document).off('.' + this.id);
this._super();
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Add a step (undo) in editor.
*/
addHistoryStep: function () {
var editor = this._summernote.modules.editor;
editor.createRange();
editor.history.recordUndo();
},
/**
* Return the editable area.
*
* @returns {jQuery}
*/
getEditable: function () {
if (this._summernote.invoke('HelperPlugin.hasJinja', this._summernote.code())) {
return this._summernote.layoutInfo.codable;
} else if (this._summernote.invoke('codeview.isActivated')) {
this._summernote.invoke('codeview.deactivate');
}
return this._summernote.layoutInfo.editable;
},
/**
* Perform undo or redo in the editor.
*
* @param {integer} step
*/
history: function (step) {
if (step < 0) {
while (step) {
this._summernote.modules.editor.history.rewind();
step++;
}
} else if (step > 0) {
while (step) {
this._summernote.modules.editor.history.redo();
step--;
}
}
return this.$editor;
},
/**
* Return true if the content has changed.
@@ -150,56 +109,24 @@ var Wysiwyg = Widget.extend({
* @returns {Boolean}
*/
isDirty: function () {
if (!this._dirty && this._value !== this._summernote.code()) {
console.warn("not dirty flag ? Please fix it.");
}
return this._value !== this._summernote.code();
},
/**
* Return true if the current node is unbreakable.
* An unbreakable node can be removed or added but can't by split into
* different nodes (for keypress and selection).
* An unbreakable node can contain nodes that can be edited.
*
* @param {Node} node
* @returns {Boolean}
*/
isUnbreakableNode: function (node) {
return ["TD", "TR", "TBODY", "TFOOT", "THEAD", "TABLE"].indexOf(node.tagName) !== -1 || $(node).is(this.getEditable()) ||
!this.isEditableNode(node.parentNode) || !this.isEditableNode(node) || $.summernote.dom.isMedia(node);
},
/**
* Return true if the current node is editable (for keypress and selection).
*
* @param {Node} node
* @returns {Boolean}
*/
isEditableNode: function (node) {
return $(node).is(':o_editable') && !$(node).is('table, thead, tbody, tfoot, tr');
return this._value !== (this.$editor.html() || this.$editor.val());
},
/**
* Set the focus on the element.
*/
focus: function () {
this.$el.mousedown();
console.log('focus');
},
/**
* Get the value of the editable element.
*
* @param {object} [options]
* @param {Boolean} [options.keepPopover]
* @param {jQueryElement} [options.$layout]
* @returns {String}
*/
getValue: function (options) {
if (!options || !options.keepPopover) {
this._summernote.invoke('editor.hidePopover');
}
var $editable = options && options.$layout || this.getEditable().clone();
$editable.find('.o_wysiwyg_to_remove').remove();
var $editable = options && options.$layout || this.$editor.clone();
$editable.find('[contenteditable]').removeAttr('contenteditable');
$editable.find('.o_fake_not_editable').removeClass('o_fake_not_editable');
$editable.find('.o_fake_editable').removeClass('o_fake_editable');
$editable.find('[class=""]').removeAttr('class');
$editable.find('[style=""]').removeAttr('style');
$editable.find('[title=""]').removeAttr('title');
@@ -207,8 +134,7 @@ var Wysiwyg = Widget.extend({
$editable.find('[data-original-title=""]').removeAttr('data-original-title');
$editable.find('a.o_image, span.fa, i.fa').html('');
$editable.find('[aria-describedby]').removeAttr('aria-describedby').removeAttr('data-original-title');
return $editable.html() || $editable.val();
return $editable.html();
},
/**
* Save the content in the target
@@ -235,468 +161,42 @@ var Wysiwyg = Widget.extend({
* @returns {String}
*/
setValue: function (value, options) {
if (this._summernote.invoke('HelperPlugin.hasJinja', value)) {
this._summernote.invoke('codeview.forceActivate');
}
this._dirty = true;
this._summernote.invoke('HistoryPlugin.clear');
this._summernote.invoke('editor.hidePopover');
this._summernote.invoke('editor.clearTarget');
var $editable = this.getEditable().html(value + '');
this._summernote.invoke('UnbreakablePlugin.secureArea');
if (!options || options.notifyChange !== false) {
$editable.change();
if (this.$editor.is('textarea')) {
this.$target.val(value);
} else {
this.$target.html(value);
}
this.$editor.html(value);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @returns {Object} the summernote configuration
*/
_editorOptions: function () {
var self = this;
var allowAttachment = !this.options.noAttachment;
var options = JSON.parse(JSON.stringify(wysiwygOptions));
options.parent = this;
options.lang = "odoo";
options.focus = false;
options.disableDragAndDrop = !allowAttachment;
options.styleTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
options.fontSizes = [_t('Default'), '8', '9', '10', '11', '12', '13', '14', '18', '24', '36', '48', '62'];
options.minHeight = 180;
options.keyMap.pc['CTRL+K'] = 'LinkPlugin.show';
options.keyMap.mac['CMD+K'] = 'LinkPlugin.show';
delete options.keyMap.pc['CTRL+LEFTBRACKET'];
delete options.keyMap.mac['CMD+LEFTBRACKET'];
delete options.keyMap.pc['CTRL+RIGHTBRACKET'];
delete options.keyMap.mac['CMD+RIGHTBRACKET'];
options.toolbar = [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['colorpicker']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', allowAttachment ? ['linkPlugin', 'mediaPlugin'] : ['linkPlugin']],
['history', ['undo', 'redo']],
['view', this.options.codeview ? ['fullscreen', 'codeview', 'help'] : ['fullscreen', 'help']]
];
options.popover = {
image: [
['padding'],
['imagesize', ['imageSizeAuto', 'imageSize100', 'imageSize50', 'imageSize25']],
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
['imageShape'],
['cropImage'],
['media', ['mediaPlugin', 'removePluginMedia']],
['alt']
],
video: [
['padding'],
['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
['media', ['mediaPlugin', 'removePluginMedia']]
],
icon: [
['padding'],
['faSize'],
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
['faSpin'],
['media', ['mediaPlugin', 'removePluginMedia']]
],
document: [
['float', ['alignLeft', 'alignCenter', 'alignRight', 'alignNone']],
['media', ['mediaPlugin', 'removePluginMedia']]
],
link: [
['link', ['linkPlugin', 'unlink']]
],
table: [
['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
['delete', ['deleteRow', 'deleteCol', 'deleteTable']]
],
};
options.callbacks = {
onBlur: this._onBlurEditable.bind(this),
onFocus: this._onFocusEditable.bind(this),
onChange: this._onChange.bind(this),
onImageUpload: this._onImageUpload.bind(this),
onFocusnode: this._onFocusnode.bind(this),
};
options.isUnbreakableNode = function (node) {
node = node && (node.tagName ? node : node.parentNode);
if (!node) {
return true;
}
return self.isUnbreakableNode(node);
};
options.isEditableNode = function (node) {
node = node && (node.tagName ? node : node.parentNode);
if (!node) {
return false;
}
return self.isEditableNode(node);
};
options.displayPopover = this._isDisplayingPopover.bind(this);
options.hasFocus = function () {
return self._isFocused;
};
var options = Object.assign({}, $.summernote.options, this.defaultOptions, this.options);
if (this.options.generateOptions) {
this.options.generateOptions(options);
options = this.options.generateOptions(options);
}
options.airPopover = options.toolbar;
options.onChange = function (html, $editable) {
$editable.trigger('content_changed');
self.trigger_up('wysiwyg_change');
};
options.onUpload = function (attachments) {
self.trigger_up('wysiwyg_attachment', attachments);
};
options.onFocus = function () {
self.trigger_up('wysiwyg_focus');
};
options.onBlur = function () {
self.trigger_up('wysiwyg_blur');
};
return options;
},
/**
* @private
* @returns {Object} modules list to load
*/
_getPlugins: function () {
return _.extend({}, wysiwygOptions.modules, modulesRegistry.plugins());
},
/**
* Return an object describing the linked record.
*
* @private
* @param {Object} options
* @returns {Object} {res_id, res_model, xpath}
*/
_getRecordInfo: function (options) {
var data = this.options.recordInfo || {};
if (typeof data === 'function') {
data = data(options);
}
if (!data.context) {
throw new Error("Context is missing");
}
return data;
},
/**
* Return true if the editor is displaying the popover.
*
* @private
* @param {Node} node
* @returns {Boolean}
*/
_isDisplayingPopover: function (node) {
return true;
},
/**
* Return true if the given node is in the editor.
* Note: a button in the MediaDialog returns true.
*
* @private
* @param {Node} node
* @returns {Boolean}
*/
_isEditorContent: function (node) {
if (this.el === node) {
return true;
}
if ($.contains(this.el, node)) {
return true;
}
var children = this.getChildren();
var allChildren = [];
var child;
while ((child = children.pop())) {
allChildren.push(child);
children = children.concat(child.getChildren());
}
var childrenDom = _.filter(_.unique(_.flatten(_.map(allChildren, function (obj) {
return _.map(obj, function (value) {
return value instanceof $ ? value.get() : value;
});
}))), function (node) {
return node && node.DOCUMENT_NODE && node.tagName && node.tagName !== 'BODY' && node.tagName !== 'HTML';
});
return !!$(node).closest(childrenDom).length;
},
/**
* Create an instance with the API lib.
*
* @private
* @returns {$.Promise}
*/
_loadInstance: function () {
var defaultOptions = this._editorOptions();
var summernoteOptions = _.extend({
id: this.id,
}, defaultOptions, _.omit(this.options, 'isEditableNode', 'isUnbreakableNode'));
_.extend(summernoteOptions.callbacks, defaultOptions.callbacks, this.options.callbacks);
if (this.options.keyMap) {
_.defaults(summernoteOptions.keyMap.pc, defaultOptions.keyMap.pc);
_.each(summernoteOptions.keyMap.pc, function (v, k, o) {
if (!v) {
delete o[k];
}
});
_.defaults(summernoteOptions.keyMap.mac, defaultOptions.keyMap.mac);
_.each(summernoteOptions.keyMap.mac, function (v, k, o) {
if (!v) {
delete o[k];
}
});
}
var plugins = _.extend(this._getPlugins(), this.options.plugins);
summernoteOptions.modules = _.omit(plugins, function (v) {
return !v;
});
if (this.$target.parent().length) {
summernoteOptions.container = this.$target.parent().css('position', 'relative')[0];
} else {
summernoteOptions.container = this.$target[0].ownerDocument.body;
}
this.$target.summernote(summernoteOptions);
this._summernote = this.$target.data('summernote');
this._summernote.layoutInfo.editable.data('wysiwyg', this);
this.$target.attr('data-wysiwyg-id', this.id).data('wysiwyg', this);
$('.note-editor, .note-popover').not('[data-wysiwyg-id]').attr('data-wysiwyg-id', this.id);
this.setElement(this._summernote.layoutInfo.editor);
$(document).on('mousedown.' + this.id, this._onMouseDown.bind(this));
$(document).on('mouseenter.' + this.id, '*', this._onMouseEnter.bind(this));
$(document).on('mouseleave.' + this.id, '*', this._onMouseLeave.bind(this));
$(document).on('mousemove.' + this.id, this._onMouseMove.bind(this));
this.$el.removeClass('card');
return Promise.resolve();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* trigger_up 'wysiwyg_focus'.
*
* @private
* @param {Object} [options]
*/
_onFocus: function (options) {
this.trigger_up('wysiwyg_focus', options);
},
/**
* trigger_up 'wysiwyg_blur'.
*
* @private
* @param {Object} [options]
*/
_onBlur: function (options) {
this.trigger_up('wysiwyg_blur', options);
},
//--------------------------------------------------------------------------
// Handler
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryEvent} ev
*/
_onMouseEnter: function (ev) {
if (this._isFocused && !this._mouseInEditor && this._isEditorContent(ev.target)) {
this._mouseInEditor = true;
}
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onMouseLeave: function (ev) {
if (this._isFocused && this._mouseInEditor) {
this._mouseInEditor = null;
}
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onMouseMove: function (ev) {
if (this._mouseInEditor === null) {
this._mouseInEditor = !!this._isEditorContent(ev.target);
}
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onMouseDown: function (ev) {
var self = this;
if (this._isEditorContent(ev.target)) {
setTimeout(function () {
if (!self._editableHasFocus && !self._isEditorContent(document.activeElement)) {
self._summernote.layoutInfo.editable.focus();
}
if (!self._isFocused) {
self._isFocused = true;
self._onFocus();
}
});
} else if (this._isFocused) {
this._isFocused = false;
this._onBlur();
}
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onBlurEditable: function (ev) {
var self = this;
this._editableHasFocus = false;
if (!this._isFocused) {
return;
}
if (!this._justFocused && !this._mouseInEditor) {
if (this._isFocused) {
this._isFocused = false;
this._onBlur();
}
} else if (!this._forceEditableFocus) {
this._forceEditableFocus = true;
setTimeout(function () {
if (!self._isEditorContent(document.activeElement)) {
self._summernote.layoutInfo.editable.focus();
}
self._forceEditableFocus = false; // prevent stack size exceeded.
});
} else {
this._mouseInEditor = null;
}
},
/**
* @private
* @param {OdooEvent} ev
*/
_onWysiwygBlur: function (ev) {
if (ev.target === this) {
return;
}
ev.stopPropagation();
this._isFocused = false;
this._forceEditableFocus = false;
this._mouseInEditor = false;
this._summernote.disable();
this.$target.focus();
setTimeout(this._summernote.enable.bind(this._summernote));
this._onBlur(ev.data);
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onFocusEditable: function (ev) {
var self = this;
this._editableHasFocus = true;
this._justFocused = true;
setTimeout(function () {
self._justFocused = false;
});
},
/**
* trigger_up 'wysiwyg_change'
*
* @private
*/
_onChange: function () {
var html = this._summernote.code();
if (this.hints.length) {
var hints = [];
_.each(this.hints, function (hint) {
if (html.indexOf('@' + hint.name) !== -1) {
hints.push(hint);
}
});
this.hints = hints;
}
this._dirty = true;
this.trigger_up('wysiwyg_change', {
html: html,
hints: this.hints,
attachments: this.attachments,
});
},
/**
* trigger_up 'wysiwyg_attachment' when add an image found in the view.
*
* This method is called when an image is uploaded by the media dialog and returns the
* object attachment as recorded in the "ir.attachment" model, via a wysiwyg_attachment event.
*
* For e.g. when sending email, this allows people to add attachments with the content
* editor interface and that they appear in the attachment list.
* The new documents being attached to the email, they will not be erased by the CRON
* when closing the wizard.
*
* @private
*/
_onImageUpload: function (attachments) {
var self = this;
attachments = _.filter(attachments, function (attachment) {
return !_.findWhere(self.attachments, {
id: attachment.id,
});
});
if (!attachments.length) {
return;
}
this.attachments = this.attachments.concat(attachments);
// todo remove image not in the view
this.trigger_up.bind(this, 'wysiwyg_attachment', this.attachments);
},
/**
* Called when the carret focuses on another node (focus event, mouse event, or key arrow event)
* from Unbreakable
*
* @private
* @param {Node} node
*/
_onFocusnode: function (node) {},
/**
* Do not override.
*
* @see _getRecordInfo
* @private
* @param {OdooEvent} ev
* @param {Object} ev.data
* @param {Object} ev.data.recordInfo
* @param {Function(recordInfo)} ev.data.callback
*/
_onGetRecordInfo: function (ev) {
var data = this._getRecordInfo(ev.data);
data.attachmentIDs = _.pluck(this.attachments, 'id');
data.user_id = session.uid || session.user_id;
ev.data.callback(data);
},
});
//--------------------------------------------------------------------------
// Public helper
//--------------------------------------------------------------------------
/**
* @param {Node} node (editable or node inside)
* @returns {Object}
@@ -706,7 +206,7 @@ var Wysiwyg = Widget.extend({
* @returns {Number} eo - end offset
*/
Wysiwyg.getRange = function (node) {
var range = $.summernote.range.create();
var range = $.summernote.core.range.create();
return range && {
sc: range.sc,
so: range.so,
@@ -723,9 +223,9 @@ Wysiwyg.getRange = function (node) {
Wysiwyg.setRange = function (startNode, startOffset, endNode, endOffset) {
$(startNode).focus();
if (endNode) {
$.summernote.range.create(startNode, startOffset, endNode, endOffset).select();
$.summernote.core.range.create(startNode, startOffset, endNode, endOffset).select();
} else {
$.summernote.range.create(startNode, startOffset).select();
$.summernote.core.range.create(startNode, startOffset).select();
}
// trigger for Unbreakable
$(startNode.tagName ? startNode : startNode.parentNode).trigger('wysiwyg.range');
@@ -745,7 +245,6 @@ Wysiwyg.setRangeFromNode = function (node, options) {
while (first.firstChild) {
first = first.firstChild;
}
if (options && options.begin && !options.end) {
Wysiwyg.setRange(first, 0);
} else if (options && !options.begin && options.end) {
@@ -754,38 +253,13 @@ Wysiwyg.setRangeFromNode = function (node, options) {
Wysiwyg.setRange(first, 0, last, last.tagName ? last.childNodes.length : last.textContent.length);
}
};
//--------------------------------------------------------------------------
// jQuery extensions
//--------------------------------------------------------------------------
$.extend($.expr[':'], {
o_editable: function (node, i, m) {
while (node) {
if (node.attributes) {
var className = _.isString(node.className) && node.className || '';
if (
className.indexOf('o_not_editable') !== -1 ||
(node.attributes.contenteditable &&
node.attributes.contenteditable.value !== 'true' &&
className.indexOf('o_fake_not_editable') === -1)
) {
return false;
}
if (
className.indexOf('o_editable') !== -1 ||
(node.attributes.contenteditable &&
node.attributes.contenteditable.value === 'true' &&
className.indexOf('o_fake_editable') === -1)
) {
return true;
}
}
node = node.parentNode;
}
return false;
},
});
return Wysiwyg;
});
odoo.define('web_editor.widget', function (require) {
'use strict';
return {
Dialog: require('wysiwyg.widgets.Dialog'),
MediaDialog: require('wysiwyg.widgets.MediaDialog'),
LinkDialog: require('wysiwyg.widgets.LinkDialog'),
};
});
@@ -6,15 +6,9 @@ var ajax = require('web.ajax');
var core = require('web.core');
var qweb = core.qweb;
var promiseCommon;
var promiseWysiwyg;
var _fnSummernoteMaster = $.fn.summernote;
var _summernoteMaster = $.summernote;
$.fn.summernote = function () {
var summernote = this[0].ownerDocument.defaultView._fnSummenoteSlave || _fnSummernoteMaster;
return summernote.apply(this, arguments);
};
window._fnSummernoteMaster = $.fn.summernote;
window._summernoteMaster = _summernoteMaster;
/**
* Add option (inIframe) to load Wysiwyg in an iframe.
@@ -44,99 +38,34 @@ Wysiwyg.include({
if (!this.options.inIframe) {
return this._super();
}
var debug = odoo.debug;
odoo.debug = false;
var defAsset;
if (this.options.iframeCssAssets) {
this.defAsset = ajax.loadAsset(this.options.iframeCssAssets);
defAsset = ajax.loadAsset(this.options.iframeCssAssets);
} else {
this.defAsset = Promise.resolve({cssLibs: [], cssContents: []});
defAsset = Promise.resolve({
cssLibs: [],
cssContents: []
});
}
promiseWysiwyg = promiseWysiwyg || ajax.loadAsset('web_editor.wysiwyg_iframe_editor_assets');
odoo.debug = debug;
this.defAsset = Promise.all([promiseWysiwyg, defAsset]);
this.$target = this.$el;
return this.defAsset
.then(this._loadIframe.bind(this))
.then(this._super.bind(this)).then(function () {
var _summernoteMaster = $.summernote;
var _summernoteSlave = this.$iframe[0].contentWindow._summernoteSlave;
_summernoteSlave.options = _.extend({}, _summernoteMaster.options, {modules: _summernoteSlave.options.modules});
this._enableBootstrapInIframe();
}.bind(this));
},
/**
* @override
*/
destroy: function () {
if (!this.options.inIframe) {
return this._super();
}
$(document.body).off('.' + this.id);
this.$iframe.parents().removeClass('o_wysiwyg_no_transform');
this.$target.insertBefore(this.$iframe);
delete window.top[this._onUpdateIframeId];
if (this.$iframeTarget) {
this.$iframeTarget.remove();
}
if (this.$iframe) {
this.$iframe.remove();
}
this._super();
.then(this._super.bind(this));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Change fullsreen feature.
*
* @override
* @returns {Object} modules list to load
*/
_getPlugins: function () {
var self = this;
var plugins = this._super();
plugins.fullscreen = plugins.fullscreen.extend({
toggle: function () {
if (!self.$iframe) {
return this._super();
}
self.$iframe.toggleClass('o_fullscreen');
self.$iframe.contents().find('body').toggleClass('o_fullscreen');
// Hack to avoid a parent of the fullscreen iframe to have a
// transform (otherwise the position: fixed won't work)
self.$iframe.parents().toggleClass('o_wysiwyg_no_transform');
},
isFullscreen: function () {
if (!self.$iframe) {
return this._super();
}
return self.$iframe.hasClass('o_fullscreen');
},
});
return plugins;
},
/**
* This method is called after the iframe is loaded with the editor. This is
* to activate the bootstrap features that out of the iframe would launch
* automatically when changing the dom.
*
* @private
*/
_enableBootstrapInIframe: function () {
var body = this.$iframe[0].contentWindow.document.body;
var $toolbarButtons = this._summernote.layoutInfo.toolbar.find('[data-toggle="dropdown"]').dropdown({
boundary: body,
});
function hideDrowpdown() {
var $expended = $toolbarButtons.filter('[aria-expanded="true"]').parent();
$expended.children().removeAttr('aria-expanded').removeClass('show');
$expended.removeClass('show');
}
$(body).on('mouseup.' + this.id, hideDrowpdown);
$(document.body).on('click.' + this.id, hideDrowpdown);
},
/**
* Create iframe, inject css and create a link with the content,
* then inject the target inside.
@@ -152,40 +81,51 @@ Wysiwyg.include({
});
var avoidDoubleLoad = 0; // this bug only appears on some configurations.
// resolve deferred on load
// resolve promise on load
var def = new Promise(function (resolve) {
window.top[self._onUpdateIframeId] = function (_avoidDoubleLoad) {
window.top[self._onUpdateIframeId] = function (Editor, _avoidDoubleLoad) {
if (_avoidDoubleLoad !== avoidDoubleLoad) {
console.warn('Wysiwyg iframe double load detected');
return;
}
delete window.top[self._onUpdateIframeId];
var $iframeTarget = self.$iframe.contents().find('#iframe_target');
$iframeTarget.append(self.$target);
$iframeTarget.find('.o_editable').html(self.$target.val());
self.options.toolbarHandler = $('#web_editor-top-edit', self.$iframe[0].contentWindow.document);
$(qweb.render('web_editor.FieldTextHtml.fullscreen'))
.appendTo(self.options.toolbarHandler)
.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)
});
self.Editor = Editor;
resolve();
};
});
this.$iframe.data('loadDef', def); // for unit test
this.$iframe.data('loadDef', def); // for unit test
// inject content in iframe
this.$iframe.on('load', function onLoad (ev) {
var _avoidDoubleLoad = ++avoidDoubleLoad;
this.defAsset.then(function (asset) {
self.defAsset.then(function (assets) {
if (_avoidDoubleLoad !== avoidDoubleLoad) {
console.warn('Wysiwyg immediate iframe double load detected');
return;
}
var iframeContent = qweb.render('wysiwyg.iframeContent', {
asset: asset,
updateIframeId: this._onUpdateIframeId,
assets: assets,
updateIframeId: self._onUpdateIframeId,
avoidDoubleLoad: _avoidDoubleLoad
});
this.$iframe[0].contentWindow.document
self.$iframe[0].contentWindow.document
.open("text/html", "replace")
.write(iframeContent);
}.bind(this));
}.bind(this));
});
});
this.$iframe.insertAfter(this.$target);
@@ -193,42 +133,4 @@ Wysiwyg.include({
},
});
//--------------------------------------------------------------------------
// Public helper
//--------------------------------------------------------------------------
/**
* Get the current range from Summernote.
*
* @param {Node} [DOM]
* @returns {Object}
*/
Wysiwyg.getRange = function (DOM) {
var summernote = (DOM.defaultView || DOM.ownerDocument.defaultView)._summernoteSlave || _summernoteMaster;
var range = summernote.range.create();
return range && {
sc: range.sc,
so: range.so,
ec: range.ec,
eo: range.eo,
};
};
/**
* @param {Node} sc - start container
* @param {Number} so - start offset
* @param {Node} ec - end container
* @param {Number} eo - end offset
*/
Wysiwyg.setRange = function (sc, so, ec, eo) {
var summernote = sc.ownerDocument.defaultView._summernoteSlave || _summernoteMaster;
$(sc).focus();
if (ec) {
summernote.range.create(sc, so, ec, eo).select();
} else {
summernote.range.create(sc, so).select();
}
// trigger for Unbreakable
$(sc.tagName ? sc : sc.parentNode).trigger('wysiwyg.range');
};
});
});
@@ -0,0 +1,33 @@
odoo.define('web_editor.wysiwyg.snippets', function (require) {
'use strict';
var editor = require('web_editor.editor');
var Wysiwyg = require('web_editor.wysiwyg');
Wysiwyg.include({
init: function (parent, options) {
this._super.apply(this, arguments);
this.Editor = editor.Class;
if (!this.options.toolbarHandler) {
this.options.toolbarHandler = $('#web_editor-top-edit');
}
},
start: async function () {
if (this.options.snippets) {
var self = this;
this.editor = new (this.Editor)(this, this.options);
this.$editor = this.editor.rte.editable();
await this.editor.prependTo(this.$editor[0].ownerDocument.body);
this.options.toolbarHandler.append(this.editor.$el);
this.$el.on('content_changed', function (e) {
var $editable = $(e.target).closest('[data-oe-model], .o_editable');
$editable.addClass('o_dirty');
self.trigger_up('wysiwyg_change');
});
} else {
return this._super();
}
},
});
});
@@ -1,291 +0,0 @@
odoo.define('web_editor.wysiwyg.snippets', function (require) {
'use strict';
var Wysiwyg = require('web_editor.wysiwyg');
var snippetsEditor = require('web_editor.snippet.editor');
Wysiwyg.include({
events: _.extend({}, Wysiwyg.prototype.events, {
'content_changed .o_editable': '_onContentChange',
'content_changed .note-editable': '_onContentChange',
}),
custom_events: _.extend({}, Wysiwyg.prototype.custom_events, {
request_history_undo_record: '_onHistoryUndoRecordRequest',
cover_will_change: '_onCoverWillChange',
snippet_cloned: '_onSnippetCloned',
snippet_dropped: '_onSnippetDropped',
snippet_focused: '_onSnippetFocused',
reload_snippet_dropzones: '_onReloadSnippetDropzones',
}),
selectorEditableArea: '.note-editable',
init: function (parent, options) {
this._super.apply(this, arguments);
options = _.clone(this.options);
if (!options.snippets) {
return;
}
if (options.snippets === true) {
options.snippets = 'web_editor.snippets';
}
options.isUnbreakableNode = this.isUnbreakableNode.bind(this);
options.isEditableNode = this.isEditableNode.bind(this);
this.snippets = new snippetsEditor.Class(this, options);
},
/**
* Preload snippets.
*
* @override
**/
willStart: function () {
if (this.snippets) {
this.snippets.loadSnippets(); // don't use the deferred
}
return this._super();
},
/**
* Add options (snippets) to load snippet building block
* snippets can by url begin with '/' or an view xml_id.
*
* @override
*/
start: function () {
var self = this;
return this._super().then(function () {
if (!self.snippets) {
return;
}
var $editable = $(self._summernote.layoutInfo.editable);
self.snippets.setSelectorEditableArea(self.$el, self.selectorEditableArea);
self.snippets.insertBefore(self.$el).then(function () {
self.$el.before(self.snippets.$el);
var $wrap = $('<div class="o_wrap_editable_snippets"/>');
$wrap.on('scroll', function (event) {
self._summernote.triggerEvent('scroll', event);
});
var $contents = self.snippets.$el.siblings('#oe_manipulators')
.addClass('o_wysiwyg_to_remove').attr('contentEditable', false);
$wrap.append($contents);
$editable.before($wrap);
$wrap.append($editable);
setTimeout(function () { // add a set timeout for the transition
self.snippets.$el.addClass('o_loaded');
self.$el.addClass('o_snippets_loaded');
self.trigger_up('snippets_loaded', self.snippets.$el);
});
});
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
isUnbreakableNode: function (node) {
if (!this.snippets) {
return this._super(node);
}
return this._super(node) || node.tagName === 'DIV' || snippetsEditor.globalSelector.is($(node));
},
/**
* @override
*/
isEditableNode: function (node) {
if (!this._super(node)) {
return false;
}
if (this.snippets && node.tagName === 'DIV' && !node.contentEditable) {
if ($(node).parent('.row').length) {
return true;
}
if (!_.find(node.childNodes, function (node) {
var char = this.context.invoke('HelperPlugin.getRegex', 'char');
return node.tagName !== 'DIV' && (node.tagName || node.textContent.match(char));
})) {
return false;
}
}
return true;
},
/**
* @override
*/
save: function () {
if (!this.snippets) {
return this._super();
}
var defs = [];
this.trigger_up('ready_to_save', {defs: defs});
return Promise.all(defs)
.then(this.snippets.cleanForSave.bind(this.snippets))
.then(this._super.bind(this));
},
//--------------------------------------------------------------------------
// Handler
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryEvent} ev
*/
_onContentChange: function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (this.snippets) {
this.addHistoryStep();
}
this._onChange();
},
/**
* @private
* @override
*/
_onChange: function () {
if (this.snippets) {
this.snippets.updateCurrentSnippetEditorOverlay();
}
this._summernote.invoke('toolbar.update', true);
this._super.apply(this, arguments);
},
/**
* Called when an element askes to record an history undo -> records it.
*
* @private
*/
_onHistoryUndoRecordRequest: function () {
this.addHistoryStep();
},
/**
* Triggered when the user click on the snippet to drag&drop, remove or clone
* (hide the popover)
*
* @private
*/
_onCoverWillChange: function () {
var context = this._summernote;
var target = context.invoke('editor.restoreTarget', target);
context.invoke('MediaPlugin.hidePopovers');
},
/**
* trigger reload_snippet_dropzones on snippets
*
* @private
*/
_onReloadSnippetDropzones: function () {
this.snippets.trigger('reload_snippet_dropzones');
},
/**
* Triggered when a snippet is cloned in the editable area
*
* @private
* @param {OdooEvent} ev
* @param {Object} ev.data
* @param {Object} ev.data.$target
*/
_onSnippetCloned: function (ev) {
this._summernote.invoke('UnbreakablePlugin.secureArea', ev.data.$target[0]);
},
/**
* Triggered when a snippet is dropped in the editable area
*
* @private
* @param {OdooEvent} ev
* @param {Object} ev.data
* @param {Object} ev.data.$target
*/
_onSnippetDropped: function (ev) {
this._summernote.invoke('UnbreakablePlugin.secureArea', ev.data.$target[0]);
},
/**
* Triggered when the user focuses on a snippet: hides the popover.
*
* @private
*/
_onSnippetFocused: function () {
var context = this._summernote;
var target = context.invoke('editor.restoreTarget', target);
context.invoke('MediaPlugin.hidePopovers');
context.invoke('MediaPlugin.update', target);
},
});
//--------------------------------------------------------------------------
// jQuery extensions
//--------------------------------------------------------------------------
$.fn.extend({
/**
* Set the range and focus at the start of the first found node.
*
* @returns {JQuery}
*/
focusIn: function () {
if (this.length) {
Wysiwyg.setRangeFromNode(this[0], {begin: true});
$(this).trigger('mouseup');
}
return this;
},
/**
* Set the range and focus at the end of the first found node.
*
* @returns {JQuery}
*/
focusInEnd: function () {
if (this.length) {
Wysiwyg.setRangeFromNode(this[0], {end: true});
$(this).trigger('mouseup');
}
return this;
},
/**
* Set the range and focus on a selection, accounting for zero-width spaces.
*
* @returns {JQuery}
*/
selectContent: function () {
if (this.length) {
Wysiwyg.setRangeFromNode(this[0]);
var range = $.summernote.range.create();
try {
range.so;
} catch (e) {
/* Under certain unknown conditions, the range cannot be
applied and triggered an error. In order to allow the user to
keep editing, we need to prevent the dialog from showing, hence
the use of `console.error`.
If this error appears, the carret is moved to the beginning
of the focused node.*/
console.error(e);
console.error('Informations for debugging', [
['elements', this],
['outerHTML', this[0].outerHTML],
['textContent', this[0].textContent],
['html', this.html()],
['previous selection', this.end().html()],
]);
}
if (!range.sc.tagName && range.so === 0 && range.sc.textContent[range.so] === '\u200B') {
range.so += 1;
}
if (!range.ec.tagName && range.eo === range.ec.textContent.length && range.ec.textContent[range.eo - 1] === '\u200B') {
range.eo -= 1;
}
range.select();
$(this).trigger('mouseup');
}
return this;
},
});
return Wysiwyg;
});
@@ -56,3 +56,29 @@
}
}
}
.o_field_widgetTextHtml_fullscreen {
.oe_form_field_html.o_form_fullscreen_ancestor iframe {
position: absolute !important;
left: 0 !important;
right: 0 !important;
top: 0 !important;
bottom: 0 !important;
width: 100% !important;
min-height: 100% !important;
z-index: 1001 !important;
border: 0;
}
* {
display: none;
}
.o_form_fullscreen_ancestor {
display: block !important;
position: static !important;
top: 0 !important;
left: 0 !important;
width: auto !important;
overflow: hidden !important;
transform: none !important;
}
}
@@ -21,6 +21,17 @@ html, body {
display: none;
}
.note-popover .popover, .note-editor {
.dropdown-menu .dropdown-item {
> i {
visibility: hidden;
}
&.checked > i {
visibility: visible;
}
}
}
/* ----- GENERIC LAYOUTING HELPERS ---- */
/* table */
#wrapwrap, .o_editable {
+63 -17
View File
@@ -1,5 +1,37 @@
// NOTE EDITOR
body.editor_enable {
padding-top: $o-wysiwyg-toolbar-height !important;
}
#wrapwrap {
height: calc(100vh - #{$o-wysiwyg-toolbar-height});
border: 1px solid $o-wysiwyg-border-color;
border-left: 0;
border-top: 0;
.btn {
-webkit-user-select: none;
}
}
// TOP BAR (EDIT)
#web_editor-top-edit {
height: $o-wysiwyg-toolbar-height;
background-color: $o-wysiwyg-bg-color;
z-index: $o-wysiwyg-zindex - 2;
position: fixed;
top: 0px;
right: 0px;
left: 0px;
white-space: nowrap;
border: 1px solid $o-wysiwyg-border-color;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
transition: background-color 400ms $o-we-md-ease 0s;
font-family: $o-we-font-family;
}
.note-editor.note-frame {
background-color: inherit;
@@ -15,27 +47,39 @@
}
}
.note-popover.popover {
z-index: $o-wysiwyg-zindex;
height: auto;
.note-editor:not(.note-frame) .note-editing-area {
overflow: visible;
}
#web_editor-top-edit .note-popover .popover {
height: $o-wysiwyg-toolbar-height - 2;
top: 0 !important;
left: 0 !important;
padding: 0;
margin: 0;
border: 0;
width: auto;
text-align: center;
box-shadow: none;
background-color: $o-wysiwyg-bg-color;
z-index: $o-wysiwyg-zindex;
.popover-content {
padding-bottom: 0;
max-width: 334px;
.popover-body {
height: $o-wysiwyg-toolbar-height - 2;
>note-btn-group {
white-space: nowrap;
> .btn-group {
height: $o-wysiwyg-toolbar-height - 2;
margin-top: 0;
}
.btn {
height: $o-wysiwyg-toolbar-height - 2;
border-top: 0;
}
}
}
.note-editor .note-toolbar {
white-space: nowrap;
z-index: $o-wysiwyg-zindex + 10;
height: $o-wysiwyg-toolbar-height;
}
#web_editor-toolbars .popover,
.note-popover.popover .popover-content,
.note-editor .note-toolbar {
font-family: $o-wysiwyg-font-family;
@@ -56,12 +100,10 @@
}
.btn {
height: $o-wysiwyg-toolbar-height;
border: none;
border-radius: 0;
background-color: inherit;
color: inherit;
border-top: 2px solid transparent;
transition: all 0.3s ease 0s;
>.fa {
@@ -83,7 +125,6 @@
}
&.active {
border-top: 2px solid white;
background-color: inherit;
color: inherit;
box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.5);
@@ -343,6 +384,11 @@ body .modal {
}
}
.o_we_attachment_selected {
border-color: $o-brand-primary;
box-shadow: 0px 0px 2px 2px $o-brand-primary;
}
.font-icons-icons {
>span {
text-align: center;
@@ -16,7 +16,7 @@ iframe.wysiwyg_iframe.o_fullscreen {
body.o_in_iframe {
background-color: $o-view-background-color;
overflow: hidden;
overflow: auto;
.note-editable {
height: calc(100vh - #{$o-wysiwyg-toolbar-height} - 2px);
@@ -25,4 +25,12 @@ body.o_in_iframe {
.note-statusbar {
display: none;
}
#web_editor-toolbars .popover .btn {
font-size: 1rem;
}
.o_mail_theme_selector .btn {
font-size: 1rem;
}
}
@@ -13,6 +13,14 @@ $o-wysiwyg-handle-border-width: 1px !default;
$o-wysiwyg-handles-offset-to-hide: 10000px !default;
// OVERRIDES FOR EDITOR WITH SNIPPETS
body.editor_enable.editor_has_snippets {
padding-left: $o-we-sidebar-width !important;
#web_editor-top-edit .note-popover .popover {
margin: 0 0 0 $o-we-sidebar-width;
}
}
// NOTE EDITOR
.note-editor.o_snippets_loaded {
@@ -38,8 +46,8 @@ $o-wysiwyg-handles-offset-to-hide: 10000px !default;
display: flex;
flex-flow: column nowrap;
@include o-position-absolute(0, auto, 0, -$o-wysiwyg-sidebar-width);
position: absolute;
position: fixed;
top: 0;
width: $o-wysiwyg-sidebar-width;
z-index: $o-wysiwyg-zindex - 1;
@@ -52,7 +60,7 @@ $o-wysiwyg-handles-offset-to-hide: 10000px !default;
#snippets_menu {
flex: 0 0 auto;
height: $o-wysiwyg-sidebar-height;
height: $o-wysiwyg-toolbar-height;
line-height: $o-wysiwyg-sidebar-height;
margin: 0;
padding: 0;
@@ -341,6 +349,7 @@ $o-wysiwyg-handles-offset-to-hide: 10000px !default;
// MANIPULATORS
#oe_manipulators {
position: relative;
z-index: 1;
// SNIPPET MANIPULATORS
.oe_overlay {
@@ -9,4 +9,12 @@
</div>
</t>
<t t-name="web_editor.FieldTextHtml.fullscreen">
<span style="margin: 5px; position: fixed; top: 0; right: 0; z-index: 2000;">
<button class="o_fullscreen btn btn-primary" style="width: 24px; height: 24px; background-color: #337ab7; border: 1px solid #2e6da4; border-radius: 4px; padding: 0; position: relative;">
<img src="/web_editor/font_to_img/61541/rgb(255,255,255)/16" style="position: absolute; top: 3px; left: 4px;" alt="Fullscreen"/>
</button>
</span>
</t>
</templates>
@@ -1,5 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<!--=================-->
<!-- Base components -->
<!--=================-->
<!-- Editor top bar which contains the summernote tools and save/discard buttons -->
<t t-name="web_editor.editorbar">
<div id="web_editor-toolbars"/>
</t>
<!--=================-->
<!-- Snippet options -->
<!--=================-->
+46 -22
View File
@@ -9,32 +9,56 @@
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<t t-foreach="asset.cssLibs || []" t-as="cssLib">
<link type="text/css" rel="stylesheet" t-att-href="cssLib"/>
<t t-foreach="assets || []" t-as="asset">
<t t-foreach="asset.cssLibs || []" t-as="cssLib">
<link type="text/css" rel="stylesheet" t-att-href="cssLib"/>
</t>
<t t-foreach="asset.cssContents || []" t-as="cssContent">
<style type="text/css" t-raw="cssContent"/>
</t>
<t t-foreach="asset.jsLibs || []" t-as="jsLib">
<script type="text/javascript" t-att-src="jsLib"/>
</t>
<t t-foreach="asset.jsContents || []" t-as="jsContent" t-if="jsContent.indexOf('inline asset') !== -1">
<script type="text/javascript" t-raw="jsContent"/>
</t>
</t>
<t t-foreach="asset.cssContents || []" t-as="cssContent">
<style type="text/css" t-raw="cssContent"/>
</t>
<t t-foreach="asset.jsContents || []" t-as="jsContent" t-if="jsContent.indexOf('inline asset') !== -1">
<script type="text/javascript" t-raw="jsContent"/>
</t>
</head>
<body class="o_in_iframe">
<div id="iframe_target" style="height: calc(100vh - 6px);"/>
<script type="text/javascript">
window.$ = window.jQuery = window.top.jQuery;
odoo.define('web.session', function () {
return window.top.odoo.__DEBUG__.services['web.session'];
});
odoo.define('root.widget', function (require) {
'use strict';
var Widget = require('web.Widget');
var widget = new Widget();
widget.appendTo(document.body);
return widget;
});
odoo.define('web.core.top', function (require) {
var core = require('web.core');
core.qweb.templates = window.top.odoo.__DEBUG__.services['web.core'].qweb.templates;
});
</script>
<script type="text/javascript" src="/web_editor/static/lib/summernote/summernote.js"/>
</head>
<body id="iframe_target" class="o_in_iframe">
<div id="web_editor-top-edit"></div>
<div id="wrapwrap">
<div data-oe-model="model" data-oe-type="html" class="o_editable oe_structure"></div>
</div>
<script type="text/javascript">
window._summernoteSlave = $.summernote;
window._summernoteSlave.iframe = true;
window._summernoteSlave.lang = window.top._summernoteMaster.lang;
window._fnSummenoteSlave = $.fn.summernote;
$.summernote = window.top._summernoteMaster;
$.fn.summernote = window.top._fnSummernoteMaster;
if (window.top.<t t-esc="updateIframeId"/>) {
window.top.<t t-esc="updateIframeId"/>(<t t-esc="avoidDoubleLoad"/>);
}
odoo.define('web_editor.wysiwyg.iniframe', function (require) {
'use strict';
var editor = require('web_editor.editor');
window._summernoteSlave = $.summernote;
window._summernoteSlave.iframe = true;
window._summernoteSlave.lang = window.top.$.summernote.lang;
if (window.top.<t t-esc="updateIframeId"/>) {
window.top.<t t-esc="updateIframeId"/>(editor.Class, <t t-esc="avoidDoubleLoad"/>);
}
});
</script>
</body>
</t>
@@ -137,14 +137,14 @@ QUnit.module('web_editor', {}, function () {
assert.strictEqual(range.sc, pText,
"should select the text");
$field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview button:first'));
assert.ok($field.find('.note-bg-color').hasClass('show') && $field.find('.note-bg-color .dropdown-menu').hasClass('show'),
assert.ok($field.find('.note-back-color-preview').hasClass('show'),
"should display the color picker");
$field.find('.note-toolbar .note-bg-color button[data-value="#00FFFF"]').mousedown().click();
await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview button[data-value="#00FFFF"]'));
assert.ok(!$field.find('.note-bg-color').hasClass('show') && !$field.find('.note-bg-color .dropdown-menu').hasClass('show'),
assert.ok(!$field.find('.note-back-color-preview').hasClass('show'),
"should close the color picker");
assert.strictEqual($field.find('.note-editable').html(),
@@ -167,11 +167,11 @@ QUnit.module('web_editor', {}, function () {
Wysiwyg.setRange(fontContent, 5, pText, 2);
// text is selected
$field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
$field.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview button:first'));
await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview button[data-value="bg-gamma"]'));
assert.strictEqual($field.find('.note-editable').html(),
'<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font class="bg-gamma">oto&nbsp;to</font>to</p><p>tata</p>',
'<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font style="" class="bg-gamma">oto&nbsp;</font><font class="bg-gamma" style="">to</font>to</p><p>tata</p>',
"should have rendered the field correctly in edit");
form.destroy();
@@ -215,7 +215,7 @@ QUnit.module('web_editor', {}, function () {
var pText = $field.find('.note-editable p').first().contents()[0];
Wysiwyg.setRange(pText, 1);
$field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)').mousedown().click();
await testUtils.dom.click($field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)'));
// load static xml file (dialog, media dialog, unsplash image widget)
await defMediaDialog;
@@ -266,7 +266,7 @@ QUnit.module('web_editor', {}, function () {
var pText = $field.find('.note-editable p').first().contents()[0];
Wysiwyg.setRange(pText, 1);
$field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)').mousedown().click();
await testUtils.dom.click($field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)'));
// load static xml file (dialog, media dialog, unsplash image widget)
await defMediaDialog;
@@ -315,82 +315,8 @@ QUnit.module('web_editor', {}, function () {
Wysiwyg.setRange(pText, 1, pText, 10);
// text is selected
await testUtils.dom.click($field.find('.note-toolbar .note-bg-color button:first').mousedown());
await testUtils.dom.click($field.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown());
await testUtils.form.clickSave(form);
form.destroy();
});
QUnit.module('inline-style');
QUnit.test('convert style to class on edit', async function (assert) {
assert.expect(5);
this.data['note.note'].records[0].body = '<p class="pull-right" style="float: right;">toto ' +
'<img data-class="fa fa-star" data-style="color: red;" src="/web_editor/font_to_img/61445/rgb(255,0,0)/13" style="border-style:none;vertical-align:middle;height: auto; width: auto;">' +
'toto toto</p><p>tata</p>';
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'style-inline\': true}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === "write") {
assert.strictEqual(args.args[1].body,
'<p class="pull-right" style="margin:0px;font-size:13px;font-family:&quot;Lucida Grande&quot;, Helvetica, Verdana, Arial, sans-serif;float:right;">' +
'toto ' +
'<img ' +
'data-class="fa fa-star" ' +
'data-style="color: red;" ' +
'style="border-style:none;vertical-align:middle;color: red; height: auto; width: auto;" ' +
'data-src="/web_editor/font_to_img/61445/rgb(255,0,0)/13"' +
'>' +
'toto toto' +
'</p>' +
'<p style="margin:0px;font-size:13px;font-family:&quot;Lucida Grande&quot;, Helvetica, Verdana, Arial, sans-serif;">' +
'tata' +
'</p>',
"should save the content");
}
if (route.indexOf("/web_editor/font_to_img/61445/rgb(") !== -1) {
assert.strictEqual(route,
'/web_editor/font_to_img/61445/rgb(255,0,0)/13',
"should use the image in function of the font and the color");
return Promise.resolve('#');
}
return this._super.apply(this, arguments);
},
});
var $field = form.$('.oe_form_field[name="body"]');
assert.strictEqual($field.children('.o_readonly').html(),
'<p class="pull-right" style="float: right;">' +
'toto ' +
'<img data-class="fa fa-star" data-style="color: red;" ' +
'style="border-style:none;vertical-align:middle;height: auto; width: auto;" ' +
'data-src="/web_editor/font_to_img/61445/rgb(255,0,0)/13">' +
'toto toto' +
'</p>' +
'<p>tata</p>',
"should have rendered a div with correct content in readonly");
await testUtils.form.clickEdit(form);
$field = form.$('.oe_form_field[name="body"]');
assert.strictEqual($field.find('.note-editable').html(),
'<p class="pull-right">' +
'toto ' +
'<span class="fa fa-star o_fake_not_editable" style="color: red;" contenteditable="false"></span>' +
'toto toto' +
'</p>' +
'<p>tata</p>',
"should have rendered the field correctly in edit (remove inline style that used class)");
await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview button:first'));
await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview button[data-value="bg-gamma"]'));
await testUtils.form.clickSave(form);
@@ -433,96 +359,10 @@ QUnit.module('web_editor', {}, function () {
form.destroy();
});
QUnit.module('cssEdit');
QUnit.test('rendering with iframe for edit mode', async function (assert) {
assert.expect(4);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
});
var $field = form.$('.oe_form_field[name="body"]');
assert.strictEqual($field.children('.o_readonly').html(),
'<p>toto toto toto</p><p>tata</p>',
"should have rendered a div with correct content in readonly");
await testUtils.form.clickEdit(form);
$field = form.$('.oe_form_field[name="body"]');
var $iframe = $field.find('iframe');
await $iframe.data('loadDef');
var doc = $iframe.contents()[0];
var $content = $('#iframe_target', doc);
await testUtils.nextTick();
assert.strictEqual($content.find('.note-editable').html(),
'<p>toto toto toto</p><p>tata</p>',
"should have rendered a div with correct content in edit mode");
assert.strictEqual(doc.defaultView.getComputedStyle(doc.body).backgroundColor,
'rgb(255, 0, 0)',
"should load the asset css");
$content.find('.note-toolbar .note-bg-color button:first').mousedown().click();
await testUtils.nextTick();
assert.ok($content.find('.note-bg-color').hasClass('show') && $content.find('.note-bg-color .dropdown-menu').hasClass('show'),
"should display toolbar dropdown menu in iframe");
form.destroy();
});
QUnit.test('save immediately before iframe is rendered in edit mode', async function (assert) {
assert.expect(1);
var form = await testUtils.createAsyncView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
});
testUtils.form.clickEdit(form);
await testUtils.nextTick();
testUtils.form.clickSave(form);
await testUtils.nextTick();
assert.ok(true, "No traceback encountered. The wysiwyg was cut while not loaded.");
form.destroy();
});
QUnit.test('save immediately before iframe is rendered in edit mode with style-inline', async function (assert) {
QUnit.test('save immediately before iframe is rendered in edit mode', async function (assert) {
assert.expect(1);
var form = await testUtils.createAsyncView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\', \'style-inline\': true}"/>' +
'</form>',
res_id: 1,
});
testUtils.form.clickEdit(form);
await testUtils.nextTick();
testUtils.form.clickSave(form);
await testUtils.nextTick();
assert.ok(true, "No traceback encountered. The wysiwyg was cut while not loaded.");
form.destroy();
});
QUnit.test('use colorpicker and save', async function (assert) {
assert.expect(1);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
@@ -530,268 +370,15 @@ QUnit.test('save immediately before iframe is rendered in edit mode', async func
'<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === "write") {
assert.strictEqual(args.args[1].body,
'<p>t<font class="bg-gamma">oto toto&nbsp;</font>toto</p><p>tata</p>',
"should save the content");
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
var $field = form.$('.oe_form_field[name="body"]');
var $iframe = $field.find('iframe');
await $iframe.data('loadDef');
await testUtils.nextTick();
var doc = $iframe.contents()[0];
var $content = $('#iframe_target', doc);
var $editable = $content.find('.note-editable');
// select the text
var pText = $editable.find('p').first().contents()[0];
Wysiwyg.setRange(pText, 1, pText, 10);
// text is selected
await testUtils.dom.click($content.find('.note-toolbar .note-bg-color button:first').mousedown());
await testUtils.dom.click($content.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown());
await testUtils.form.clickSave(form);
await testUtils.nextTick();
assert.ok(true, "No traceback encountered. The wysiwyg was cut while not loaded.");
form.destroy();
});
QUnit.module('wrapper');
// todo add test wrapper
QUnit.module('snippet (without iframe)');
QUnit.test('rendering with snippet panel', async function (assert) {
var done = assert.async();
assert.expect(1);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\'}"/>' +
'</form>',
res_id: 1,
});
testUtils.mock.intercept(form, "snippets_loaded", function () {
assert.strictEqual(form.$('.oe_form_field #oe_snippets').length, 1,
"should display the snippet panel");
form.destroy();
done();
});
await testUtils.form.clickEdit(form);
});
QUnit.test('drag&drop snippet', async function (assert) {
assert.expect(1);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\'}"/>' +
'</form>',
res_id: 1,
});
var defSnippets = testUtils.makeTestPromise();
testUtils.mock.intercept(form, "snippets_loaded", function () {
defSnippets.resolve();
});
await testUtils.form.clickEdit(form);
var $field = form.$('.oe_form_field[name="body"]');
var $editable = $field.find('.note-editable');
await defSnippets;
var $hr = $field.find('.oe_snippet_thumbnail:first');
testUtils.dom.dragAndDrop($hr, $editable.find('p'));
assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
'<div class=\"s_hr pt32 pb32\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>toto toto toto</p><p>tata</p>',
"should drop the snippet");
form.destroy();
});
// todo add test snippet + customize snippet
QUnit.module('snippet & cssEdit');
QUnit.test('rendering with snippet panel', async function (assert) {
var done = assert.async();
assert.expect(1);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\', \'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
});
testUtils.mock.intercept(form, "snippets_loaded", function () {
var doc = form.$('iframe').contents()[0];
var $content = $('#iframe_target', doc);
assert.strictEqual($content.find('#oe_snippets').length, 1,
"should display the snippet panel");
form.destroy();
done();
});
await testUtils.form.clickEdit(form);
});
QUnit.test('drag&drop snippet', async function (assert) {
assert.expect(1);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\', \'cssEdit\': \'template.assets_all_style\'}"/>' +
'</form>',
res_id: 1,
});
var defSnippets = testUtils.makeTestPromise();
testUtils.mock.intercept(form, "snippets_loaded", function () {
defSnippets.resolve();
});
await testUtils.form.clickEdit(form);
await defSnippets;
var doc = form.$('iframe').contents()[0];
var $content = $('#iframe_target', doc);
var $editable = $content.find('.note-editable');
var $hr = $content.find('.oe_snippet_thumbnail:first');
var from = $hr.offset();
var to = $editable.find('p').offset();
$hr.trigger($.Event("mousedown", {
which: 1,
pageX: from.left + 1,
pageY: from.top + 1
}));
$hr.trigger($.Event("mousemove", {
which: 1,
pageX: to.left,
pageY: to.top
}));
$hr.trigger($.Event("mouseup", {
which: 1,
pageX: to.left,
pageY: to.top
}));
assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
'<div class=\"s_hr pt32 pb32\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>toto toto toto</p><p>tata</p>',
"should drop the snippet");
form.destroy();
});
QUnit.test('drag&drop snippet options', async function (assert) {
var done = assert.async();
assert.expect(6);
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\', \'cssEdit\': \'template.assets_all_style\'}"/>' +
'</form>',
res_id: 1,
});
var defSnippets = testUtils.makeTestPromise();
testUtils.mock.intercept(form, "snippets_loaded", function () {
defSnippets.resolve();
});
var defActivateSnippet;
testUtils.mock.intercept(form, "snippet_focused", function (e) {
defActivateSnippet.resolve(e.target);
});
await testUtils.form.clickEdit(form);
defSnippets.then(function () {
var doc = form.$('iframe').contents()[0];
var $content = $('#iframe_target', doc);
var $editable = $content.find('.note-editable');
function dropSnippet() {
defActivateSnippet = testUtils.makeTestPromise();
var $hr = $content.find('.oe_snippet_thumbnail:first');
var from = $hr.offset();
var to = $editable.find('p').offset();
$hr.trigger($.Event("mousedown", {
which: 1,
pageX: from.left + 1,
pageY: from.top + 1
}));
$hr.trigger($.Event("mousemove", {
which: 1,
pageX: to.left,
pageY: to.top
}));
$hr.trigger($.Event("mouseup", {
which: 1,
pageX: to.left,
pageY: to.top
}));
}
dropSnippet();
defActivateSnippet.then(function (snippet) {
assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
'<div class=\"s_hr pt32 pb32 built focus\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>toto toto toto</p><p>tata</p>',
"should drop the snippet");
assert.strictEqual(snippet.$target.index(), 0,
'should show the snippet editor for the first dropped block');
dropSnippet();
defActivateSnippet.then(function (snippet) {
assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
'<div class=\"s_hr pt32 pb32 built\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> ' +
'</div><div class=\"s_hr pt32 pb32 built focus\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> ' +
'</div><p>toto toto toto</p><p>tata</p>',
"should drop the snippet");
assert.strictEqual(snippet.$target.index(), 1,
'should show the snippet editor for the second dropped block');
assert.strictEqual($('.oe_active .oe_overlay_options .btn:visible', doc).length, 4,
'should show the snippet editor buttons');
defActivateSnippet = testUtils.makeTestPromise();
$editable.find('hr:first').trigger('click');
defActivateSnippet.then(function (snippet) {
assert.strictEqual(snippet.$target.index(), 0,
'should show the snippet editor for the first dropped block');
form.destroy();
done();
});
});
});
});
});
QUnit.module('translation');
QUnit.test('field html translatable', async function (assert) {
@@ -822,49 +409,7 @@ QUnit.test('save immediately before iframe is rendered in edit mode', async func
"should not have a translate button in readonly mode");
await testUtils.form.clickEdit(form);
var $button = form.$('.oe_form_field_html .note-toolbar .o_field_translate');
assert.strictEqual($button.length, 1, "should have a translate button");
$button.click();
form.destroy();
_t.database.multi_lang = multiLang;
});
QUnit.test('field html translatable in iframe', async function (assert) {
assert.expect(2);
var multiLang = _t.database.multi_lang;
_t.database.multi_lang = true;
this.data['note.note'].fields.body.translate = true;
var form = await testUtils.createView({
View: FormView,
model: 'note.note',
data: this.data,
arch: '<form>' +
'<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
assert.deepEqual(args.args, ['note.note', 1, 'body'], "should call 'call_button' route");
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
});
var $field = form.$('.oe_form_field[name="body"]');
await testUtils.form.clickEdit(form);
$field = form.$('.oe_form_field[name="body"]');
var $iframe = $field.find('iframe');
await $iframe.data('loadDef');
await testUtils.nextTick();
var doc = $iframe.contents()[0];
var $content = $('#iframe_target', doc);
var $button = $content.find('.note-toolbar .o_field_translate');
var $button = form.$('.oe_form_field_html .o_field_translate');
assert.strictEqual($button.length, 1, "should have a translate button");
await testUtils.dom.click($button);
@@ -152,40 +152,6 @@ var WysiwygTest = Wysiwyg.extend({
this.$target.remove();
this._parentToDestroyForTest.destroy();
},
/**
* @override
*/
isUnbreakableNode: function (node) {
var Node = (node.tagName ? node : node.parentNode);
return (!this.options.useOnlyTestUnbreakable && this._super(node)) ||
!this.isEditableNode(node) ||
Node.tagName === "UNBREAKABLE" ||
(Node.className + '').indexOf('unbreakable') !== -1;
},
/**
* @override
*/
isEditableNode: function (node) {
if (!$(node).closest(this.$el).length) {
return false;
}
while (node) {
if (node.tagName === "EDITABLE") {
return true;
}
if (node.tagName === "NOTEDITABLE") {
return false;
}
if ((node.className + '').indexOf('editable') !== -1) {
return true;
}
if (this.$el[0] === node) {
return true;
}
node = node.parentNode;
}
return false;
},
});

Some files were not shown because too many files have changed in this diff Show More