slab / quill

Quill is a modern WYSIWYG editor built for compatibility and extensibility
https://quilljs.com
BSD 3-Clause "New" or "Revised" License
42.93k stars 3.34k forks source link

How to insert editable images and videos? #3987

Open vrrobz opened 7 months ago

vrrobz commented 7 months ago

Please describe the a concise description and fill out the details below. It will help others efficiently understand your request and get to an answer instead of repeated back and forth. Providing a minimal, complete and verifiable example will further increase your chances that someone can help.

Steps for Reproduction

  1. Insert an image
  2. Try to update its details
  3. Fail

Expected behavior:

  1. insert an image
  2. click on it to change details
  3. Get some kind of form to update details (preferably a custom callback)
  4. Save
  5. See updated image in editor

Actual behavior: You can;t edit images once placed.

Platforms:

All

Include browser, operating system and respective versions

Version: 2.0 - Develop branch

Run Quill.version to find out

All that out of the way...

I am trying to create a user-friendly WYSIWYG editor for my CMS. Of all the options - and I have tried way too many to be still called sane - Quill is the closest I can get to working. And I'm still failing.

All I really need outside of the standard functionality is the ability to use my custom asset / image picker for images and videos and to use my custom link picker for link URLs - the rest can be stock Quill.

I got to the point where I am attaching my custom image callback to any images inside the editor container so that clicking calls it up - awesome. However, I am currently editing the HTMLElement for the image directly. That seems to really mess the editor up, leaving images that can;t be deleted in the editor, which is a terrible user experience. I suspect that's because I'm updating the elemnt and not its Parchment blot or whatever, but I can not seem to make heads or tails of this thing - how can I update a BlockEmbed blot's attributes and have that reflected int he editor? Do I need to delete the old blot and replace with it a new one with updated attributes? If so, how the hell do I do that?

Here's the code I use to set up the editor:

        let BlockEmbed = Editor.import('blots/block/embed');

        class ImageBlot extends BlockEmbed {
            static create(value) {
                let node = super.create();
                node.setAttribute('alt', value.alt);
                node.setAttribute('src', value.src);
                node.setAttribute('width', value.width);
                node.setAttribute('height', value.height);
                return node;
            }

            static value(node) {
                return {
                    alt: node.getAttribute('alt'),
                    src: node.getAttribute('src'),
                    width: node.getAttribute('width'),
                    height: node.getAttribute('height')
                };
            }
        }

        ImageBlot.blotName = 'image';
        ImageBlot.tagName = 'img';

        Editor.register(ImageBlot);

        var editor = new Editor(el, {
            modules: {
                toolbar: toolbarOptions
            },
            theme: 'snow'
        })

        var editBox = editor.root
        var images = editBox.getElementsByTagName('img')

        for(let n = 0; n < images.length; n++) {
            images[n].addEventListener('click', (event) => { imgHandler(false, event.currentTarget, editor)})
        }

        var toolbar = editor.getModule("toolbar");

        toolbar.addHandler("image", (i, cb) => { imgHandler(i, cb, editor) });

        return editor;

Here's my imgHandler - all it really does is grab the currently selected element or location and open a form that allows a user to select an image from a list, which then uses a callback to populate the image with those attributes.

        var index;
        var imgElement = '';
        var imgObj = {}

        if(typeof value == 'string') {
            console.log("It's a string!")
            //We can assume the value is an href; put that in the image form fields
            imgObj["href"] = value
        } else if(value) {

            if(value.nodeType) {
                let quillImg = Editor.find(value);
                currImage = value

                imgObj.width = ((value.getAttribute("width") !== null && value.getAttribute("width") != 'null') ? value.getAttribute("width") : '')
                imgObj.height = ((value.getAttribute("height") !== null && value.getAttribute("height") != 'null') ? value.getAttribute("height") : '')
                imgObj.alt = ((value.getAttribute("alt") !== null && value.getAttribute("alt") != 'null') ? value.getAttribute("alt") : '')
                imgObj.src = ((value.getAttribute("src") !== null && value.getAttribute("src") != 'null')  ? value.getAttribute("src") : '')

            } else if(value.href) {
                console.log("It's an object!")
                //It's an object in the format {"href": link_url, "alt": alt_text, "width": width, "height": height}
                //TODO something
                //Put the values in the object into the image form for editing
            } else {
                console.log("It's... " + typeof value)
                //Doing nothing for now
            }
        } else {
            console.log("Value is " + value + ": " + typeof value)
            //Nothing to do here
        }

        document.getElementById("image_width").value = (imgObj.width && imgObj.width !== null ? imgObj.width : '');
        document.getElementById("image_height").value = (imgObj.height && imgObj.height !== null ? imgObj.height : '');
        document.getElementById("image_alt").value = (imgObj.alt && imgObj.alt !== null ? imgObj.alt : '');
        document.getElementById("src").value = (imgObj.src && imgObj.src !== null ? imgObj.src : '');

