quill-image-resize-module: Image align styles not being retained when they are already set

I cannot get the image styles that are set with the align to be kept when re-displaying the image inside a quill editor.

Use case:

  1. Add and right align an image in the editor.
  2. Save the editor innerHTML in a database
  3. Redisplay the edited HTML in a new quill editor to allow user to update the HTML

Expected: image in the new editor should still be aligned right. Actual: image in new editor has had all the style attributes removed

I’ve updated the demo Plunk to show the issue. In this example, I’ve added the align style attributes to the first image in the HTML (style="display: inline; float: right; margin: 0px 0px 1em 1em;"). The editor has removed them and the image is not being aligned right.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 24
  • Comments: 28

Most upvoted comments

@bdurand , you’re absolutely right! thanks!

here’s my solution:

var BaseImageFormat = Quill.import('formats/image');
const ImageFormatAttributesList = [
    'alt',
    'height',
    'width',
    'style'
];

class ImageFormat extends BaseImageFormat {
  static formats(domNode) {
    return ImageFormatAttributesList.reduce(function(formats, attribute) {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute);
      }
      return formats;
    }, {});
  }
  format(name, value) {
    if (ImageFormatAttributesList.indexOf(name) > -1) {
      if (value) {
        this.domNode.setAttribute(name, value);
      } else {
        this.domNode.removeAttribute(name);
      }
    } else {
      super.format(name, value);
    }
  }
}

Quill.register(ImageFormat, true);
                
                
var quill = new Quill('#editor', {
    theme: 'snow',
    modules: {
        imageResize: {}
    }
});

Have you tried adding 'width’ to the formats: [...] option?

Solution for Angular app (and other typeScript one), heavily inspired by xeger, MaximusBaton comments

import Quill from 'quill';

interface EmbedBlot {
  new(...args: any[]): EmbedBlot;
  domNode: any;
  format(name, value);
}

const Image: EmbedBlot = Quill.import('formats/image');
const ImageFormatAttributesList = [
  'alt',
  'height',
  'width',
  'style'
];

class StyledImage extends Image {
  static formats(domNode) {
    return ImageFormatAttributesList.reduce(function (formats, attribute) {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute);
      }
      return formats;
    }, {});
  }
  format(name, value) {
    if (ImageFormatAttributesList.indexOf(name) > -1) {
      if (value) {
        this.domNode.setAttribute(name, value);
      } else {
        this.domNode.removeAttribute(name);
      }
    } else {
      super.format(name, value);
    }
  }
}

Quill.register(StyledImage, true);

What I do to fix it is rewriting the image format. In order to sanitize style preventing from attacks such as XSS Or unexpected style, I also add white list for style. Environment: React-quill

Like this:

import {Quill} from 'react-quill';
const Parchment = Quill.import('parchment');
const BaseImage = Quill.import('formats/image');

const ATTRIBUTES = [
  'alt',
  'height',
  'width',
  'style'
];

const WHITE_STYLE = ['margin', 'display', 'float'];

class Image extends BaseImage {
  static formats(domNode) {
    return ATTRIBUTES.reduce(function(formats, attribute) {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute);
      }
      return formats;
    }, {});
  }
  
  format(name, value) {
    if (ATTRIBUTES.indexOf(name) > -1) {
      if (value) {
        if (name === 'style') {
          value = this.sanitize_style(value);
        }
        this.domNode.setAttribute(name, value);
      } else {
        this.domNode.removeAttribute(name);
      }
    } else {
      super.format(name, value);
    }
  }

  sanitize_style(style) {
    let style_arr = style.split(";")
    let allow_style = "";
    style_arr.forEach((v, i) => {
      if (WHITE_STYLE.indexOf(v.trim().split(":")[0]) !== -1) {
        allow_style += v + ";"
      }
    })
    return allow_style;
  }
}

export default Image;

@bogdaniel Had to monkeypatch the imageformat.

    ImageFormat.formats = function formats(domNode: any): any {
        return IMAGE_FORMAT_ATTRIBUTES.reduce(
            (formats, attribute) => {
                if (domNode.hasAttribute(attribute)) {
                    formats[attribute] = domNode.getAttribute(attribute);
                }
                return formats;
            },
            {}
        );
    };

    ImageFormat.prototype.format = function format(name: string, value: any): void {
        if (IMAGE_FORMAT_ATTRIBUTES.indexOf(name) !== -1) {
            if (value) {
                this.domNode.setAttribute(name, value);
            } else {
                this.domNode.removeAttribute(name);
            }
        } else {
            this.super.format(name, value);
        }
    };

    Quill.register(ImageFormat, true);

had an issue with creating a new Format so went down this route for a quickfix.

Preserving Image Styles In Quill Deltas

I have a working example in Codesandbox if this is TL;DR.

If you want to preserve image styles during Delta⇄HTML conversion, you must do three things:

  1. extend the default Quill Image blot so it can apply the new formats to the HTML DOM
  2. register width, height and float with the Parchment registry with the proper scope (INLINE_BLOT or BLOCK_BLOT depending on the superclass of your custom Image )
  3. add width, height and float to the list of allowed formats when you instantiate your Quill editor

If you only work with Quill HTML and never with delta, you can skip all of the above. If you only extend Image, then its static formats will allow the new attributes to appear in deltas, but the formats won’t be applied to the DOM. To achieve that final step of applying the formats to HTML DOM when processing a delta, the formats must be registered and allowed.

