diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web_editor/static/src/js/backend | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web_editor/static/src/js/backend')
| -rw-r--r-- | addons/web_editor/static/src/js/backend/convert_inline.js | 485 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/backend/field_html.js | 536 |
2 files changed, 1021 insertions, 0 deletions
diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js new file mode 100644 index 00000000..071caa3f --- /dev/null +++ b/addons/web_editor/static/src/js/backend/convert_inline.js @@ -0,0 +1,485 @@ +odoo.define('web_editor.convertInline', function (require) { +'use strict'; + +var FieldHtml = require('web_editor.field.html'); + +/** + * Returns the css rules which applies on an element, tweaked so that they are + * browser/mail client ok. + * + * @param {DOMElement} a + * @returns {Object} css property name -> css property value + */ +function getMatchedCSSRules(a) { + var i, r, k; + var doc = a.ownerDocument; + var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []); + + if (!rulesCache.length) { + var sheets = doc.styleSheets; + for (i = sheets.length-1 ; i >= 0 ; i--) { + var rules; + // try...catch because browser may not able to enumerate rules for cross-domain sheets + try { + 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) { + for (r = rules.length-1; r >= 0; r--) { + var selectorText = rules[r].selectorText; + if (selectorText && + rules[r].cssText && + selectorText !== '*' && + selectorText.indexOf(':hover') === -1 && + selectorText.indexOf(':before') === -1 && + selectorText.indexOf(':after') === -1 && + selectorText.indexOf(':active') === -1 && + selectorText.indexOf(':link') === -1 && + selectorText.indexOf('::') === -1 && + selectorText.indexOf("'") === -1) { + var st = selectorText.split(/\s*,\s*/); + for (k = 0 ; k < st.length ; k++) { + rulesCache.push({ 'selector': st[k], 'style': rules[r].style }); + } + } + } + } + } + rulesCache.reverse(); + } + + var css = []; + var style; + a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector; + for (r = 0; r < rulesCache.length; r++) { + if (a.matches(rulesCache[r].selector)) { + style = rulesCache[r].style; + if (style.parentRule) { + var style_obj = {}; + var len; + for (k = 0, len = style.length ; k < len ; k++) { + if (style[k].indexOf('animation') !== -1) { + continue; + } + style_obj[style[k]] = style[style[k].replace(/-(.)/g, function (a, b) { return b.toUpperCase(); })]; + if (new RegExp(style[k] + '\s*:[^:;]+!important' ).test(style.cssText)) { + style_obj[style[k]] += ' !important'; + } + } + rulesCache[r].style = style = style_obj; + } + css.push([rulesCache[r].selector, style]); + } + } + + function specificity(selector) { + // http://www.w3.org/TR/css3-selectors/#specificity + var a = 0; + selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ''; }); + var b = 0; + selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ''; }); + var c = 0; + selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if (a.indexOf(':not(')===-1) c++; return ''; }); + return a*100 + b*10 + c; + } + css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); }); + + style = {}; + _.each(css, function (v,k) { + _.each(v[1], function (v,k) { + if (v && _.isString(v) && k.indexOf('-webkit') === -1 && (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1)) { + style[k] = v; + } + }); + }); + + _.each(style, function (v,k) { + if (v.indexOf('important') !== -1) { + style[k] = v.slice(0, v.length-11); + } + }); + + if (style.display === 'block') { + delete style.display; + } + + // The css generates all the attributes separately and not in simplified form. + // In order to have a better compatibility (outlook for example) we simplify the css tags. + // e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none + _.each([ + {property: 'margin'}, + {property: 'padding'}, + {property: 'border', propertyEnd: '-style', defaultValue: 'none'}, + ], function (propertyInfo) { + var p = propertyInfo.property; + var e = propertyInfo.propertyEnd || ''; + var defVal = propertyInfo.defaultValue || 0; + + if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) { + if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) { + // keep => property: [top/right/bottom/left value]; + style[p+e] = style[p+'-top'+e]; + } + else { + // keep => property: [top value] [right value] [bottom value] [left value]; + style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal); + if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) { + // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value]; + delete style[p+e]; + return; + } + } + delete style[p+'-top'+e]; + delete style[p+'-right'+e]; + delete style[p+'-bottom'+e]; + delete style[p+'-left'+e]; + } + }); + + if (style['border-bottom-left-radius']) { + style['border-radius'] = style['border-bottom-left-radius']; + delete style['border-bottom-left-radius']; + delete style['border-bottom-right-radius']; + delete style['border-top-left-radius']; + delete style['border-top-right-radius']; + } + + // if the border styling is initial we remove it to simplify the css tags for compatibility. + // Also, since we do not send a css style tag, the initial value of the border is useless. + _.each(_.keys(style), function (k) { + if (k.indexOf('border') !== -1 && style[k] === 'initial') { + delete style[k]; + } + }); + + // text-decoration rule is decomposed in -line, -color and -style. This is + // however not supported by many browser/mail clients and the editor does + // not allow to change -color and -style rule anyway + if (style['text-decoration-line']) { + style['text-decoration'] = style['text-decoration-line']; + delete style['text-decoration-line']; + delete style['text-decoration-color']; + delete style['text-decoration-style']; + delete style['text-decoration-thickness']; + } + + // text-align inheritance does not seem to get past <td> elements on some + // mail clients + if (style['text-align'] === 'inherit') { + var $el = $(a).parent(); + do { + var align = $el.css('text-align'); + if (_.indexOf(['left', 'right', 'center', 'justify'], align) >= 0) { + style['text-align'] = align; + break; + } + $el = $el.parent(); + } while ($el.length && !$el.is('html')); + } + + return style; +} + +/** + * Converts font icons to images. + * + * @param {jQuery} $editable - the element in which the font icons have to be + * converted to images + */ +function fontToImg($editable) { + var fonts = odoo.__DEBUG__.services["wysiwyg.fonts"]; + + $editable.find('.fa').each(function () { + var $font = $(this); + var icon, content; + _.find(fonts.fontIcons, function (font) { + return _.find(fonts.getCssSelectors(font.parser), function (data) { + if ($font.is(data.selector.replace(/::?before/g, ''))) { + icon = data.names[0].split('-').shift(); + content = data.css.match(/content:\s*['"]?(.)['"]?/)[1]; + return true; + } + }); + }); + if (content) { + var color = $font.css('color').replace(/\s/g, ''); + $font.replaceWith($('<img/>', { + src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, Math.round($font.height()))), + 'data-class': $font.attr('class'), + 'data-style': $font.attr('style'), + class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style + style: $font.attr('style'), + }).css({height: 'auto', width: 'auto'})); + } else { + $font.remove(); + } + }); +} + +/** + * Converts images which were the result of a font icon convertion to a font + * icon again. + * + * @param {jQuery} $editable - the element in which the images will be converted + * back to font icons + */ +function imgToFont($editable) { + $editable.find('img[src*="/web_editor/font_to_img/"]').each(function () { + var $img = $(this); + $img.replaceWith($('<span/>', { + class: $img.data('class'), + style: $img.data('style') + })); + }); +} + +/* + * Utility function to apply function over descendants elements + * + * This is needed until the following issue of jQuery is solved: + * https://github.com./jquery/sizzle/issues/403 + * + * @param {Element} node The root Element node + * @param {Function} func The function applied over descendants + */ +function applyOverDescendants(node, func) { + node = node.firstChild; + while (node) { + if (node.nodeType === 1) { + func(node); + applyOverDescendants(node, func); + } + var $node = $(node); + if (node.nodeName === 'A' && $node.hasClass('btn') && !$node.children().length && $(node).parents('.o_outlook_hack').length) { + node = $(node).parents('.o_outlook_hack')[0]; + } + else if (node.nodeName === 'IMG' && $node.parent('p').hasClass('o_outlook_hack')) { + node = $node.parent()[0]; + } + node = node.nextSibling; + } +} + +/** + * Converts css style to inline style (leave the classes on elements but forces + * the style they give as inline style). + * + * @param {jQuery} $editable + */ +function classToStyle($editable) { + applyOverDescendants($editable[0], function (node) { + var $target = $(node); + var css = getMatchedCSSRules(node); + var style = $target.attr('style') || ''; + _.each(css, function (v,k) { + if (!(new RegExp('(^|;)\s*' + k).test(style))) { + style = k+':'+v+';'+style; + } + }); + if (_.isEmpty(style)) { + $target.removeAttr('style'); + } else { + $target.attr('style', style); + } + // Apple Mail + if (node.nodeName === 'TD' && !node.childNodes.length) { + node.innerHTML = ' '; + } + + // Outlook + if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) { + var $hack = $('<table class="o_outlook_hack" style="display: inline-table;vertical-align:middle"><tr><td></td></tr></table>'); + $hack.find('td') + .attr('height', $target.outerHeight()) + .css({ + 'text-align': $target.parent().css('text-align'), + 'margin': $target.css('padding'), + 'border-radius': $target.css('border-radius'), + 'background-color': $target.css('background-color'), + }); + $target.after($hack); + $target.appendTo($hack.find('td')); + // the space add a line when it's a table but it's invisible when it's a link + node = $hack[0].previousSibling; + if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) { + $(node).remove(); + } + node = $hack[0].nextSibling; + if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) { + $(node).remove(); + } + } + else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) { + $target.wrap('<p class="o_outlook_hack" style="text-align:center;margin:0"/>'); + } + }); +} + +/** + * Removes the inline style which is not necessary (because, for example, a + * class on an element will induce the same style). + * + * @param {jQuery} $editable + */ +function styleToClass($editable) { + // Outlook revert + $editable.find('.o_outlook_hack').each(function () { + $(this).after($('a,img', this)); + }).remove(); + + var $c = $('<span/>').appendTo($editable[0].ownerDocument.body); + + applyOverDescendants($editable[0], function (node) { + var $target = $(node); + var css = getMatchedCSSRules(node); + var style = ''; + _.each(css, function (v,k) { + if (!(new RegExp('(^|;)\s*' + k).test(style))) { + style = k+':'+v+';'+style; + } + }); + css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/); + style = ($target.attr('style') || '').replace(/\s*:\s*/, ':').replace(/\s*;\s*/, ';'); + _.each(css, function (v) { + style = style.replace(v, ''); + }); + style = style.replace(/;+(\s;)*/g, ';').replace(/^;/g, ''); + if (style !== '') { + $target.attr('style', style); + } else { + $target.removeAttr('style'); + } + }); + $c.remove(); +} + +/** + * Converts css display for attachment link to real image. + * Without this post process, the display depends on the css and the picture + * does not appear when we use the html without css (to send by email for e.g.) + * + * @param {jQuery} $editable + */ +function attachmentThumbnailToLinkImg($editable) { + $editable.find('a[href*="/web/content/"][data-mimetype]').filter(':empty, :containsExact( )').each(function () { + var $link = $(this); + var $img = $('<img/>') + .attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, '')) + .css('height', Math.max(1, $link.height()) + 'px') + .css('width', Math.max(1, $link.width()) + 'px'); + $link.prepend($img); + }); +} + +/** + * Revert attachmentThumbnailToLinkImg changes + * + * @see attachmentThumbnailToLinkImg + * @param {jQuery} $editable + */ +function linkImgToAttachmentThumbnail($editable) { + $editable.find('a[href*="/web/content/"][data-mimetype] > img').remove(); +} + + +//-------------------------------------------------------------------------- +//-------------------------------------------------------------------------- + + +FieldHtml.include({ + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + commitChanges: function () { + if (this.nodeOptions['style-inline'] && this.mode === "edit") { + this._toInline(); + } + return this._super(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Converts CSS dependencies to CSS-independent HTML. + * - CSS display for attachment link -> real image + * - Font icons -> images + * - CSS styles -> inline styles + * + * @private + */ + _toInline: function () { + var $editable = this.wysiwyg.getEditable(); + var html = this.wysiwyg.getValue({'style-inline': true}); + $editable.html(html); + + attachmentThumbnailToLinkImg($editable); + fontToImg($editable); + classToStyle($editable); + + // fix outlook image rendering bug + _.each(['width', 'height'], function(attribute) { + $editable.find('img[style*="width"], img[style*="height"]').attr(attribute, function(){ + return $(this)[attribute](); + }).css(attribute, function(){ + return $(this).get(0).style[attribute] || 'auto'; + }); + }); + + this.wysiwyg.setValue($editable.html(), { + notifyChange: false, + }); + }, + /** + * Revert _toInline changes. + * + * @private + */ + _fromInline: function () { + var $editable = this.wysiwyg.getEditable(); + var html = this.wysiwyg.getValue(); + $editable.html(html); + + styleToClass($editable); + imgToFont($editable); + linkImgToAttachmentThumbnail($editable); + + // fix outlook image rendering bug + $editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width'); + + this.wysiwyg.setValue($editable.html(), { + notifyChange: false, + }); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * @override + */ + _onLoadWysiwyg: function () { + if (this.nodeOptions['style-inline'] && this.mode === "edit") { + this._fromInline(); + } + this._super(); + }, +}); + +return { + fontToImg: fontToImg, + imgToFont: imgToFont, + classToStyle: classToStyle, + styleToClass: styleToClass, + attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg, + linkImgToAttachmentThumbnail: linkImgToAttachmentThumbnail, +}; +});
\ No newline at end of file diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js new file mode 100644 index 00000000..37f2a8ba --- /dev/null +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -0,0 +1,536 @@ +odoo.define('web_editor.field.html', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var basic_fields = require('web.basic_fields'); +var config = require('web.config'); +var core = require('web.core'); +var Wysiwyg = require('web_editor.wysiwyg.root'); +var field_registry = require('web.field_registry'); +// must wait for web/ to add the default html widget, otherwise it would override the web_editor one +require('web._field_registry'); + +var _lt = core._lt; +var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin; +var QWeb = core.qweb; +var assetsLoaded; + +var jinjaRegex = /(^|\n)\s*%\s(end|set\s)/; + +/** + * FieldHtml Widget + * Intended to display HTML content. This widget uses the wysiwyg editor + * improved by odoo. + * + * nodeOptions: + * - style-inline => convert class to inline style (no re-edition) => for sending by email + * - no-attachment + * - cssEdit + * - cssReadonly + * - snippets + * - wrapper + */ +var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { + description: _lt("Html"), + className: 'oe_form_field oe_form_field_html', + supportedFieldTypes: ['html'], + + custom_events: { + wysiwyg_focus: '_onWysiwygFocus', + wysiwyg_blur: '_onWysiwygBlur', + wysiwyg_change: '_onChange', + wysiwyg_attachment: '_onAttachmentChange', + }, + + /** + * @override + */ + willStart: function () { + var self = this; + this.isRendered = false; + this._onUpdateIframeId = 'onLoad_' + _.uniqueId('FieldHtml'); + var defAsset; + if (this.nodeOptions.cssReadonly) { + defAsset = ajax.loadAsset(this.nodeOptions.cssReadonly); + } + + if (!assetsLoaded) { // avoid flickering when begin to edit + assetsLoaded = new Promise(function (resolve) { + var wysiwyg = new Wysiwyg(self, {}); + wysiwyg.attachTo($('<textarea>')).then(function () { + wysiwyg.destroy(); + resolve(); + }); + }); + } + + return Promise.all([this._super(), assetsLoaded, defAsset]); + }, + /** + * @override + */ + destroy: function () { + delete window.top[this._onUpdateIframeId]; + if (this.$iframe) { + this.$iframe.remove(); + } + this._super(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + activate: function (options) { + if (this.wysiwyg) { + this.wysiwyg.focus(); + return true; + } + }, + /** + * Wysiwyg doesn't notify for changes done in code mode. We override + * commitChanges to manually switch back to normal mode before committing + * changes, so that the widget is aware of the changes done in code mode. + * + * @override + */ + commitChanges: function () { + var self = this; + if (config.isDebug() && this.mode === 'edit') { + var layoutInfo = $.summernote.core.dom.makeLayoutInfo(this.wysiwyg.$editor); + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false); + } + if (this.mode == "readonly" || !this.isRendered) { + return this._super(); + } + var _super = this._super.bind(this); + return this.wysiwyg.saveModifiedImages(this.$content).then(function () { + return self.wysiwyg.save(self.nodeOptions).then(function (result) { + self._isDirty = result.isDirty; + _super(); + }); + }); + }, + /** + * @override + */ + isSet: function () { + var value = this.value && this.value.split(' ').join('').replace(/\s/g, ''); // Removing spaces & html spaces + return value && value !== "<p></p>" && value !== "<p><br></p>" && value.match(/\S/); + }, + /** + * @override + */ + getFocusableElement: function () { + return this.$target || $(); + }, + /** + * Do not re-render this field if it was the origin of the onchange call. + * + * @override + */ + reset: function (record, event) { + this._reset(record, event); + var value = this.value; + if (this.nodeOptions.wrapper) { + value = this._wrap(value); + } + value = this._textToHtml(value); + if (!event || event.target !== this) { + if (this.mode === 'edit') { + this.wysiwyg.setValue(value); + } else { + this.$content.html(value); + } + } + return Promise.resolve(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _getValue: function () { + var value = this.$target.val(); + if (this.nodeOptions.wrapper) { + return this._unWrap(value); + } + return value; + }, + /** + * Create the wysiwyg instance with the target (this.$target) + * then add the editable content (this.$content). + * + * @private + * @returns {$.Promise} + */ + _createWysiwygIntance: function () { + var self = this; + this.wysiwyg = new Wysiwyg(this, this._getWysiwygOptions()); + this.wysiwyg.__extraAssetsForIframe = this.__extraAssetsForIframe || []; + + // 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.closest('body, odoo-wysiwyg-container'); + self._onLoadWysiwyg(); + self.isRendered = true; + }); + }, + /** + * Get wysiwyg options to create wysiwyg instance. + * + * @private + * @returns {Object} + */ + _getWysiwygOptions: function () { + var self = this; + return Object.assign({}, this.nodeOptions, { + recordInfo: { + context: this.record.getContext(this.recordParams), + res_model: this.model, + res_id: this.res_id, + }, + noAttachment: this.nodeOptions['no-attachment'], + inIframe: !!this.nodeOptions.cssEdit, + iframeCssAssets: this.nodeOptions.cssEdit, + snippets: this.nodeOptions.snippets, + + tabsize: 0, + height: 180, + generateOptions: function (options) { + var toolbar = options.toolbar || options.airPopover || {}; + var para = _.find(toolbar, function (item) { + return item[0] === 'para'; + }); + if (para && para[1] && para[1].indexOf('checklist') === -1) { + para[1].splice(2, 0, 'checklist'); + } + if (config.isDebug()) { + options.codeview = true; + var view = _.find(toolbar, function (item) { + return item[0] === 'view'; + }); + if (view) { + if (!view[1].includes('codeview')) { + view[1].splice(-1, 0, 'codeview'); + } + } else { + toolbar.splice(-1, 0, ['view', ['codeview']]); + } + } + if (self.model === "mail.compose.message" || self.model === "mailing.mailing") { + options.noVideos = true; + } + options.prettifyHtml = false; + return options; + }, + }); + }, + /** + * trigger_up 'field_changed' add record into the "ir.attachment" field found in the view. + * This method is called when an image is uploaded via the media dialog. + * + * For e.g. when sending email, this allows people to add attachments with the content + * editor interface and that they appear in the attachment list. + * The new documents being attached to the email, they will not be erased by the CRON + * when closing the wizard. + * + * @private + * @param {Object} attachments + */ + _onAttachmentChange: function (attachments) { + if (!this.fieldNameAttachment) { + return; + } + this.trigger_up('field_changed', { + dataPointID: this.dataPointID, + changes: _.object([this.fieldNameAttachment], [{ + operation: 'ADD_M2M', + ids: attachments + }]) + }); + }, + /** + * @override + */ + _renderEdit: function () { + var value = this._textToHtml(this.value); + if (this.nodeOptions.wrapper) { + value = this._wrap(value); + } + this.$target = $('<textarea>').val(value).hide(); + this.$target.appendTo(this.$el); + + var fieldNameAttachment = _.chain(this.recordData) + .pairs() + .find(function (value) { + return _.isObject(value[1]) && value[1].model === "ir.attachment"; + }) + .first() + .value(); + if (fieldNameAttachment) { + this.fieldNameAttachment = fieldNameAttachment; + } + + if (this.nodeOptions.cssEdit) { + // must be async because the target must be append in the DOM + this._createWysiwygIntance(); + } else { + return this._createWysiwygIntance(); + } + }, + /** + * @override + */ + _renderReadonly: function () { + var self = this; + var value = this._textToHtml(this.value); + if (this.nodeOptions.wrapper) { + value = this._wrap(value); + } + + this.$el.empty(); + var resolver; + var def = new Promise(function (resolve) { + resolver = resolve; + }); + if (this.nodeOptions.cssReadonly) { + this.$iframe = $('<iframe class="o_readonly"/>'); + this.$iframe.appendTo(this.$el); + + var avoidDoubleLoad = 0; // this bug only appears on some computers with some chrome version. + + // inject content in iframe + + this.$iframe.data('loadDef', def); // for unit test + window.top[this._onUpdateIframeId] = function (_avoidDoubleLoad) { + if (_avoidDoubleLoad !== avoidDoubleLoad) { + console.warn('Wysiwyg iframe double load detected'); + return; + } + self.$content = $('#iframe_target', self.$iframe[0].contentWindow.document.body); + resolver(); + }; + + this.$iframe.on('load', function onLoad() { + var _avoidDoubleLoad = ++avoidDoubleLoad; + ajax.loadAsset(self.nodeOptions.cssReadonly).then(function (asset) { + if (_avoidDoubleLoad !== avoidDoubleLoad) { + console.warn('Wysiwyg immediate iframe double load detected'); + return; + } + var cwindow = self.$iframe[0].contentWindow; + cwindow.document + .open("text/html", "replace") + .write( + '<head>' + + '<meta charset="utf-8"/>' + + '<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n' + + '<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>\n' + + _.map(asset.cssLibs, function (cssLib) { + return '<link type="text/css" rel="stylesheet" href="' + cssLib + '"/>'; + }).join('\n') + '\n' + + _.map(asset.cssContents, function (cssContent) { + return '<style type="text/css">' + cssContent + '</style>'; + }).join('\n') + '\n' + + '</head>\n' + + '<body class="o_in_iframe o_readonly">\n' + + '<div id="iframe_target">' + value + '</div>\n' + + '<script type="text/javascript">' + + 'if (window.top.' + self._onUpdateIframeId + ') {' + + 'window.top.' + self._onUpdateIframeId + '(' + _avoidDoubleLoad + ')' + + '}' + + '</script>\n' + + '</body>'); + + var height = cwindow.document.body.scrollHeight; + self.$iframe.css('height', Math.max(30, Math.min(height, 500)) + 'px'); + }); + }); + } else { + this.$content = $('<div class="o_readonly"/>').html(value); + this.$content.appendTo(this.$el); + resolver(); + } + + def.then(function () { + self.$content.on('click', 'ul.o_checklist > li', self._onReadonlyClickChecklist.bind(self)); + }); + }, + /** + * @private + * @param {string} text + * @returns {string} the text converted to html + */ + _textToHtml: function (text) { + var value = text || ""; + if (jinjaRegex.test(value)) { // is jinja + return value; + } + try { + $(text)[0].innerHTML; // crashes if text isn't html + } catch (e) { + if (value.match(/^\s*$/)) { + value = '<p><br/></p>'; + } else { + value = "<p>" + value.split(/<br\/?>/).join("<br/></p><p>") + "</p>"; + value = value + .replace(/<p><\/p>/g, '') + .replace('<p><p>', '<p>') + .replace('<p><p ', '<p ') + .replace('</p></p>', '</p>'); + } + } + return value; + }, + /** + * Move HTML contents out of their wrapper. + * + * @private + * @param {string} html content + * @returns {string} html content + */ + _unWrap: function (html) { + var $wrapper = $(html).find('#wrapper'); + return $wrapper.length ? $wrapper.html() : html; + }, + /** + * Wrap HTML in order to create a custom display. + * + * The wrapper (this.nodeOptions.wrapper) must be a static + * XML template with content id="wrapper". + * + * @private + * @param {string} html content + * @returns {string} html content + */ + _wrap: function (html) { + return $(QWeb.render(this.nodeOptions.wrapper)) + .find('#wrapper').html(html) + .end().prop('outerHTML'); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * Method called when wysiwyg triggers a change. + * + * @private + * @param {OdooEvent} ev + */ + _onChange: function (ev) { + this._doDebouncedAction.apply(this, arguments); + + var $lis = this.$content.find('.note-editable ul.o_checklist > li:not(:has(> ul.o_checklist))'); + if (!$lis.length) { + return; + } + var max = 0; + var ids = []; + $lis.map(function () { + var checklistId = parseInt(($(this).attr('id') || '0').replace(/^checklist-id-/, '')); + if (ids.indexOf(checklistId) === -1) { + if (checklistId > max) { + max = checklistId; + } + ids.push(checklistId); + } else { + $(this).removeAttr('id'); + } + }); + $lis.not('[id]').each(function () { + $(this).attr('id', 'checklist-id-' + (++max)); + }); + }, + /** + * Allows Enter keypress in a textarea (source mode) + * + * @private + * @param {OdooEvent} ev + */ + _onKeydown: function (ev) { + if (ev.which === $.ui.keyCode.ENTER) { + ev.stopPropagation(); + return; + } + this._super.apply(this, arguments); + }, + /** + * Method called when wysiwyg triggers a change. + * + * @private + * @param {OdooEvent} ev + */ + _onReadonlyClickChecklist: function (ev) { + var self = this; + if (ev.offsetX > 0) { + return; + } + ev.stopPropagation(); + ev.preventDefault(); + var checked = $(ev.target).hasClass('o_checked'); + var checklistId = parseInt(($(ev.target).attr('id') || '0').replace(/^checklist-id-/, '')); + + this._rpc({ + route: '/web_editor/checklist', + params: { + res_model: this.model, + res_id: this.res_id, + filename: this.name, + checklistId: checklistId, + checked: !checked, + }, + }).then(function (value) { + self._setValue(value); + }); + }, + /** + * Method called when the wysiwyg instance is loaded. + * + * @private + */ + _onLoadWysiwyg: function () { + var $button = this._renderTranslateButton(); + $button.css({ + 'font-size': '15px', + position: 'absolute', + right: '+5px', + top: '+5px', + }); + this.$el.append($button); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onWysiwygBlur: function (ev) { + ev.stopPropagation(); + this._doAction(); + if (ev.data.key === 'TAB') { + this.trigger_up('navigation_move', { + direction: ev.data.shiftKey ? 'left' : 'right', + }); + } + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onWysiwygFocus: function (ev) {}, +}); + + +field_registry.add('html', FieldHtml); + + +return FieldHtml; +}); |