And here's the callback that's actually trying to embed the image in the editor:

        let range = currEditor.getSelection(true);
        if(currImage !== null) {

            /*
            //This is what I *was* doing - it caused images to not be able to be deleted from the editor.

            if(imageObj.src && imageObj.src != '') {
                currImage.setAttribute("src",  imageObj.src)
            }
            if(imageObj.src && imageObj.src != '') {
                currImage.setAttribute("width",  imageObj.width)
            }
            if(imageObj.src && imageObj.src != '') {
                currImage.setAttribute("height",  imageObj.height)
            }
            if(imageObj.src && imageObj.src != '') {
                currImage.setAttribute("alt",  imageObj.alt)
            }
            */

            var quillImg = Editor.find(currImage)
            quillImg.format('image', imageObj)
            currImage = null;
        } else {
            currEditor.insertText(range.index, '\n', Editor.sources.USER);
            currEditor.insertEmbed(range.index + 1, 'image', imageObj, Editor.sources.USER);
        }
        currEditor.setSelection(range.index + 2, Editor.sources.SILENT);
        currEditor = null
        currImage = null

        var modal = document.getElementById("image_picker_modal")
        modal.close()

Help?

dodgydre commented 6 months ago

I had been using quill-image-resize for a while but wanted to add rotation as well so I modified that package a bit.. can't remember exactly where I modified it.. I've been trying to make vuequill work with quill 2 so i've got a lot of packages taken offline since they don't seem to be maintained any more..

Modified from this one: https://github.com/kensnyder/quill-image-resize-module/tree/master/src

I have the alignment functions commented out in the Toolbar.js file since I never used those.. I also added in the bit that adds 1 pixel to the image.width on the two rotate functions since rotations weren't writing out unless I resized the image afterwards or clicked outsize and added a space or changed some text.. modifying the width seemed to make it save the rotation properly..

It's a hack but it works for me at the moment.. :) A lot of quill modules don't seem to be very active.

ImageResize.js

import defaultsDeep from "lodash/defaultsDeep";
import DefaultOptions from "./DefaultOptions";
import { DisplaySize } from "./modules/DisplaySize";
import { Toolbar } from "./modules/Toolbar";
import { Resize } from "./modules/Resize";

const knownModules = { DisplaySize, Toolbar, Resize };

/**
 * Custom module for quilljs to allow user to resize <img> elements
 * (Works on Chrome, Edge, Safari and replaces Firefox's native resize behavior)
 * @see https://quilljs.com/blog/building-a-custom-module/
 */
export default class ImageResize {
  constructor(quill, options = {}) {
    // save the quill reference and options
    this.quill = quill;

    // Apply the options to our defaults, and stash them for later
    // defaultsDeep doesn't do arrays as you'd expect, so we'll need to apply the classes array from options separately
    let moduleClasses = false;
    if (options.modules) {
      moduleClasses = options.modules.slice();
    }

    // Apply options to default options
    this.options = defaultsDeep({}, options, DefaultOptions);

    // (see above about moduleClasses)
    if (moduleClasses !== false) {
      this.options.modules = moduleClasses;
    }

    // disable native image resizing on firefox
    document.execCommand("enableObjectResizing", false, "false");

    // respond to clicks inside the editor
    this.quill.root.addEventListener("click", this.handleClick, false);

    this.quill.root.parentNode.style.position =
      this.quill.root.parentNode.style.position || "relative";

    // setup modules
    this.moduleClasses = this.options.modules;

    this.modules = [];
  }

