📜 ⬆️ ⬇️

Bleed TinyMCE 4

Hello, my name is Konstantin, I work as a front-end developer on an infotainment portal, the main content of which is news and articles. And, of course, it was extremely important for us to organize comfortable work with the portal for our editors. About how much progress we have achieved in this field, and there will be this article.

We use the TinyMCE version 4.2.4 WYSIWYG editor on our portal for editing news and articles. He showed himself from the best side among all WYSIWYG editors both in terms of stability of work, and in terms of the quality of the generated HTML markup. In addition, it was the easiest to learn for people accustomed to working with office applications.

But some of its basic capabilities are not enough to fulfill all the editorial needs. I will not describe the process of configuring TinyMCE: firstly, everyone has different needs, and secondly, this point is very well covered in the documentation . But I will talk about solutions that may be useful to many and which are not so easy to find on the Internet.

And today we are talking about:

Work with images


Recently, the number of illustrations in our articles has increased markedly. And therefore, one of the most important tasks for us was the implementation of a simple and convenient mechanism for working with images.
')
Here are the most important points that we have defined for ourselves:

There are quite a few TinyMCE plug-ins for working with graphics (including its native, paid MoxieManager plugin) that mimic file managers. However, as practice has shown, all these rich possibilities “a la” Windows Explorer are completely unnecessary for editors. And so we decided to abandon this concept and simplify the loading of illustrations and adding them to the article as much as possible.

To do this, we placed an additional panel under the TinyMCE window specifically for working with images. We decided that when someone rules a certain text, he should only see those images that are directly related to this article. There won't be so many of them, and you won't have to catalog the images. Also, just in case, we added a second tab to the panel - to work with global illustrations, which can be available in all articles (but it has not yet been used).



To download images, we used the Dropzone.js plugin. It has the following features:

Its configuration, as well as the TinyMCE configuration, is well described in the documentation. I am sure that you can easily sharpen his work for yourself, and therefore I will not focus on it. You can also use any other similar plugin, since there are quite a lot of them now.

Thanks to this approach, we were able to store images on the server as we like, and simplified the process of loading them. But our ultimate goal is still adding images to the text of the article.

So, we have a certain panel on which the list of all images available for an article is displayed, and we need to insert them into the text when clicking on these images. The TinyMCE execCommand editor will help us achieve this :

tinymce.activeEditor.execCommand('mceInsertContent', false, img); 

But this is far from everything - here the most interesting part begins. By acting in this way, we get rich control over the added elements.

For example, on our portal, the width of the content area for the article is strictly limited. And if the loaded image is much wider - it will be reduced to the required size and inserted along with a link to the original. At the same time, rather large images are inserted in our wrappers, which stretch to the full width of the article and filled in along the edges with average color using a jQuery plugin .

The definition of appropriate behavior occurs at the stage of adding illustrations to the text. But what if users will edit images with standard TinyMCE controls? In order not to lose control over the elements, add an event handler for the NodeChange event for the editor (we do this at the time of configuring TinyMCE):

 tinymce.init({ /*   */ setup: function (editor) { editor.on('NodeChange', function (e) { if (e.element.nodeName === 'IMG' && e.element.classList.contains('mce-object') === false) { /*   */ } }); } }); 

Since the various embedded elements (iframe, embedded) in TinyMCE are replaced with a stub image, we perform an additional test for the absence of the mce-object class in order to distinguish them from the usual illustrations.

Having caught the event of an element change and having determined that this element is an image, we regain control over it. We can check if its dimensions have not gone out (the specified sizes will be transferred in the event object: e.width, e.height) beyond permissible limits, whether proportions are not violated (and this has happened), etc. ... I recommend to keep the original image sizes in data- * attribute elements.

You can argue by saying that in order to capture the resize of images in the editor, it is enough to use the ObjectResizeStart and ObjectResized events . However, these events will not work if the dimensions of the illustration are changed using the image insertion / editing tool.

Another trick is to prevent the image from stretching beyond the specified limits (this can be either the limitations of the content area or the maximum dimensions of the image itself), set its max-width and max-height properties in the style attribute when inserting.

Thus, we solved several points of our initial task, but we are still concerned about the resizing of images to the size specified in the article on the server side, so that portal visitors do not have to upload large (heavy) illustrations that would only visually decrease.

This problem is solved quite simply, if you use bb codes to edit text - you simply resize the images at the time of processing the command to insert them into the text. In the case of WYSIWYG editors, you have two options: to parse the generated HTML or use special links. We chose the second.

Regardless of what your backend is written on, you can make sure that, according to certain parameters, a suitable image is formed in the link and placed in the cache. When you insert an element into the editor, it is quite simple to generate the corresponding link, and when you edit the image, the NodeChange event handler will come to your rescue. The main thing to remember is that when the src attribute of an element changes, the data-mce-src attribute will also need to be changed.

