From 65fd8b45ae87062030430b06e048d07a33bf82f4 Mon Sep 17 00:00:00 2001 From: Bago Date: Tue, 10 May 2022 09:59:03 +0200 Subject: [PATCH] Attempt to support newer Tinymce releases We found that most browser/tinymce bugs related to contenteditable are about editing non-DIV elements, so we try to change the template converter to always create an utility DIV and make it editable (Fix #429 and #362). This should still work with 4.5.x (currently the last version supported by mosaico) and by updated releases. Mosaico now supports multiple editing styles: the legacy styles are named "singleline" and "multiline" and they are automatically applied depending on the data-ko-editable element (td & div use "multiline", the other ones "singleline") but a data-ko-editor-style="newstyle" can be used to force the use of a different tinymce options set. This set will inherit ko.bindingHandlers.wysiwyg.standardOptions, ko.bindingHandlers.wysiwyg.fullOptions and then ko.bindingHandlers.wysiwyg.extendedOptions. (Fix #244) This changeset also remove the support for ko.bindingHandlers.wysiwyg.getContentOptions and replaces it with a new option "_use_raw_format" that defines wether to use "raw" format when getting/setting the content from tinymce. The new default is to use "raw" format for the singleline style and non raw format for everything else (the raw in singleline is required in order to prevent tinymce from trimming contents). Note that previously mosaico used raw format for every style, so if you want to preserve the previous behaviour you need to set ko.bindingHandlers.wysiwyg.fullOptions._use_raw_format to true. (should fix #446) Another breaking change is the default "extended_valid_elements" option that changed from "strong/b,em/i,*[*]" to "strong/b,em/i" so to enable content filtering and strip unwanted tags (like scripts). You can revert it by setting that option to the old value but beware XSS vulnerabilities. This changeset enable to upgrade of tinymce to newer 4.x (4.7.x, 4.9.x) and latest 5.x (Fix #593). For 5.x and 6.x we also had to update the scrollfix binding so that the toolbar is correclty moved when the edit area is scrolled. TinyMCE 6.x compatibility requires more changes because they removed support for "forced_root_block = false" or "forced_root_block = ''" that is needed for the singleline editing. You can trick tinymce by usign "forced_root_block = 'unknownelementname'" but it will prevent use of "ENTER" to insert
. Issues #596 and #443 may be fixed by this changeset. --- spec/converter-spec.js | 2 +- src/js/bindings/scrollfix.js | 14 +++++++++ src/js/bindings/wysiwygs.js | 61 +++++++++++++++++++++++++++--------- src/js/converter/parser.js | 25 +++++++++++++-- 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/spec/converter-spec.js b/spec/converter-spec.js index 770cb1982..5c8cad47c 100644 --- a/spec/converter-spec.js +++ b/spec/converter-spec.js @@ -61,7 +61,7 @@ describe('Template converter', function() { }, { optionalName: 'simpleBlock', templateMode: 'show', - html: '
' + html: '
' }]; expect(parseData.templates).toEqual(expectedTemplates); diff --git a/src/js/bindings/scrollfix.js b/src/js/bindings/scrollfix.js index 7084e7204..4999cc2a3 100644 --- a/src/js/bindings/scrollfix.js +++ b/src/js/bindings/scrollfix.js @@ -22,6 +22,7 @@ var render = function() { timeout = undefined; + // For Tinymce 4.x if (typeof tinymce.activeEditor !== 'undefined' && tinymce.activeEditor !== null && typeof tinymce.activeEditor.theme !== 'undefined' && tinymce.activeEditor.theme !== null && typeof tinymce.activeEditor.theme.panel !== 'undefined' && tinymce.activeEditor.theme.panel !== null) { @@ -43,6 +44,19 @@ var render = function() { } } + + // For Tinymce 5.x and 6.0.x + if (typeof tinymce.activeEditor !== 'undefined' && tinymce.activeEditor !== null && + typeof tinymce.activeEditor.container !== 'undefined' && tinymce.activeEditor.container !== null && + typeof tinymce.activeEditor.ui !== 'undefined' && tinymce.activeEditor.ui !== null) { + + // this is not null when the toolbar is visible + if (tinymce.activeEditor.container.offsetParent !== null) { + // nodeChanged updates the toolbar position but doesn't move it around the editable (on top or bottom) according to the best placement, while ui.show does. + // tinymce.activeEditor.nodeChanged(); + tinymce.activeEditor.ui.show(); + } + } }; ko.bindingHandlers.wysiwygScrollfix = { diff --git a/src/js/bindings/wysiwygs.js b/src/js/bindings/wysiwygs.js index d3dfd204a..49cda0144 100644 --- a/src/js/bindings/wysiwygs.js +++ b/src/js/bindings/wysiwygs.js @@ -228,9 +228,11 @@ var _catchingFire = function(event, args) { // also, maybe we should use the "raw" only for the "before SetContent" and instead read the "non-raw" content (the raw content sometimes have data- attributes and too many ending
in the code) ko.bindingHandlers.wysiwyg = { debug: false, - // please note that setting getContentOptions to "{}" improves (clean ups) the html output generated by tinymce, but also introduces a bug in Firefox: https://github.com/voidlabs/mosaico/issues/446 - // by keeping raw the output is still broken in Firefox but empty

tags are rendered 0px height. - getContentOptions: { format: 'raw' }, + // We used to have a "getContentOptions" with a default value "{ format: 'raw' }" used to read the content from TinyMCE + // We used it to try to move to a more clean output (passing "{}" instead of raw), but this introduced issues with Firefox + // https://github.com/voidlabs/mosaico/issues/446 + // We now don't use this option anymore: the options are decided internally depending on the mode (inline/single line vs block/multiline). + // getContentOptions: { format: 'raw' }, useTarget: false, currentIndex: 0, standardOptions: {}, @@ -244,14 +246,14 @@ ko.bindingHandlers.wysiwyg = { toolbar1: 'bold italic forecolor backcolor hr styleselect removeformat | link unlink | pastetext code', //toolbar1: "bold italic | forecolor backcolor | link unlink | hr | pastetext code", // | newsletter_profile newsletter_optlink newsletter_unsubscribe newsletter_showlink"; //toolbar2: "formatselect fontselect fontsizeselect | alignleft aligncenter alignright alignjustify | bullist numlist", - plugins: ["link hr paste lists textcolor code"], + plugins: ["link", "hr", "paste", "lists", "textcolor", "code"], // valid_elements: 'strong/b,em/i,*[*]', // extended_valid_elements: 'strong/b,em/i,*[*]', // Removed: image fullscreen contextmenu // download custom: // jquery version con legacyoutput, anchor, code, importcss, link, paste, textcolor, hr, lists }, - init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { // TODO ugly, but works... ko.bindingHandlers.focusable.init(element); @@ -284,7 +286,12 @@ ko.bindingHandlers.wysiwyg = { if (!ko.isObservable(value)) throw "Wysiwyg binding called with non observable"; if (element.nodeType === 8) throw "Wysiwyg binding called on virtual node, ignoring...." + element.innerHTML; - var fullEditor = element.tagName == 'DIV' || element.tagName == 'TD'; + var fullEditor = true; + + var editorStyle = allBindings['has']('wysiwygStyle') ? allBindings.get('wysiwygStyle') : false; + if (editorStyle == 'singleline') fullEditor = false; + else if (editorStyle === false) fullEditor = element.tagName == 'DIV' || element.tagName == 'TD'; + var isSubscriberChange = false; var thisEditor; var isEditorChange = false; @@ -296,18 +303,32 @@ ko.bindingHandlers.wysiwyg = { plugins: ["paste"], toolbar1: "bold italic", toolbar2: "", - // we have to disable preview_styles otherwise tinymce push inline every style he things will be applied and this makes the style menu to inherit color/font-family and more. + // we have to disable preview_styles otherwise tinymce push inline every style he thinks will be applied and + // this makes the style menu to inherit color/font-family and more. preview_styles: false, paste_as_text: true, language: 'en', schema: "html5", - extended_valid_elements: 'strong/b,em/i,*[*]', + + // 2022-05 remove *[*] from the extended_valid_elements to let tinymce do content filtering and, for example, + // protect from XSS. + // extended_valid_elements: 'strong/b,em/i,*[*]', + extended_valid_elements: 'strong/b,em/i', menubar: false, skin: 'gray-flat', + + // 2022-05: we found that 'raw' format is mainly needed for "single line" (inline, not block multiline) editing + // NOTE: this is not a tinymce option! + // set "ko.bindingHandlers.wysiwyg.fullOptions._use_raw_format" to "true" to fallback to mosaico 0.17 behaviour + _use_raw_format: fullEditor ? false : true, + // 2018-03-07: the force_*_newlines are not effective. force_root_block is the property dealing with newlines, now. // force_br_newlines: !fullEditor, // we force BR as newline when NOT in full editor // force_p_newlines: fullEditor, + // 2022-05: tinymce 6 dropped support for forced_root_block false or empty. Using 'x' or another unknown tag is a + // workaround but then further handling is needed if you want the enter to create
newslines (like shift-enter). forced_root_block: fullEditor ? 'p' : '', + init_instance_callback : function(editor) { if (doDebug) console.debug("Editor for selector", selectorId, "is now initialized."); if (ko.bindingHandlers.wysiwyg.initializingClass) { @@ -351,7 +372,7 @@ ko.bindingHandlers.wysiwyg = { // not emptied and full of tags used by tinymce as workaround. // In future we'll probably change the default to "non raw", but at this time we keep this as an option // in order to keep backward compatibility. - value(editor.getContent(ko.bindingHandlers.wysiwyg.getContentOptions)); + value(editor.getContent(editor.getParam('_use_raw_format') ? { format: 'raw' } : {})); } catch (e) { console.warn("Unexpected error setting content value for", selectorId, e); } finally { @@ -383,10 +404,14 @@ ko.bindingHandlers.wysiwyg = { }); } - // NOTE: this fixes issue with "leading spaces" in default content that were lost during initialization. - editor.on('BeforeSetContent', function(args) { - if (args.initial) args.format = 'raw'; - }); + // 2022-05-04: use format raw only for inline contents (the ones with no force_root_block) + // for better compatibility with Tinymce 4.7+,5+ + if (editor.getParam('_use_raw_format')) { + // NOTE: this fixes issue with "leading spaces" in default content that were lost during initialization. + editor.on('BeforeSetContent', function(args) { + if (args.initial) args.format = 'raw'; + }); + } // 20180307: Newer TinyMCE versions (4.7.x for sure, maybe early versions too) stopped accepting ENTER on single paragraph elements // We try to use the "force_br_newlines : true," in non full version (see options) @@ -420,6 +445,13 @@ ko.bindingHandlers.wysiwyg = { ko.utils.extend(options, ko.bindingHandlers.wysiwyg.standardOptions); if (fullEditor) ko.utils.extend(options, ko.bindingHandlers.wysiwyg.fullOptions); + // this way you can have custom editing styles + // default ones are: singleline and multiline + // everyone already inherit "standardOptions" + every non "singleline" style inherit the "fullOptions" + if (ko.bindingHandlers.wysiwyg[editorStyle+'Options']) { + ko.utils.extend(options, ko.bindingHandlers.wysiwyg[editorStyle+'Options']); + } + // we have to put initialization in a settimeout, otherwise switching from "1" to "2" columns blocks // will start the new editors before disposing the old ones and IDs get temporarily duplicated. // using setTimeout the dispose/create order is correct on every browser tested. @@ -442,7 +474,8 @@ ko.bindingHandlers.wysiwyg = { // we failed setting contents in other ways... // $(element).html(content); if (typeof thisEditor !== 'undefined') { - thisEditor.setContent(content, { format: 'raw' }); + // 2022-05-04 changed so to use format raw only for single line editor + thisEditor.setContent(content, options._use_raw_format ? { format: 'raw' } : {}); } else { ko.utils.setHtml(element, content); } diff --git a/src/js/converter/parser.js b/src/js/converter/parser.js index 1fda1b82a..5b8b98a97 100644 --- a/src/js/converter/parser.js +++ b/src/js/converter/parser.js @@ -233,8 +233,29 @@ var processBlock = function(element, defs, themeUpdater, blockPusher, templateUr newBinding += "wysiwygOrHtml: " + modelBindValue; - if (domutils.getLowerTagName(element) == 'td') { - var wrappingDiv = $('

')[0]; + var lowerTagName = domutils.getLowerTagName(element); + + var editorStyle = domutils.getAttribute(element, 'data-ko-editor-style'); + if (editorStyle) { + domutils.removeAttribute(element, 'data-ko-editor-style'); + } else if (lowerTagName == 'div' || lowerTagName == 'td') { + editorStyle = 'multiline'; + } else { + editorStyle = 'singleline'; + } + + newBinding += ", wysiwygStyle: '"+editorStyle+"'"; + + // 2022-05-04: we now always use a wrapping DIV for every element (but a DIV). + // In past we only used the wrapping div for td elements (because it didn't work in IE10-IE11) + // https://github.com/voidlabs/mosaico/issues/11 + // but we found that every element but divs have contenteditable/tinymce issues. + // We stuck to tinymce 4.5.x for long time because of tinymce issue with editing spans. + if (lowerTagName !== 'div') { + var wrappingDivAttrs = editorStyle == 'singleline' ? + ' style="display: inline-block;"' : + ' style="width: 100%; height: 100%"'; + var wrappingDiv = $('
')[0]; domutils.setAttribute(wrappingDiv, 'data-bind', newBinding); var newContent = domutils.getInnerHtml($('
').append(wrappingDiv)); domutils.setContent(element, newContent);