  initializeModules = () => {
    this.removeModules();

    this.modules = this.moduleClasses.map(
      (ModuleClass) => new (knownModules[ModuleClass] || ModuleClass)(this)
    );

    this.modules.forEach((module) => {
      module.onCreate();
    });

    this.onUpdate();
  };

  onUpdate = () => {
    this.repositionElements();
    this.modules.forEach((module) => {
      module.onUpdate();
    });
  };

  removeModules = () => {
    this.modules.forEach((module) => {
      module.onDestroy();
    });

    this.modules = [];
  };

  handleClick = (evt) => {
    if (
      evt.target &&
      evt.target.tagName &&
      evt.target.tagName.toUpperCase() === "IMG"
    ) {
      if (this.img === evt.target) {
        // we are already focused on this image
        return;
      }
      if (this.img) {
        // we were just focused on another image
        this.hide();
      }
      // clicked on an image inside the editor
      this.show(evt.target);
    } else if (this.img) {
      // clicked on a non image
      this.hide();
    }
  };

  show = (img) => {
    // keep track of this img element
    this.img = img;

    this.showOverlay();

    this.initializeModules();
  };

  showOverlay = () => {
    if (this.overlay) {
      this.hideOverlay();
    }

    // BUG: scroll top when setSelection method is called
    this.quill.setSelection(null);

    // prevent spurious text selection
    this.setUserSelect("none");

    // listen for the image being deleted or moved
    document.addEventListener("keyup", this.checkImage, true);
    this.quill.root.addEventListener("input", this.checkImage, true);

    // Create and add the overlay
    this.overlay = document.createElement("div");
    Object.assign(this.overlay.style, this.options.overlayStyles);

    this.quill.root.parentNode.appendChild(this.overlay);

    this.repositionElements();
  };

  hideOverlay = () => {
    if (!this.overlay) {
      return;
    }

    // Remove the overlay
    this.quill.root.parentNode.removeChild(this.overlay);
    this.overlay = undefined;

    // stop listening for image deletion or movement
    document.removeEventListener("keyup", this.checkImage);
    this.quill.root.removeEventListener("input", this.checkImage);

    // reset user-select
    this.setUserSelect("");
  };

  repositionElements = () => {
    if (!this.overlay || !this.img) {
      return;
    }

    // position the overlay over the image
    const parent = this.quill.root.parentNode;
    const img = this.img;
    const imgRect = this.img.getBoundingClientRect();
    const containerRect = parent.getBoundingClientRect();

    const imgStyle = this.getImageStyle(
      img.height,
      img.width,
      imgRect.height,
      imgRect.width
    );
    Object.assign(this.img.style, imgStyle);

    setTimeout(() => {
      const rotation = +img.getAttribute("_rotation") || 0;
      const imgRect2 = img.getBoundingClientRect();
      const overlayStyle = this.getOverlayStyle(
        rotation,
        img.width,
        img.height,
        imgRect2.left,
        imgRect2.top,
        containerRect.left,
        containerRect.top,
        parent.scrollLeft,
        parent.scrollTop
      );
      console.log(overlayStyle);
      Object.assign(this.overlay.style, overlayStyle);
    }, 30);

    // Object.assign(this.overlay.style, {
    //  left: `${imgRect.left - containerRect.left - 1 + parent.scrollLeft}px`,
    //  top: `${imgRect.top - containerRect.top + parent.scrollTop}px`,
    //  width: `${imgRect.width}px`,
    //  height: `${imgRect.height}px`,
    // });
  };

  getImageStyle = (imgH, imgW, imgRectH, imgRectW) => {
    const offsetX = (imgRectW - imgW) / 2;
    const offsetY = (imgRectH - imgH) / 2;
    const styles = {
      margin: `${offsetY}px ${offsetX}px`,
    };
    return styles;
  };