This handler is used here (jQuery is used here for working with DOM):

 resizeImage = function ($image, width, height) { var originalWidth = parseInt($image.data('originalWidth'), 10), originalHeight = parseInt($image.data('originalHeight'), 10), ratio, defaultWidth, defaultHeight, link = $image.attr('src'), linkParams; if (typeof width === 'undefined' || width === null) { width = parseInt($image.attr('width'), 10); } if (typeof height === 'undefined' || height === null) { height = parseInt($image.attr('height'), 10); } defaultWidth = width; defaultHeight = height; /*   ,     */ if (isNaN(originalWidth) || originalWidth === 0 || isNaN(originalHeight) || originalHeight === 0) { $image .attr({ width: '', height: '' }) .css({ maxWidth: 'none', maxHeight: 'none' }); originalWidth = $image.width(); originalHeight = $image.height(); ratio = originalWidth / originalHeight; var maxWidth = Math.min(originalWidth, pageWidth), maxHeight = (maxWidth === originalWidth ? originalHeight : Math.round(maxWidth / ratio)); $image .attr({ width: width, height: height, 'data-original-width': originalWidth, 'data-original-height': originalHeight }) .css({ maxWidth: maxWidth, maxHeight: maxHeight }); } else { ratio = originalWidth / originalHeight; } width = Math.min(originalWidth, pageWidth, width); height = (width === originalWidth ? originalHeight : Math.round(width / ratio)); if (link.substr(0, 7) === 'http://') { linkParams = link.substr(7).split('/'); } else { linkParams = link.split('/'); } /*     ,    */ if (linkParams.length === 6 && linkParams[0] === window.location.host && (linkParams[1] === 'r' || linkParams[1] === 'c') && isDecimal(linkParams[2]) && isDecimal(linkParams[3])) { link = 'http://' + linkParams[0] + '/' + linkParams[1] + '/' + width + '/' + height + '/' + linkParams[4] + '/' + linkParams[5]; $image.attr({ src: link, 'data-mce-src': link }); } if (width !== defaultWidth || height !== defaultHeight) { $image.attr({ width: width, height: height }); } } tinymce.init({ /*   */ setup: function (editor) { editor.on('NodeChange', function (e) { if (e.element.nodeName === 'IMG' && e.element.classList.contains('mce-object') === false) { resizeImage($(e.element), e.width, e.height); } }); } }); 

As you can see, even if the original dimensions of the image are not indicated in the data- * attributes, the function tries to calculate them independently and perform all the necessary checks. This approach allows to ensure compatibility with previously accumulated material on the portal.

Formatting HTML Markup


It is this task that caused us the most difficulties.

After a thorough study of the TinyMCE documentation, it was found that there is no way to configure the editor to clean the HTML markup from various garbage when pasting text from Word or from other sites, and would not cut down the functionality of users. We also did not find ready-made solutions that satisfy our needs on the Internet.

I had to do it on my own, and that’s what we did with github.com/WEACOMRU/html-formatting .

The function presented in the repository checks the compliance of the contents of the container passed to it with certain rules and gets rid of all the excess. It is written in pure JS and does not require any dependencies and is distributed under the MIT license.

To format the markup when pasting text into TinyMCE, you need to set a paste_postprocess event handler:

 tinymce.init({ /*   */ paste_postprocess: function (plugin, args) { var valid_elements = { /*    */ }; htmlFormatting(args.node, valid_elements); } }); 

You can familiarize yourself with the principles of configuring rules on a githaba, but I will talk about how this function works.

If you look at a ready-made solution, everything turns out to be quite elementary: in the cycle we iterate through all the child elements of the HTML container and for each we start a separate processing. We perform the function recursively until we reach the deepest level of nesting.

 process = function (node, valid_elements) { var taskSet = [], i; for (i = 0; i < node.childNodes.length; i++) { processNode(node.childNodes[i], valid_elements, taskSet); } doTasks(taskSet); } 

In the process of processing an individual element, we first of all check whether we are dealing with an HTML element or text.

 processNode = function (node, valid_elements, taskSet) { var rule; if (node.nodeType === 1) { /* HTML- */ } else if (node.nodeType === 3) { /*   */ } } 

Text elements are processed in their own way - all non-breaking spaces are removed from them.

 processText = function (node) { node.nodeValue = node.nodeValue.replace(/\xa0/g, ' '); } 

This is due to the fact that our editors experienced difficulties due to lingering inseparable spaces in the copied text, which broke the hyphenation in the article. This procedure solves this problem, but it may turn out to be undesirable for you, if this is the case - correct the source code of the function to your needs.

HTML elements are processed in accordance with the specified rules.

 getRule = function (node, valid_elements) { var re = new RegExp('(?:^|,)' + node.tagName.toLowerCase() + '(?:,|$)'), rules = Object.keys(valid_elements), rule = false, i; for (i = 0; i < rules.length && !rule; i++) { if (re.test(rules[i])) { rule = valid_elements[rules[i]]; } } return rule; } ... processNode = function (node, valid_elements, taskSet) { var rule; if (node.nodeType === 1) { rule = getRule(node, valid_elements); ... } else if (node.nodeType === 3) { processText(node); } } 

