froala / wysiwyg-editor

The next generation Javascript WYSIWYG HTML Editor.
https://www.froala.com/wysiwyg-editor
Other
5.28k stars 672 forks source link

Styles removes when toggle Bold/Italic/Underline/Strikethroughs/Subscript/Superscript #4502

Open OlesZadorozhnyy opened 2 years ago

OlesZadorozhnyy commented 2 years ago
Expected behavior.

When removing text styling like bold/underline/italic... with already set paragraph styles, it should save paragraph styles as it is.

Actual behavior.

When removing text styling like bold/underline/italic... with already set paragraph styles, it removes paragraph styles.

Steps to reproduce the problem.

https://jsfiddle.net/Froala_marketing/tL0pdbnh/7/

  1. Select all text
  2. Align text to center
  3. Bold text
  4. Unbold text
  5. Observe that align is set to default - left.

HTML changes from example above: Step 1) <p>random text</p> Step 2) <p style="text-align: center;">random text</p> Step 3) <p style="text-align: center;"><strong>random text</strong></p> Step 4) <p>random text</p>

Editor version.

4.0.13

OlesZadorozhnyy commented 2 years ago

4298 Same issue here

OlesZadorozhnyy commented 2 years ago

same here #4389

Raiyan-Memon commented 2 years ago

Same issue here, can anyone provide a solution

malsieb commented 1 year ago

We had the same issue, as shown in the following video: https://cdn1.site-media.eu/uploads/237735/0/c7d92e842ce99c749a03424f430d7be21c8f59b1636cb3aa99120.webm

We fixed it by dirty-patching the whole format module. The actual fix is in line 491 and excludes the

tag from the parent node style removal feature:


/**
 * Fix for format toggle bug
 * https://github.com/froala/wysiwyg-editor/issues/4502
 *
 * Extracted from Froala Version 4.0.11
 */

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? factory(require('froala-editor'))
    : typeof define === 'function' && define.amd
    ? define(['froala-editor'], factory)
    : factory(global.FroalaEditor);
})(this, function (FroalaEditor) {
  FroalaEditor.MODULES.format = function (editor) {
    var $ = editor.$;
    /**
     * Create open tag string.
     */

    function _openTag(tag, attrs) {
      var str = '<'.concat(tag);

      for (var key in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, key)) {
          str += ' '.concat(key, '="').concat(attrs[key], '"');
        }
      }

      str += '>';
      return str;
    }
    /**
     * Create close tag string.
     */

    function _closeTag(tag) {
      return '</'.concat(tag, '>');
    }
    /**
     * Create query for the current format.
     */

    function _query(tag, attrs) {
      var selector = tag;

      for (var key in attrs) {
        if (Object.prototype.hasOwnProperty.call(attrs, key)) {
          if (key === 'id') {
            selector += '#'.concat(attrs[key]);
          } else if (key === 'class') {
            selector += '.'.concat(attrs[key]);
          } else {
            selector += '['.concat(key, '="').concat(attrs[key], '"]');
          }
        }
      }

      return selector;
    }
    /**
     * Test matching element.
     */

    function _matches(el, selector) {
      if (!el || el.nodeType !== Node.ELEMENT_NODE) {
        return false;
      }

      return (
        el.matches ||
        el.matchesSelector ||
        el.msMatchesSelector ||
        el.mozMatchesSelector ||
        el.webkitMatchesSelector ||
        el.oMatchesSelector
      ).call(el, selector);
    }
    /**
     * Apply format to the current node till we find a marker.
     */

    function _processNodeFormat(start_node, tag, attrs) {
      var parent_li_node;
      var style_alt_for_tags = {
        strong: {
          prop: 'font-weight',
          val: 'bold',
        },
        em: {
          prop: 'font-style',
          val: 'italic',
        },
      };
      var prop;
      var val; // No start node.

      if (!start_node) {
        return;
      } // https://github.com/froala-labs/froala-editor-js-2/issues/2353

      if (
        (editor.node.isBlock(start_node) &&
          start_node.hasAttribute('contenteditable') &&
          start_node.getAttribute('contenteditable') === 'false') ||
        (start_node.parentNode &&
          start_node.parentNode.hasAttribute('contenteditable') &&
          start_node.parentNode.getAttribute('contenteditable') === 'false')
      ) {
        if (start_node.nextSibling && $(start_node.nextSibling).hasClass('fr-marker')) {
          return;
        } else if (start_node.nextSibling) {
          _processNodeFormat(start_node.nextSibling, tag, attrs);

          return;
        } else if (start_node.parentNode && editor.node.isEditable(start_node.parentNode)) {
          _processNodeFormat(start_node.parentNode, tag, attrs);

          return;
        }
      } // Skip comments.

      while (start_node && start_node.nodeType === Node.COMMENT_NODE) {
        start_node = start_node.nextSibling;
      } // No start node.

      if (!start_node) {
        return;
      } // If we are in a block process starting with the first child.

      if (
        editor.node.isBlock(start_node) &&
        start_node.tagName !== 'HR' &&
        start_node.tagName !== 'LI'
      ) {
        if (editor.node.hasClass(start_node.firstChild, 'fr-marker')) {
          _processNodeFormat(start_node.firstChild.nextSibling, tag, attrs);
        } else {
          _processNodeFormat(start_node.firstChild, tag, attrs);
        }

        return false;
      } // Create new element.

      var $span = $(editor.doc.createElement(tag));
      $span.attr(attrs);
      $span.insertBefore(start_node); // Apply style for the parent when the parent element is a list item.

      parent_li_node = _getStyleApplicableLI(start_node);

      if (
        parent_li_node &&
        (['strong', 'em'].indexOf(tag) >= 0 || (tag === 'span' && attrs.hasOwnProperty('style')))
      ) {
        if (tag === 'span') {
          style_alt_for_tags = attrs.style.replace(/;$/, '').split(':');
          prop = style_alt_for_tags[0].trim();
          val = style_alt_for_tags[1].trim();
        } else {
          prop = style_alt_for_tags[tag].prop;
          val = style_alt_for_tags[tag].val;
        } // Style will not be applied to the parent list element,  if it is the background-color style
        // as it is not making any effect on bullets in HTML.

        if (prop !== 'background-color') {
          $(parent_li_node).css(prop, val);

          _keepDefaultStylesForSubElements(parent_li_node, prop);
        }
      } // Start with the next sibling of the current node.

      var node = start_node; // Search while there is a next node.
      // Next node is not marker.
      // Next node does not contain marker.
      // Next node is not an inner list.

      while (
        node &&
        !$(node).hasClass('fr-marker') &&
        $(node).find('.fr-marker').length === 0 &&
        node.tagName !== 'UL' &&
        node.tagName !== 'OL'
      ) {
        var tmp = node; // Check for Track Changes deleted content

        if (node.tagName === 'SPAN' && $(node).hasClass('fr-tracking-deleted')) {
          node = node.nextSibling;
          continue;
        }

        if (editor.node.isBlock(node) && start_node.tagName !== 'HR') {
          _processNodeFormat(node.firstChild, tag, attrs);

          return false;
        } // merged conflict from master, kept both changes

        if (node.tagName === 'SPAN' && editor.node.isEditable(node)) {
          _processNodeFormat(node.firstChild, tag, attrs);

          return false;
        } // https://github.com/froala-labs/froala-editor-js-2/issues/2353

        if (
          node.tagName &&
          node.hasAttribute('contenteditable') &&
          node.getAttribute('contenteditable') === 'false'
        ) {
          _processNodeFormat(node.nextSibling, tag, attrs);

          return;
        } // till here

        if (editor.node.isEditable(node.parentNode)) {
          node = node.nextSibling;
          $span.append(tmp);
        } else {
          editor.selection.restore();
          editor.toolbar.disable();
          return;
        }
      } // If there is no node left at the right look at parent siblings.

      if (!node) {
        var p_node = $span.get(0).parentNode;

        while (p_node && !p_node.nextSibling && !editor.node.isElement(p_node)) {
          p_node = p_node.parentNode;
        }

        if (p_node) {
          var sibling = p_node.nextSibling;

          if (sibling) {
            // Parent sibling is block then look next.
            if (!editor.node.isBlock(sibling)) {
              _processNodeFormat(sibling, tag, attrs);
            } else if (sibling.tagName === 'HR') {
              _processNodeFormat(sibling.nextSibling, tag, attrs);
            } else {
              _processNodeFormat(sibling.firstChild, tag, attrs);
            }
          }
        }
      } // Start processing child nodes if there is a marker or an inner list.
      else if (
        $(node).find('.fr-marker').length ||
        node.tagName === 'UL' ||
        node.tagName === 'OL'
      ) {
        _processNodeFormat(node.firstChild, tag, attrs);
      }

      if ($span.is(':empty')) {
        $span.remove();
      }
    }
    /**
     * Apply tag format.
     */

    function apply(tag, attrs) {
      var i;

      if (typeof attrs === 'undefined') {
        attrs = {};
      }

      if (attrs.style) {
        delete attrs.style;
      } // Selection is collapsed.

      if (editor.selection.isCollapsed()) {
        editor.markers.insert();
        var $marker = editor.$el.find('.fr-marker'); // https://github.com/froala-labs/froala-editor-js-2/issues/2598
        // in case of list item, if the marker is outside the default tag, then place the marker inside

        if (
          $marker.get(0).nextSibling &&
          editor.node.isBlock($marker.get(0).nextSibling) &&
          !$marker.get(0).previousSibling &&
          $marker.get(0).parentNode.tagName === 'LI'
        ) {
          $marker.get(0).nextSibling.prepend($marker.get(0));
        }

        $marker.replaceWith(
          _openTag(tag, attrs) +
            FroalaEditor.INVISIBLE_SPACE +
            FroalaEditor.MARKERS +
            _closeTag(tag)
        );
        editor.selection.restore();
      } // Selection is not collapsed.
      else {
        editor.selection.save(); // Check if selection can be deleted.

        var start_marker =
          editor.$el.find('.fr-marker[data-type="true"]').length &&
          editor.$el.find('.fr-marker[data-type="true"]').get(0).nextSibling;

        _processNodeFormat(start_marker, tag, attrs); //clean empty anchor tags

        $(start_marker).parent().find('a:empty').remove(); // Clean inner spans.

        var inner_spans;

        do {
          inner_spans = editor.$el.find(
            ''.concat(_query(tag, attrs), ' > ').concat(_query(tag, attrs))
          );

          for (i = 0; i < inner_spans.length; i++) {
            inner_spans[i].outerHTML = inner_spans[i].innerHTML;
          }
        } while (inner_spans.length);

        editor.el.normalize(); // Have markers inside the new tag.

        var markers = editor.el.querySelectorAll('.fr-marker');

        for (i = 0; i < markers.length; i++) {
          var $mk = $(markers[i]);

          if ($mk.data('type') === true) {
            if (_matches($mk.get(0).nextSibling, _query(tag, attrs))) {
              $mk.next().prepend($mk);
            }
          } else if (_matches($mk.get(0).previousSibling, _query(tag, attrs))) {
            $mk.prev().append($mk);
          }
        }

        editor.selection.restore();
      }
    }
    /**
     * Split at current node the parents with tag.
     */

    function _split($node, tag, attrs, collapsed) {
      if (!collapsed) {
        var changed = false;

        if ($node.data('type') === true) {
          while (
            editor.node.isFirstSibling($node.get(0)) &&
            !$node.parent().is(editor.$el) &&
            !$node.parent().is('ol') &&
            !$node.parent().is('ul')
          ) {
            $node.parent().before($node);
            changed = true;
          }
        } else if ($node.data('type') === false) {
          while (
            editor.node.isLastSibling($node.get(0)) &&
            !$node.parent().is(editor.$el) &&
            !$node.parent().is('ol') &&
            !$node.parent().is('ul')
          ) {
            $node.parent().after($node);
            changed = true;
          }
        }

        if (changed) {
          return true;
        }
      } // Check if current node has parents which match our tag.

      if ($node.parents(tag).length || typeof tag === 'undefined') {
        var close_str = '';
        var open_str = '';
        var $p_node = $node.parent();
        var p_html; //https://github.com/froala-labs/froala-editor-js-2/issues/4261

        if ($p_node[0].tagName === 'A') {
          $p_node = $p_node.parent();
        } // Do not split when parent is block.

        if ($p_node.is(editor.$el) || editor.node.isBlock($p_node.get(0))) {
          return false;
        } // Check undefined so that we.

        while (
          !editor.node.isBlock($p_node.parent().get(0)) &&
          (typeof tag === 'undefined' || !_matches($p_node.get(0), _query(tag, attrs)))
        ) {
          close_str += editor.node.closeTagString($p_node.get(0));
          open_str = editor.node.openTagString($p_node.get(0)) + open_str;
          $p_node = $p_node.parent();
        } // Node STR.

        var node_str = $node.get(0).outerHTML; // Replace node with marker.

        $node.replaceWith('<span id="mark"></span>'); // Rebuild the HTML for the node.

        p_html = $p_node
          .html()
          .replace(
            /<span id="mark"><\/span>/,
            close_str +
              editor.node.closeTagString($p_node.get(0)) +
              open_str +
              node_str +
              close_str +
              editor.node.openTagString($p_node.get(0)) +
              open_str
          );
        $p_node.replaceWith(
          editor.node.openTagString($p_node.get(0)) +
            p_html +
            editor.node.closeTagString($p_node.get(0))
        );
        return true;
      }

      return false;
    }
    /**
     * Process node remove.
     */

    function _processNodeRemove($node, should_remove, tag, attrs) {
      var parent_li_node;
      var style_alt_for_tags = {
        strong: {
          prop: 'font-weight',
          val: 'bold',
        },
        em: {
          prop: 'font-style',
          val: 'italic',
        },
      }; // Get contents.

      var contents = editor.node.contents($node.get(0)); // Loop contents.

      for (var i = 0; i < contents.length; i++) {
        var node = contents[i]; // https://github.com/froala-labs/froala-editor-js-2/issues/1954
        // https://github.com/froala-labs/froala-editor-js-2/issues/2644
        // https://github.com/froala-labs/froala-editor-js-2/issues/2770
        // Remove node if it contains zero-width space character only.

        if (
          node.innerHTML &&
          node.innerHTML.charCodeAt() == 8203 &&
          node.tagName.toLocaleLowerCase() == tag &&
          node.childNodes.length < 2 &&
          !editor.helpers.isMobile()
        ) {
          node.outerHTML = node.innerHTML;
        } // We found a marker => change should_remove flag.

        if (editor.node.hasClass(node, 'fr-marker')) {
          should_remove = (should_remove + 1) % 2;
        } // We should remove.
        else if (should_remove) {
          // Check if we have a marker inside it.
          if ($(node).find('.fr-marker').length > 0) {
            should_remove = _processNodeRemove($(node), should_remove, tag, attrs);
          } // Remove everything starting with the most inner nodes which match the current selector.
          else {
            // Clear the style for the parent list item.
            parent_li_node =
              node.tagName === 'LI' ? node : $(node).parentsUntil(editor.$el, 'li').get(0);

            if (
              parent_li_node &&
              (typeof tag === 'undefined' || ['strong', 'em'].indexOf(tag) >= 0)
            ) {
              if (tag) {
                $(parent_li_node).css(style_alt_for_tags[tag].prop, '');
              } else {
                parent_li_node.style = '';
              }
            } // https://github.com/froala-labs/froala-editor-js-2/issues/2916

            parent_li_node = node.parentNode !== editor.el ? node.parentNode : null;

            if (
              parent_li_node &&
              parent_li_node.nodeType === 1 &&
              tag !== 'a' &&
              parent_li_node.hasAttribute('style') &&
              parent_li_node.tagName !== 'SPAN' &&
              parent_li_node.tagName !== 'LI' &&
              /** custom fix - also exclude p tags from style removal */
              parent_li_node.tagName !== 'P'
              /** custom fix end */
            ) {
              parent_li_node.style = '';
            } else if (node && node.nodeType === 1 && tag !== 'a' && node.hasAttribute('style')) {
              //https://github.com/froala-labs/froala-editor-js-2/issues/4261
              //if block check for msie browsr and else for other browsers
              if (editor.browser.msie) {
                $(node).attr('style', '');
              } else {
                node.style = '';
              }
            }

            var nodes = $(node).find(tag || '*:not(br)');

            for (var j = nodes.length - 1; j >= 0; j--) {
              var nd = nodes[j]; // Clear the style for the parent list item.

              parent_li_node =
                nd.tagName === 'LI' ? nd : $(nd).parentsUntil(editor.$el, 'li').get(0);

              if (parent_li_node && (!tag || ['strong', 'em'].indexOf(tag) >= 0)) {
                if (tag) {
                  $(parent_li_node).css(style_alt_for_tags[tag].prop, '');
                } else {
                  parent_li_node.style = '';
                }
              }

              if (
                nd.tagName !== 'A' &&
                !editor.node.isBlock(nd) &&
                !editor.node.isVoid(nd) &&
                (typeof tag === 'undefined' || _matches(nd, _query(tag, attrs)))
              ) {
                if (
                  !editor.node.hasClass(nd, 'fr-clone') &&
                  !editor.node.hasClass(nd, 'fr-tracking-deleted') &&
                  !$(nd).data('tracking')
                ) {
                  nd.outerHTML = nd.innerHTML;
                }
              } else if (
                editor.node.isBlock(nd) &&
                typeof tag === 'undefined' &&
                node.tagName !== 'TABLE'
              ) {
                editor.node.clearAttributes(nd);
              }
            } // Check inner nodes.

            if (
              (node.tagName !== 'A' &&
                typeof tag === 'undefined' &&
                node.nodeType === Node.ELEMENT_NODE &&
                !editor.node.isVoid(node)) ||
              _matches(node, _query(tag, attrs))
            ) {
              if (!editor.node.isBlock(node)) {
                if (!editor.node.hasClass(node, 'fr-clone') && !editor.opts.trackChangesEnabled) {
                  node.outerHTML = node.innerHTML;
                } else if (
                  !editor.node.hasClass(node, 'fr-clone') &&
                  editor.opts.trackChangesEnabled &&
                  node.parentNode
                ) {
                  node.outerHTML = node.innerHTML;
                }
              }
            } // Remove formatting from block nodes.
            else if (
              typeof tag === 'undefined' &&
              node.nodeType === Node.ELEMENT_NODE &&
              editor.node.isBlock(node) &&
              node.tagName !== 'TABLE'
            ) {
              editor.node.clearAttributes(node);
            }
          }
        } else {
          // There is a marker.
          if ($(node).find('.fr-marker').length > 0) {
            should_remove = _processNodeRemove($(node), should_remove, tag, attrs);
          }
        }
      }

      return should_remove;
    }
    /**
     * Remove tag.
     */

    function remove(tag, attrs) {
      if (typeof attrs === 'undefined') {
        attrs = {};
      }

      if (attrs.style) {
        delete attrs.style;
      }

      var collapsed = editor.selection.isCollapsed();
      editor.selection.save(); // Split at start and end marker.

      var reassess = true;

      while (reassess) {
        reassess = false;
        var markers = editor.$el.find('.fr-marker');

        for (var i = 0; i < markers.length; i++) {
          var $marker = $(markers[i]);
          var $clone = null;

          if (!$marker.attr('data-cloned') && !collapsed) {
            $clone = $marker.clone().removeClass('fr-marker').addClass('fr-clone');

            if ($marker.data('type') && $marker.data('type').toString() === 'true') {
              $marker.attr('data-cloned', true).after($clone);
            } else {
              $marker.attr('data-cloned', true).before($clone);
            }
          }

          if (_split($marker, tag, attrs, collapsed)) {
            reassess = true;
            break;
          }
        }
      } // Remove format between markers.

      _processNodeRemove(editor.$el, 0, tag, attrs); // Replace markers with their clones.

      if (!collapsed) {
        editor.$el.find('.fr-marker').remove();
        editor.$el.find('.fr-clone').removeClass('fr-clone').addClass('fr-marker');
      } // Selection is collapsed => add invisible spaces.

      if (collapsed) {
        editor.$el
          .find('.fr-marker')
          .before(FroalaEditor.INVISIBLE_SPACE)
          .after(FroalaEditor.INVISIBLE_SPACE);
      }

      editor.html.cleanEmptyTags();
      editor.el.normalize();
      editor.selection.restore(); // https://github.com/froala-labs/froala-editor-js-2/issues/2168

      var anchorNode = editor.win.getSelection() && editor.win.getSelection().anchorNode;

      if (anchorNode) {
        var blockParent = editor.node.blockParent(anchorNode);
        var multiSelection = anchorNode.textContent.replace(/\u200B/g, '').length ? true : false;

        var _editor$win$getSelect = editor.win.getSelection().getRangeAt(0),
          startOffset = _editor$win$getSelect.startOffset,
          endOffset = _editor$win$getSelect.endOffset; // Keep only one zero width space and remove all the other zero width spaces if selection consists of only zerowidth spaces.

        if (!editor.selection.text().replace(/\u200B/g, '').length) {
          removeZeroWidth(blockParent, anchorNode);
        }

        var range = editor.win.getSelection().getRangeAt(0); // Setting the range to the zerowidthspace index

        if (anchorNode.nodeType === Node.TEXT_NODE) {
          if (!multiSelection || (!editor.selection.text().length && startOffset === endOffset)) {
            var newOffset = anchorNode.textContent.search(/\u200B/g) + 1; // Fix for IE browser

            if (editor.browser.msie) {
              var tmprange = editor.doc.createRange();
              editor.selection.get().removeAllRanges();
              tmprange.setStart(anchorNode, newOffset);
              tmprange.setEnd(anchorNode, newOffset);
              editor.selection.get().addRange(tmprange);
            } else {
              range.setStart(anchorNode, newOffset);
              range.setEnd(anchorNode, newOffset);
            }
          }
        } else {
          var txtNodeToFocus;
          var tmpNode;
          var tmpIndex = 0;
          var tmpContents = $(anchorNode).contents(); // Fix for IE browser

          if (editor.browser.msie) {
            while ((tmpNode = tmpContents[tmpIndex])) {
              if (
                tmpNode.nodeType === Node.TEXT_NODE &&
                tmpNode.textContent.search(/\u200B/g) >= 0
              ) {
                txtNodeToFocus = tmpNode;
              }

              tmpIndex++;
            }

            txtNodeToFocus = $(txtNodeToFocus);
          } else {
            txtNodeToFocus = tmpContents.filter(function (tmpNode) {
              return (
                tmpNode.nodeType === Node.TEXT_NODE && tmpNode.textContent.search(/\u200B/g) >= 0
              );
            });
          }

          if (txtNodeToFocus.length && !editor.opts.trackChangesEnabled) {
            var _newOffset = txtNodeToFocus.text().search(/\u200B/g) + 1;

            range.setStart(txtNodeToFocus.get(0), _newOffset);
            range.setEnd(txtNodeToFocus.get(0), _newOffset);
          }
        }
      }
    } // Removes zerowidth spaces and keeps only one zero width space for the marker.

    function removeZeroWidth(blockParent, compareNode) {
      if (blockParent && compareNode) {
        if (blockParent.isSameNode(compareNode)) {
          // keeping only one zerowidth space if there are multiple
          blockParent.textContent = blockParent.textContent.replace(/\u200B(?=.*\u200B)/g, '');
        } else {
          if (blockParent.nodeType === Node.TEXT_NODE)
            blockParent.textContent = blockParent.textContent.replace(/\u200B/g, '');
        }

        if (!blockParent.childNodes.length) {
          return false;
        } else if (Array.isArray(blockParent.childNodes)) {
          blockParent.childNodes.forEach(function (node) {
            removeZeroWidth(node, compareNode);
          });
        }
      }
    }
    /**
     * Toggle format.
     */

    function toggle(tag, attrs) {
      if (is(tag, attrs)) {
        remove(tag, attrs);
      } else {
        apply(tag, attrs);
      }
    }
    /**
     * Clean format.
     */

    function _cleanFormat(elem, prop) {
      var $elem = $(elem);
      $elem.css(prop, '');

      if ($elem.attr('style') === '') {
        $elem.replaceWith($elem.html());
      }
    }
    /**
     * Filter spans with specific property.
     */

    function _filterSpans(elem, prop) {
      return (
        $(elem).attr('style').indexOf(''.concat(prop, ':')) === 0 ||
        $(elem).attr('style').indexOf(';'.concat(prop, ':')) >= 0 ||
        $(elem).attr('style').indexOf('; '.concat(prop, ':')) >= 0
      );
    }
    /**
     * Apply inline style.
     */

    function applyStyle(prop, val) {
      var i;
      var $marker;
      var $span = null;
      var parent_li_node;
      var inner_spans; // Selection is collapsed.

      if (editor.selection.isCollapsed()) {
        editor.markers.insert();
        $marker = editor.$el.find('.fr-marker');
        var $parent = $marker.parent(); // https://github.com/froala/wysiwyg-editor/issues/1084

        if (
          editor.node.openTagString($parent.get(0)) ===
          '<span style="'.concat(prop, ': ').concat($parent.css(prop), ';">')
        ) {
          if (editor.node.isEmpty($parent.get(0))) {
            $span = $(editor.doc.createElement('span'))
              .attr('style', ''.concat(prop, ': ').concat(val, ';'))
              .html(''.concat(FroalaEditor.INVISIBLE_SPACE).concat(FroalaEditor.MARKERS));
            $parent.replaceWith($span);
          } // We should get out of the current span with the same props.
          else {
            var x = {};
            x['style*'] = ''.concat(prop, ':');

            _split($marker, 'span', x, true);

            $marker = editor.$el.find('.fr-marker');

            if (val) {
              $span = $(editor.doc.createElement('span'))
                .attr('style', ''.concat(prop, ': ').concat(val, ';'))
                .html(''.concat(FroalaEditor.INVISIBLE_SPACE).concat(FroalaEditor.MARKERS));
              $marker.replaceWith($span);
            } else {
              $marker.replaceWith(FroalaEditor.INVISIBLE_SPACE + FroalaEditor.MARKERS);
            }
          }

          editor.html.cleanEmptyTags();
        } else if (editor.node.isEmpty($parent.get(0)) && $parent.is('span')) {
          $marker.replaceWith(FroalaEditor.MARKERS);
          $parent.css(prop, val);
        } else {
          // https://github.com/froala-labs/froala-editor-js-2/issues/2598
          // in case of list item, if the marker is outside the default tag, then place the marker inside
          if (
            $marker.get(0).nextSibling &&
            editor.node.isBlock($marker.get(0).nextSibling) &&
            !$marker.get(0).previousSibling &&
            $marker.get(0).parentNode.tagName === 'LI'
          ) {
            $marker.get(0).nextSibling.prepend($marker.get(0));
          }

          $span = $(
            '<span style="'
              .concat(prop, ': ')
              .concat(val, ';">')
              .concat(FroalaEditor.INVISIBLE_SPACE)
              .concat(FroalaEditor.MARKERS, '</span>')
          );
          $marker.replaceWith($span);
        } // If we have a span, then split the parent nodes.

        if ($span) {
          _splitParents($span, prop, val);
        }
      } else {
        editor.selection.save(); // When removing selection we should make sure we have selection outside of the first/last parent node.
        // We also need to do this for U tags.

        if (
          val === null ||
          (prop === 'color' && editor.$el.find('.fr-marker').parents('u, a').length > 0)
        ) {
          var markers = editor.$el.find('.fr-marker');

          for (i = 0; i < markers.length; i++) {
            $marker = $(markers[i]);

            if ($marker.data('type') === true || $marker.data('type') === 'true') {
              while (
                editor.node.isFirstSibling($marker.get(0)) &&
                !$marker.parent().is(editor.$el) &&
                !editor.node.isElement($marker.parent().get(0)) &&
                !editor.node.isBlock($marker.parent().get(0))
              ) {
                $marker.parent().before($marker);
              }
            } else {
              while (
                editor.node.isLastSibling($marker.get(0)) &&
                !$marker.parent().is(editor.$el) &&
                !editor.node.isElement($marker.parent().get(0)) &&
                !editor.node.isBlock($marker.parent().get(0))
              ) {
                $marker.parent().after($marker);
              }
            }
          }
        } // Check if selection can be deleted.

        var start_marker = editor.$el.find('.fr-marker[data-type="true"]').get(0).nextSibling;

        while (start_marker.firstChild) {
          start_marker = start_marker.firstChild;
        }

        var attrs = {
          class: 'fr-unprocessed',
        };

        if (val) {
          attrs.style = ''.concat(prop, ': ').concat(val, ';');
        }

        _processNodeFormat(start_marker, 'span', attrs);

        editor.$el.find('.fr-marker + .fr-unprocessed').each(function () {
          $(this).prepend($(this).prev());
        });
        editor.$el.find('.fr-unprocessed + .fr-marker').each(function () {
          $(this).prev().append($(this));
        }); // When em are being used keep them as the most inner props.

        if ((val || '').match(/\dem$/)) {
          editor.$el.find('span.fr-unprocessed').removeClass('fr-unprocessed');
        }

        while (editor.$el.find('span.fr-unprocessed').length > 0) {
          $span = editor.$el.find('span.fr-unprocessed').first().removeClass('fr-unprocessed');
          parent_li_node = _getStyleApplicableLI($span); // Look at parent node to see if we can merge with it.

          $span.parent().get(0).normalize();

          if ($span.parent().is('span') && $span.parent().get(0).childNodes.length === 1) {
            var tempVal = val; // https://github.com/froala-labs/froala-editor-js-2/issues/3827

            if (editor.browser.msie && !val) {
              tempVal = '';
            }

            $span.parent().css(prop, tempVal);
            var $child = $span;
            $span = $span.parent();
            $child.replaceWith($child.html());
          } // Replace in reverse order to take care of the inner spans first.

          inner_spans = $span.find('span'); // Check if there is a valid parent list item, where the style is applied,
          // then clear the unnecessary spans inside that.

          if (parent_li_node && prop !== 'background-color') {
            parent_li_node.normalize();
            inner_spans = $(parent_li_node).find('span:not(.fr-unprocessed)');
          }

          for (i = inner_spans.length - 1; i >= 0; i--) {
            _cleanFormat(inner_spans[i], prop);
          } // Split parent nodes.

          _splitParents($span, prop, val);
        }
      }

      _normalize();
    }
    /**
     * Keep the initial style for the child elements.
     */

    function _keepDefaultStylesForSubElements(node, prop) {
      var childElements = node.childNodes;
      var i;

      for (i = 0; i < childElements.length; i++) {
        if (
          ['UL', 'OL', 'LI'].indexOf(childElements[i].tagName) >= 0 &&
          childElements[i].style[prop] === ''
        ) {
          $(childElements[i]).css(prop, 'initial');
        }
      }
    }
    /**
     * Get the parent list item, when the whole content inside is selected,
     * except the nested list elements if any.
     */

    function _getStyleApplicableLI(start_node) {
      var parent_li_node;
      var end_marker;
      var end_marker_parent_li_node;
      var end_marker_parent;
      var next_sibling;
      var li_node_info;
      parent_li_node =
        start_node.tagName === 'LI'
          ? start_node
          : $(start_node).parentsUntil(editor.$el, 'li').get(0);

      if (parent_li_node) {
        li_node_info = editor.selection.info(parent_li_node); // Return the parent list element if the whole content inside is selected.

        if (li_node_info.atStart && li_node_info.atEnd) {
          return parent_li_node;
        } else if (li_node_info.atStart && !li_node_info.atEnd) {
          end_marker = $(parent_li_node).find('.fr-marker[data-type=false]').get(0);
          end_marker_parent_li_node = $(end_marker).parentsUntil(editor.$el, 'li').get(0);
          end_marker_parent = $(end_marker).parent().get(0);
          next_sibling = end_marker.nextSibling; // Return the parent list element if the whole content inside is selected
          // except the nested elements.

          if (
            (next_sibling && ['UL', 'OL'].indexOf(next_sibling.tagName) >= 0) ||
            !end_marker_parent_li_node.isSameNode(parent_li_node) ||
            (!next_sibling &&
              (end_marker_parent.tagName === 'LI' ||
                !end_marker_parent.nextSibling ||
                ['UL', 'OL'].indexOf(end_marker_parent.nextSibling.tagName) >= 0 ||
                editor.node.isVoid(end_marker_parent.nextSibling)))
          ) {
            return parent_li_node;
          }
        }
      }

      return;
    }

    function _splitParents($span, prop, val) {
      var i; // Look at parents with the same property.

      var $outer_span = $span.parentsUntil(editor.$el, 'span[style]');
      var to_remove = [];

      for (i = $outer_span.length - 1; i >= 0; i--) {
        if (!_filterSpans($outer_span[i], prop)) {
          to_remove.push($outer_span[i]);
        }
      }

      $outer_span = $outer_span.not(to_remove);

      if ($outer_span.length) {
        var c_str = '';
        var o_str = '';
        var ic_str = '';
        var io_str = '';
        var c_node = $span.get(0);

        do {
          c_node = c_node.parentNode;
          $(c_node).addClass('fr-split');
          c_str += editor.node.closeTagString(c_node);
          o_str = editor.node.openTagString($(c_node).clone().addClass('fr-split').get(0)) + o_str; // Inner close and open.

          if ($outer_span.get(0) !== c_node) {
            ic_str += editor.node.closeTagString(c_node);
            io_str =
              editor.node.openTagString($(c_node).clone().addClass('fr-split').get(0)) + io_str;
          }
        } while ($outer_span.get(0) !== c_node); // Build breaking string.

        var str = ''
          .concat(
            c_str +
              editor.node.openTagString(
                $($outer_span.get(0))
                  .clone()
                  .css(prop, val || '')
                  .get(0)
              ) +
              io_str +
              $span.css(prop, '').get(0).outerHTML +
              ic_str,
            '</span>'
          )
          .concat(o_str);
        $span.replaceWith('<span id="fr-break"></span>');
        var html = $outer_span.get(0).outerHTML; // Replace the outer node.

        $($outer_span.get(0)).replaceWith(
          html.replace(/<span id="fr-break"><\/span>/g, function () {
            return str;
          })
        );
      }
    }

    function _normalize() {
      var i;

      while (editor.$el.find('.fr-split:empty').length > 0) {
        editor.$el.find('.fr-split:empty').remove();
      }

      editor.$el.find('.fr-split').removeClass('fr-split');
      editor.$el.find('[style=""]').removeAttr('style');
      editor.$el.find('[class=""]').removeAttr('class');
      editor.html.cleanEmptyTags();
      var $spans = editor.$el.find('span');

      for (var k = $spans.length - 1; k >= 0; k--) {
        var msp = $spans[k];

        if (!msp.attributes || msp.attributes.length === 0) {
          $(msp).replaceWith(msp.innerHTML);
        }
      }

      editor.el.normalize(); // Join current spans together if they are one next to each other.

      var just_spans = editor.$el.find('span[style] + span[style]');

      for (i = 0; i < just_spans.length; i++) {
        var $x = $(just_spans[i]);
        var $p = $(just_spans[i]).prev();

        if (
          $x.get(0).previousSibling === $p.get(0) &&
          editor.node.openTagString($x.get(0)) === editor.node.openTagString($p.get(0))
        ) {
          $x.prepend($p.html());
          $p.remove();
        }
      } // Check if we have span(font-size) inside span(background-color).
      // Then, make a split.

      editor.$el.find('span[style] span[style]').each(function () {
        if ($(this).attr('style').indexOf('font-size') >= 0) {
          var $parent = $(this).parents('span[style]'); // https://github.com/froala-labs/froala-editor-js-2/issues/3406

          if ($parent.attr('style') && $parent.attr('style').indexOf('background-color') >= 0) {
            $(this).attr(
              'style',
              ''.concat($(this).attr('style'), ';').concat($parent.attr('style'))
            );

            _split($(this), 'span[style]', {}, false);
          }
        }
      });
      editor.el.normalize();
      editor.selection.restore();
    }
    /**
     * Remove inline style.
     */

    function removeStyle(prop) {
      applyStyle(prop, null);
    }
    /**
     * Get the current state.
     */

    function is(tag, attrs) {
      if (typeof attrs === 'undefined') {
        attrs = {};
      }

      if (attrs.style) {
        delete attrs.style;
      }

      var range = editor.selection.ranges(0);
      var el = range.startContainer;

      if (el.nodeType === Node.ELEMENT_NODE) {
        // Search for node deeper.
        if (el.childNodes.length > 0 && el.childNodes[range.startOffset]) {
          el = el.childNodes[range.startOffset];
        }
      } // If we are at the end of text node, then check next elements.

      if (
        !range.collapsed &&
        el.nodeType === Node.TEXT_NODE &&
        range.startOffset === (el.textContent || '').length
      ) {
        while (!editor.node.isBlock(el.parentNode) && !el.nextSibling) {
          el = el.parentNode;
        }

        if (el.nextSibling) {
          el = el.nextSibling;
        }
      } // Check first childs.

      var f_child = el;

      while (
        f_child &&
        f_child.nodeType === Node.ELEMENT_NODE &&
        !_matches(f_child, _query(tag, attrs))
      ) {
        f_child = f_child.firstChild;
      }

      if (
        f_child &&
        f_child.nodeType === Node.ELEMENT_NODE &&
        _matches(f_child, _query(tag, attrs))
      ) {
        return true;
      } // Check parents.

      var p_node = el;

      if (p_node && p_node.nodeType !== Node.ELEMENT_NODE) {
        p_node = p_node.parentNode;
      }

      while (
        p_node &&
        p_node.nodeType === Node.ELEMENT_NODE &&
        p_node !== editor.el &&
        !_matches(p_node, _query(tag, attrs))
      ) {
        p_node = p_node.parentNode;
      }

      if (
        p_node &&
        p_node.nodeType === Node.ELEMENT_NODE &&
        p_node !== editor.el &&
        _matches(p_node, _query(tag, attrs))
      ) {
        return true;
      }

      return false;
    }

    return {
      is: is,
      toggle: toggle,
      apply: apply,
      remove: remove,
      applyStyle: applyStyle,
      removeStyle: removeStyle,
    };
  };
});

Just import this file wherever you are initializing the Froala editor.

We didn't yet see any negative side effects. What do you think?

manigandan-ravi commented 6 months ago

Hey @malsieb just out of curiosity, how did you find/extract this code from Froala repository, I have a different issue for which i wasn't able to find a patch. I know which release has the fix but can't see sub commits and was only able to find the minified files.

It will be really helpful if you can walkthrough on how you arrived at this solution. Thanks in advance.