  getOverlayStyle = (
    rotation,
    imgW,
    imgH,
    imgRectL,
    imgRectT,
    cRectL,
    cRectT,
    pScrollL,
    pScrollT
  ) => {
    const styles = {};
    switch (rotation) {
      case 90:
      case 270:
        styles.width = `${imgH}px`;
        styles.height = `${imgW}px`;
        styles.left = `${imgRectL - cRectL - 1 + pScrollL}px`;
        styles.top = `${imgRectT - cRectT + pScrollT}px`;
        break;
      case 180:
      case 0:
      default:
        styles.width = `${imgW}px`;
        styles.height = `${imgH}px`;
        styles.left = `${imgRectL - cRectL - 1 + pScrollL}px`;
        styles.top = `${imgRectT - cRectT + pScrollT}px`;
    }
    return styles;
  };

  hide = () => {
    this.hideOverlay();
    this.removeModules();
    this.img = undefined;
  };

  setUserSelect = (value) => {
    ["userSelect", "mozUserSelect", "webkitUserSelect", "msUserSelect"].forEach(
      (prop) => {
        // set on contenteditable element and <html>
        this.quill.root.style[prop] = value;
        document.documentElement.style[prop] = value;
      }
    );
  };

  checkImage = (evt) => {
    if (this.img) {
      if (evt.keyCode == 46 || evt.keyCode == 8) {
        this.quill.find(this.img).deleteAt(0);
      }
      this.hide();
    }
  };
}

DefaultOptions.js

export default {
  modules: ["DisplaySize", "Toolbar", "Resize"],
  overlayStyles: {
    position: "absolute",
    boxSizing: "border-box",
    border: "1px dashed #444",
  },
  handleStyles: {
    position: "absolute",
    height: "12px",
    width: "12px",
    backgroundColor: "white",
    border: "1px solid #777",
    boxSizing: "border-box",
    opacity: "0.80",
  },
  displayStyles: {
    position: "absolute",
    font: "12px/1.0 Arial, Helvetica, sans-serif",
    padding: "4px 8px",
    textAlign: "center",
    backgroundColor: "white",
    color: "#333",
    border: "1px solid #777",
    boxSizing: "border-box",
    opacity: "0.80",
    cursor: "default",
  },
  toolbarStyles: {
    position: "absolute",
    top: "-12px",
    right: "0",
    left: "0",
    height: "0",
    minWidth: "100px",
    font: "12px/1.0 Arial, Helvetica, sans-serif",
    textAlign: "center",
    color: "#333",
    boxSizing: "border-box",
    cursor: "default",
  },
  toolbarButtonStyles: {
    display: "inline-block",
    width: "24px",
    height: "24px",
    padding: "3px",
    background: "white",
    border: "1px solid #999",
    verticalAlign: "middle",
  },
  toolbarButtonSvgStyles: {
    fill: "#444",
    stroke: "#444",
    strokeWidth: "2",
  },
};

modules/baseModule.js

export class BaseModule {
    constructor(resizer) {
        this.overlay = resizer.overlay;
        this.img = resizer.img;
        this.options = resizer.options;
        this.requestUpdate = resizer.onUpdate;
    }
    /*
        requestUpdate (passed in by the library during construction, above) can be used to let the library know that
        you've changed something about the image that would require re-calculating the overlay (and all of its child
        elements)

        For example, if you add a margin to the element, you'll want to call this or else all the controls will be
        misaligned on-screen.
     */

    /*
        onCreate will be called when the element is clicked on

        If the module has any user controls, it should create any containers that it'll need here.
        The overlay has absolute positioning, and will be automatically repositioned and resized as needed, so you can
        use your own absolute positioning and the 'top', 'right', etc. styles to be positioned relative to the element
        on-screen.
     */
    onCreate = () => {};

    /*
        onDestroy will be called when the element is de-selected, or when this module otherwise needs to tidy up.

        If you created any DOM elements in onCreate, please remove them from the DOM and destroy them here.
     */
    onDestroy = () => {};

    /*
        onUpdate will be called any time that the element is changed (e.g. resized, aligned, etc.)

        This frequently happens during resize dragging, so keep computations light while here to ensure a smooth
        user experience.
     */
    onUpdate = () => {};
}