If the rule for this element is not found, it is unpacked, i.e. all its child elements are moved to a higher level and replace this element.

 unpack = function (node) { var parent = node.parentNode; while (node.childNodes.length > 0) { parent.insertBefore(node.childNodes[0], node); } } ... if (rule) { if (typeof rule.valid_elements === 'undefined') { process(node, valid_elements); } else { process(node, rule.valid_elements) } ... } else { process(node, valid_elements); if (node.hasChildNodes()) { taskSet.push({ task: 'unpack', node: node }); } taskSet.push({ task: 'remove', node: node }) } 

If there is a corresponding rule, the element is preserved if it is not empty (a container containing at least one text element at any nesting level, consisting not only of spaces, is considered empty) or there is no setting in the rule to delete empty elements (no_empty).

 isEmpty = function (node) { var result = true, re = /^\s*$/, i, child; if (node.hasChildNodes()) { for (i = 0; i < node.childNodes.length && result; i++) { child = node.childNodes[i]; if (child.nodeType === 1) { result = isEmpty(child); } else if (child.nodeType === 3 && !re.test(child.nodeValue)) { result = false; } } } return result; } ... if (rule.no_empty && isEmpty(node)) { taskSet.push({ task: 'remove', node: node }); } else { ... } 

Depending on the configuration of the rule, styles and element classes are checked.

 checkStyles = function (node, valid_styles) { var i, re; if (typeof valid_styles === 'string' && node.style.length) { for (i = node.style.length - 1; i >= 0; i--) { re = new RegExp('(?:^|,)' + node.style[i] + '(?:,|$)'); if (!re.test(valid_styles)) { node.style[node.style[i]] = ''; } } if (!node.style.cssText) { node.removeAttribute('style'); } } } checkClasses = function (node, valid_classes) { var i, re; if (typeof valid_classes === 'string' && node.classList.length) { for (i = node.classList.length - 1; i >= 0; i--) { re = new RegExp('(?:^|\\s)' + node.classList[i] + '(?:\\s|$)'); if (!re.test(valid_classes)) { node.classList.remove(node.classList[i]); } } if (!node.className) { node.removeAttribute('class'); } } } ... checkStyles(node, rule.valid_styles); checkClasses(node, rule.valid_classes); 

The need for conversion and its additional processing is immediately checked and the identifier is deleted.

 if (rule.convert_to) { taskSet.push({ task: 'convert', node: node, convert_to: rule.convert_to }); } else if (node.id) { node.removeAttribute('id'); } if (typeof rule.process === 'function') { taskSet.push({ task: 'process', node: node, process: rule.process }); } 

It should be noted that when converting, a new element is created, into which all child elements of the current container are placed, also, if available, styles and classes are transferred, after which the container is replaced with this new element.

 convert = function (node, convert_to) { var parent = node.parentNode, converted = document.createElement(convert_to); if (node.style.cssText) { converted.style.cssText = node.style.cssText; } if (node.className) { converted.className = node.className; } while (node.childNodes.length > 0) { converted.appendChild(node.childNodes[0]); } parent.replaceChild(converted, node); } 

As you have probably noticed, all the manipulations with the DOM are queued and executed at the end of the current loop on the elements, so as not to violate it.

 doTasks = function (taskSet) { var i; for (i = 0; i < taskSet.length; i++) { switch (taskSet[i].task) { case 'remove': taskSet[i].node.parentNode.removeChild(taskSet[i].node); break; case 'convert': convert(taskSet[i].node, taskSet[i].convert_to); break; case 'process': taskSet[i].process(taskSet[i].node); break; case 'unpack': unpack(taskSet[i].node); break; } } } 

You can find demo of the function in the repository I hope that this description will help you modify the function to suit your specific needs, if in its pure form it does not satisfy you, or at least serves as an ideological inspiration.

Typography


And finally, the simplest thing remains - the introduction of the printer. We used the wonderful script of Denis Seleznev hcodes github.com/typograf/typograf .

All you need to do is write a small TinyMCE plugin:

 tinymce.PluginManager.add('typograf', function (editor, url) { 'use strict'; var scriptLoader = new tinymce.dom.ScriptLoader(), tp, typo = function () { if (tp) { editor.setContent(tp.execute(editor.getContent())); editor.undoManager.add(); } }; scriptLoader.add(url + '/typograf.min.js'); scriptLoader.loadQueue(function () { tp = new Typograf({ lang: 'ru', mode: 'name' }); }); editor.addButton('typograf', { text: '', icon: 'blockquote', onclick: typo }); editor.addMenuItem('typograf', { context: 'format', text: '', icon: 'blockquote', onclick: typo }); }); 

As you can see, the typographer's script is placed in the folder with the plugin and loaded asynchronously using the tools of the editor https://www.tinymce.com/docs/api/class/tinymce.dom.scriptloader/ . You can download the script separately from TinyMCE, if you plan to use the printer and other functions.

When the script is loaded, the tp variable is initialized. The content of the editor is accessed using the getContent () and setContent () methods. And, of course, after applying typography, you need to add another level of rollback changes using undoManager .

As an icon for the button and menu item, we used the font icon of the editor for quotes. You can see the list of available icons in TinyMCE in the skin.css file - (.mce-i- * classes).

That's all, we hope our experience will help you in the implementation of your own ideas and reduce the time for finding solutions.

Source: https://habr.com/ru/post/266337/


All Articles