Some subtleties that you also need to pay attention to:

  • Always use Quill.import to get the Image blot, or Parchment or other Quill primitives. If you import them directly via ES6 then you will probably not get the right objects!
  • Instead of rewriting Quill’s built-in Image, it is better to import it, extend it with a derived class that calls super, and then re-register formats/image your derived class. This minimizes the code you write and preserves other customizations from Quill modules.

Feel free to contact me with any questions; I’m not a Quill expert, but after 20+ hours of debugging this issue I have a certain level of understanding!

I figured out the core issue is that there is a whitelist of allowed attributes on the image tag and style is not one of them. See https://github.com/quilljs/quill/issues/1556

@vbobroff-app @MidaAiZ I applied this solution but still the problem is arising. Image resizing and alignment that applied with style, is sanitized if I go to edit mode. How can I solve this? image

Code which I applied

// Resize module start

const ImageResize = await import('quill-image-resize-module');
Quill.register('modules/ImageResize', ImageResize);
const Parchment = Quill.import('parchment');
const BaseImage = Quill.import('formats/image');

const ATTRIBUTES = ['alt', 'height', 'width', 'style'];

const WHITE_STYLE = ['margin', 'display', 'float'];

class Image extends BaseImage {
  static formats(domNode) {
    return ATTRIBUTES.reduce((formats, attribute) => {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute);
      }
      return formats;
    }, {});
  }

  format(name, value) {
    if (ATTRIBUTES.indexOf(name) > -1) {
      if (value) {
        if (name === 'style') {
          value = this.sanitize_style(value);
        }
        this.domNode.setAttribute(name, value);
      } else {
        this.domNode.removeAttribute(name);
      }
    } else {
      super.format(name, value);
    }
  }

  sanitize_style = (style) => {
    const style_arr = style.split(';');
    let allow_style = '';
    style_arr.forEach((v) => {
      if (WHITE_STYLE.indexOf(v.trim().split(':')[0]) !== -1) {
        allow_style += `${v};`;
      }
    });
    return allow_style;
  };
}

Quill.register(Image, true);
// Resize module end

return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;

I’ve tried everything recommended in this thread but no matter how I override, extend or register ImageFormat, the overridden format(name, value) is never entered. I can break into static formats(domNode) and style is being recognized and added to the returned formats but passing html with img tags containing style attributes are still stripped of their attributes and then sent through onEditorChange.

My most recent attempt was to remove ‘image’ from formats and replace it with my custom blot… No luck

Upon setting editor value, it strips attributes.

const ParchmentEmbed = Quill.import('blots/block/embed');
console.log('>>>> ParchmentEmbed');
console.log(ParchmentEmbed);
const ATTRIBUTES = [
  'alt',
  'height',
  'width',
  'style'
];


class ImageWithStyle extends ParchmentEmbed {
  static create(value) {
    let node = super.create(value);
    if (typeof value === 'string') {
      node.setAttribute('src', this.sanitize(value));
    }
    return node;
  }

  static formats(domNode) {
    //debugger;
    return ATTRIBUTES.reduce(function(formats, attribute) {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute);
      }
      return formats;
    }, {});
  }

  static match(url) { 
    return /\.(jpe?g|gif|png)$/.test(url) || /^data:image\/.+;base64/.test(url);
  }

  static sanitize(url) {
    return url;
    //return sanitize(url, ['http', 'https', 'data']) ? url : '//:0';
  }

/*
  static value(domNode) {
    debugger;
    return domNode.getAttribute('src');
  }*/

  format(name, value) {
    debugger; // never gets hit
    if (ATTRIBUTES.indexOf(name) > -1) {
      if (value) {
        this.domNode.setAttribute(name, value);
      } else {
        this.domNode.removeAttribute(name);
      }
    } else {
      super.format(name, value);
    }
  }
}
ImageWithStyle.blotName = 'imagewithstyle';
ImageWithStyle.tagName = 'IMG';

Quill.register(ImageWithStyle, true);
Quill.register('modules/imageDrop', ImageDrop);
Quill.register('modules/imageResize', ImageResize);

...

<ReactQuill
          ref={(el) => { this.reactQuillRef = el }}
          theme='snow'
          onChange={this.handleChange}
          value={this.props.editorHtml}
          modules={Editor.modules}
          formats={Editor.formats}
          bounds={'.app'}
          placeholder='Author or Paste your contents'
         />


Editor.modules = {
  toolbar: [
    [{ 'header': '1'}, {'header': '2'}, { 'font': [] }],
    [{size: []}],
    ['bold', 'italic', 'underline', 'strike', 'blockquote'],
    [{'list': 'ordered'}, {'list': 'bullet'},
     {'indent': '-1'}, {'indent': '+1'}],
    ['link', 'image', 'video'],
    ['clean']
  ],
  clipboard: {
    // toggle to add extra line breaks when pasting HTML:
    matchVisual: false,
  },
  imageDrop: true,
  imageResize: {}
}

Editor.formats = [
  'header', 'font', 'size',
  'bold', 'italic', 'underline', 'strike', 'blockquote',
  'list', 'bullet', 'indent',
  'link', 'imagewithstyle', 'video'
]

...

If anyone can point me in the right direction, I’d really appreciate it

FYI CodePen with a working example using @MaximusBaton and @bdurand’s work