DisplaySize.js (this is if you want the size of the image displayed on an overlay during resizing)

import { BaseModule } from './BaseModule';

export class DisplaySize extends BaseModule {
    onCreate = () => {
        // Create the container to hold the size display
        this.display = document.createElement('div');

        // Apply styles
        Object.assign(this.display.style, this.options.displayStyles);

        // Attach it
        this.overlay.appendChild(this.display);
    };

    onDestroy = () => {};

    onUpdate = () => {
        if (!this.display || !this.img) {
            return;
        }

        const size = this.getCurrentSize();
        this.display.innerHTML = size.join(' &times; ');
        if (size[0] > 120 && size[1] > 30) {
            // position on top of image
            Object.assign(this.display.style, {
                right: '4px',
                bottom: '4px',
                left: 'auto',
            });
        }
        else if (this.img.style.float == 'right') {
            // position off bottom left
            const dispRect = this.display.getBoundingClientRect();
            Object.assign(this.display.style, {
                right: 'auto',
                bottom: `-${dispRect.height + 4}px`,
                left: `-${dispRect.width + 4}px`,
            });
        }
        else {
            // position off bottom right
            const dispRect = this.display.getBoundingClientRect();
            Object.assign(this.display.style, {
                right: `-${dispRect.width + 4}px`,
                bottom: `-${dispRect.height + 4}px`,
                left: 'auto',
            });
        }
    };

    getCurrentSize = () => [
        this.img.width,
        Math.round((this.img.width / this.img.naturalWidth) * this.img.naturalHeight),
    ];
}

Resize.js

import { BaseModule } from "./BaseModule";

export class Resize extends BaseModule {
  onCreate = () => {
    // track resize handles
    this.boxes = [];

    // add 4 resize handles
    this.addBox("nwse-resize"); // top left
    this.addBox("nesw-resize"); // top right
    this.addBox("nwse-resize"); // bottom right
    this.addBox("nesw-resize"); // bottom left

    this.positionBoxes();
  };

  onDestroy = () => {
    // reset drag handle cursors
    this.setCursor("");
  };

  positionBoxes = () => {
    const handleXOffset = `${
      -parseFloat(this.options.handleStyles.width) / 2
    }px`;
    const handleYOffset = `${
      -parseFloat(this.options.handleStyles.height) / 2
    }px`;

    // set the top and left for each drag handle
    [
      { left: handleXOffset, top: handleYOffset }, // top left
      { right: handleXOffset, top: handleYOffset }, // top right
      { right: handleXOffset, bottom: handleYOffset }, // bottom right
      { left: handleXOffset, bottom: handleYOffset }, // bottom left
    ].forEach((pos, idx) => {
      Object.assign(this.boxes[idx].style, pos);
    });
  };

  addBox = (cursor) => {
    // create div element for resize handle
    const box = document.createElement("div");

    // Star with the specified styles
    Object.assign(box.style, this.options.handleStyles);
    box.style.cursor = cursor;

    // Set the width/height to use 'px'
    box.style.width = `${this.options.handleStyles.width}px`;
    box.style.height = `${this.options.handleStyles.height}px`;

    // listen for mousedown on each box
    box.addEventListener("touchstart", this.handleMousedown, {
      passive: true,
    });
    box.addEventListener("mousedown", this.handleMousedown, { passive: true });
    box.addEventListener("touchstart", this.handleMousedown, { passive: true });
    // add drag handle to document
    this.overlay.appendChild(box);
    // keep track of drag handle
    this.boxes.push(box);
  };

  handleMousedown = (evt) => {
    // note which box
    this.dragBox = evt.target;
    // note starting mousedown position
    if (evt.touches) {
      // for mobile devices get clientX of first touch point
      this.dragStartX = evt.touches[0].clientX;
    } else {
      this.dragStartX = evt.clientX;
    }
    // if (evt.type === 'mousedown') {
    //  this.dragStartX = evt.clientX
    // } else {
    //  this.dragStartX = evt.touches[0].clientX
    // }

    // store the width before the drag
    this.preDragWidth = this.img.width || this.img.naturalWidth;
    // set the proper cursor everywhere
    this.setCursor(this.dragBox.style.cursor);
    // listen for movement and mouseup
    document.addEventListener("touchmove", this.handleDrag, { pssive: true });
    document.addEventListener("touchend", this.handleMouseup, { pssive: true });
    document.addEventListener("mousemove", this.handleDrag, { pssive: true });
    document.addEventListener("mouseup", this.handleMouseup, { pssive: true });
  };

