Skip to content

Commit

Permalink
Attempt to support newer Tinymce releases
Browse files Browse the repository at this point in the history
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 <br/>.

Issues #596 and #443 may be fixed by this changeset.
  • Loading branch information
bago committed May 10, 2022
1 parent bd73791 commit 65fd8b4
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 17 deletions.
2 changes: 1 addition & 1 deletion spec/converter-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Template converter', function() {
}, {
optionalName: 'simpleBlock',
templateMode: 'show',
html: '<div data-bind="attr: { id: id }"><div data-bind="wysiwygId: id()+\'_text\', wysiwygClick: function(obj, evt) { $root.selectItem(text, $data); return false }, clickBubble: false, wysiwygCss: { selecteditem: $root.isSelectedItem(text) }, scrollIntoView: $root.isSelectedItem(text), wysiwygOrHtml: text"></div></div>'
html: '<div data-bind="attr: { id: id }"><div data-bind="wysiwygId: id()+\'_text\', wysiwygClick: function(obj, evt) { $root.selectItem(text, $data); return false }, clickBubble: false, wysiwygCss: { selecteditem: $root.isSelectedItem(text) }, scrollIntoView: $root.isSelectedItem(text), wysiwygOrHtml: text, wysiwygStyle: \'multiline\'"></div></div>'
}];

expect(parseData.templates).toEqual(expectedTemplates);
Expand Down
14 changes: 14 additions & 0 deletions src/js/bindings/scrollfix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 = {
Expand Down
61 changes: 47 additions & 14 deletions src/js/bindings/wysiwygs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <br> 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 <p> 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: {},
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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 <br> 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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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);
}
Expand Down
25 changes: 23 additions & 2 deletions src/js/converter/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,29 @@ var processBlock = function(element, defs, themeUpdater, blockPusher, templateUr

newBinding += "wysiwygOrHtml: " + modelBindValue;

if (domutils.getLowerTagName(element) == 'td') {
var wrappingDiv = $('<div data-ko-wrap="false" style="width: 100%; height: 100%"></div>')[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 = $('<div data-ko-wrap="false"'+wrappingDivAttrs+'></div>')[0];
domutils.setAttribute(wrappingDiv, 'data-bind', newBinding);
var newContent = domutils.getInnerHtml($('<div></div>').append(wrappingDiv));
domutils.setContent(element, newContent);
Expand Down

0 comments on commit 65fd8b4

Please sign in to comment.