  handleMouseup = () => {
    // reset cursor everywhere
    this.setCursor("");
    // stop listening for movement and mouseup
    document.removeEventListener("touchmove", this.handleDrag);
    document.removeEventListener("touchend", this.handleMouseup);
    document.removeEventListener("mousemove", this.handleDrag);
    document.removeEventListener("mouseup", this.handleMouseup);
  };

  handleDrag = (evt) => {
    if (!this.img) {
      // image not set yet
      return;
    }
    // update image size
    let deltaX;
    if (evt.touches) {
      deltaX = evt.touches[0].clientX - this.dragStartX;
    } else {
      deltaX = evt.clientX - this.dragStartX;
    }
    // let deltaX
    // if (evt.type === 'mousemove') {
    //     deltaX = evt.clientX - this.dragStartX
    // } else {
    //     deltaX = evt.changedTouches[0].clientX - this.dragStartX
    // }

    if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[3]) {
      // left-side resize handler; dragging right shrinks image
      this.img.width = Math.round(this.preDragWidth - deltaX);
    } else {
      // right-side resize handler; dragging right enlarges image
      this.img.width = Math.round(this.preDragWidth + deltaX);
    }
    this.requestUpdate();
  };

  setCursor = (value) => {
    [document.body, this.img].forEach((el) => {
      el.style.cursor = value; // eslint-disable-line no-param-reassign
    });
  };
}

Toolbar.js (I added the rotation functionality on here under the rotate-left and rotate-right

// import Parchment from "parchment";
import { Quill } from "@vueup/vue-quill";
const Parchment = Quill.import("parchment");
import { BaseModule } from "./BaseModule";
const FloatStyle = new Parchment.Attributor.Style("float", "float");
const MarginStyle = new Parchment.Attributor.Style("margin", "margin");
const DisplayStyle = new Parchment.Attributor.Style("display", "display");
const TransformStyle = new Parchment.Attributor.Style("transform", "transform");

// const IconAlignCenter = `<svg viewbox="0 0 18 18">
//  <line class="ql-stroke" x1="15" x2="3" y1="9" y2="9"></line>
//  <line class="ql-stroke" x1="14" x2="4" y1="14" y2="14"></line>
//  <line class="ql-stroke" x1="12" x2="6" y1="4" y2="4"></line>
// </svg>`;

// const IconAlignRight = `<svg viewbox="0 0 18 18">
//  <line class="ql-stroke" x1="15" x2="3" y1="9" y2="9"></line>
//  <line class="ql-stroke" x1="15" x2="5" y1="14" y2="14"></line>
//  <line class="ql-stroke" x1="15" x2="9" y1="4" y2="4"></line>
//  </svg>`;
// const IconAlignLeft = `<svg viewbox="0 0 18 18">
// <line class="ql-stroke" x1="3" x2="15" y1="9" y2="9"></line>
// <line class="ql-stroke" x1="3" x2="13" y1="14" y2="14"></line>
// <line class="ql-stroke" x1="3" x2="9" y1="4" y2="4"></line>
// </svg>`;

const IconRedo = `<svg viewbox="0 0 18 18">
<polygon class="ql-fill ql-stroke" points="12 10 14 12 16 10 12 10"></polygon>
<path class="ql-stroke" d="M9.91,13.91A4.6,4.6,0,0,1,9,14a5,5,0,1,1,5-5"></path>
</svg>`;

const IconUndo = `<svg viewbox="0 0 18 18">
<polygon class="ql-fill ql-stroke" points="6 10 4 12 2 10 6 10"></polygon>
<path class="ql-stroke" d="M8.09,13.91A4.6,4.6,0,0,0,9,14,5,5,0,1,0,4,9"></path>
</svg>`;

export class Toolbar extends BaseModule {
  rotation = 0;

  onCreate = () => {
    // Setup Toolbar
    this.toolbar = document.createElement("div");
    Object.assign(this.toolbar.style, this.options.toolbarStyles);
    this.overlay.appendChild(this.toolbar);

    // Setup Buttons
    this._defineAlignments();
    this._addToolbarButtons();
    this.rotation = +this.img.getAttribute("_rotation") || 0;
  };

  // The toolbar and its children will be destroyed when the overlay is removed
  onDestroy = () => {};

  // Nothing to update on drag because we are are positioned relative to the overlay
  onUpdate = () => {};

  _defineAlignments = () => {
    this.alignments = [
      //   {
      //     icon: IconAlignLeft,
      //     apply: () => {
      //       DisplayStyle.add(this.img, "inline");
      //       FloatStyle.add(this.img, "left");
      //       MarginStyle.add(this.img, "0 1em 1em 0");
      //     },
      //     isApplied: () => FloatStyle.value(this.img) == "left",
      //   },
      //   {
      //     icon: IconAlignCenter,
      //     apply: () => {
      //       DisplayStyle.add(this.img, "block");
      //       FloatStyle.remove(this.img);
      //       MarginStyle.add(this.img, "auto");
      //     },
      //     isApplied: () => MarginStyle.value(this.img) == "auto",
      //   },
      //   {
      //     icon: IconAlignRight,
      //     apply: () => {
      //       DisplayStyle.add(this.img, "inline");
      //       FloatStyle.add(this.img, "right");
      //       MarginStyle.add(this.img, "0 0 1em 1em");
      //     },
      //     isApplied: () => FloatStyle.value(this.img) == "right",
      //   },
      {
        name: "rotate-left",
        icon: IconUndo,
        apply: () => {
          const rotationvalue = this._setRotation("left");
          // console.log("Rotate left!", { rotationvalue });
          // console.log(this.img);
          this.img.setAttribute("_rotation", this.rotation);
          TransformStyle.add(this.img, rotationvalue);
          this.img.width = this.img.width + 1;
          // this.requestUpdate();
        },
        isApplied: () => {},
      },
      {
        name: "rotate-right",
        icon: IconRedo,
        apply: () => {
          const rotationvalue = this._setRotation("right");
          // console.log("Rotate right!", { rotationvalue });
          this.img.setAttribute("_rotation", this.rotation);
          TransformStyle.add(this.img, rotationvalue);
          this.img.width = this.img.width + 1;
          // this.requestUpdate();
        },
        isApplied: () => {},
      },
    ];
  };

  _addToolbarButtons = () => {
    const buttons = [];
    this.alignments.forEach((alignment, idx) => {
      const button = document.createElement("span");
      buttons.push(button);
      button.innerHTML = alignment.icon;
      button.addEventListener("click", () => {
        // deselect all buttons
        buttons.forEach((button) => (button.style.filter = ""));
        if (alignment.isApplied()) {
          // If applied, unapply
          FloatStyle.remove(this.img);
          MarginStyle.remove(this.img);
          DisplayStyle.remove(this.img);
        } else {
          // otherwise, select button and apply
          this._selectButton(button);
          alignment.apply();
        }
        // image may change position; redraw drag handles
        this.requestUpdate();
      });
      Object.assign(button.style, this.options.toolbarButtonStyles);
      if (idx > 0) {
        button.style.borderLeftWidth = "0";
      }
      Object.assign(
        button.children[0].style,
        this.options.toolbarButtonSvgStyles
      );
      if (alignment.isApplied()) {
        // select button if previously applied
        this._selectButton(button);
      }
      this.toolbar.appendChild(button);
    });
  };

  _selectButton = (button) => {
    button.style.filter = "invert(20%)";
  };

  _setRotation = (direction) => {
    const oldRotation = this.rotation;
    const increment = direction == "left" ? -90 : 90;
    this.rotation = (oldRotation + 360 + increment) % 360;
    return "rotate(" + this.rotation + "deg)";
  };
}
ludejun commented 4 months ago

You can use quill-react-commercial.

You can upload, resize, add remark, delete, align iamges. But not rotate. image