Masha Baranova
Masha Baranova

Reputation: 41

Quill.js removes tags like "DIV" and styles and classes

I am working on WYSIWIG editor, where I can update HTML in special popup and insert it back into Quill editor as a semantic HTML. The resulting HTML should be exactly like we edited. But when I try to change tags to "div", they are turned to "p" and classes are removed.

Here is my example: https://codepen.io/Maria2/pen/PwYWVKY

class Dom {
  constructor(selector) {
    this.$el =
      typeof selector === "string"
        ? document.querySelector(selector)
        : selector;
  }

  html(html) {
    if (typeof html === "string") {
      this.$el.innerHTML = html;
      return this;
    }

    return this.$el.outerHTML.trim();
  }

  innerHtml(html) {
    if (typeof html === "string") {
      this.$el.innerHTML = html;
      return this;
    }

    return this.$el.innerHTML.trim();
  }

  text(text) {
    if (typeof text !== "undefined") {
      this.$el.textContent = text;
      return this;
    }

    if (this.$el.tagName.toLowerCase() === "input") {
      return this.$el.value.trim();
    }

    return this.$el.textContent;
  }

  clear() {
    this.html("");
    return this;
  }

  on(eventType, callback) {
    this.$el.addEventListener(eventType, callback);
  }

  off(eventType, callback) {
    this.$el.removeEventListener(eventType, callback);
  }

  append(node) {
    if (node instanceof Dom) {
      node = node.$el;
    }

    if (Element.prototype.append) {
      this.$el.append(node);
    } else {
      this.$el.appendChild(node);
    }
  }

  closest(selector) {
    return domElt(this.$el.closest(selector));
  }

  getCoords() {
    return this.$el.getBoundingClientRect();
  }

  get data() {
    return this.$el.dataset;
  }

  findAll(selector) {
    return this.$el.querySelectorAll(selector);
  }

  css(styles = {}) {
    Object.keys(styles).forEach((key) => {
      this.$el.style[key] = styles[key];
    });
    return this.$el;
  }

  getStyles(styles = []) {
    return styles.reduce((res, s) => {
      res[s] = this.$el.style[s];
      return res;
    }, {});
  }

  find(selector) {
    return domElt(this.$el.querySelector(selector));
  }

  addClass(className) {
    this.$el.classList.add(className);
    return this;
  }

  removeClass(className) {
    this.$el.classList.remove(className);
    return this;
  }

  attr(name, value) {
    if (value) {
      this.$el.setAttribute(name, value);
      return this;
    }

    return this.$el.getAttribute(name);
  }

  focus() {
    this.$el.focus();
    if (
      typeof window.getSelection !== "undefined" &&
      typeof document.createRange !== "undefined"
    ) {
      const range = document.createRange();
      range.selectNodeContents(this.$el);
      range.collapse(false);
      const sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    } else if (typeof document.body.createTextRange !== "undefined") {
      const textRange = document.body.createTextRange();
      textRange.moveToElementText(this.$el);
      textRange.collapse(false);
      textRange.select();
    }

    return this;
  }

  destroy() {
    this.$el.remove();
    this.$el = null;
  }
}

function domElt(selector) {
  return new Dom(selector);
}

domElt.create = (tagName, classes = "") => {
  const el = document.createElement(tagName);
  if (classes) {
    el.classList.add(classes);
  }

  return domElt(el);
};

class PopupElt {
  constructor({ togglerClass, beforeOpenCallback, onOpenCallback,onCloseCallback, additionalClass }) {
    this.togglerClass = togglerClass;
    this.additionalClass = additionalClass;
    this.popup = null;
    this.overlay = null;
    this.closeBtn = null;
    this.innerBlock = null;
    this.onCloseCallback = null;
    this.beforeOpenCallback = beforeOpenCallback;
    this.onOpenCallback = onOpenCallback;
    this.onCloseCallback = onCloseCallback;
    this.clickHandler = this.clickHandler.bind(this);
    this.closeHandler = this.closeHandler.bind(this);
  }

  clickHandler(evt) {
    evt.preventDefault();
    this.start();
  }

  start() {
    if (this.beforeOpenCallback) this.beforeOpenCallback(this);
    this.createOverlay();
    this.createPopup();
    if (this.onOpenCallback) this.onOpenCallback(this);
    this.setCloseListeners();
  }

  closeHandler(evt) {
    evt.preventDefault();
    this.closePopup();
  }

  closePopup() {
    this.removeCloseListeners();
    this.closeBtn.destroy();
    this.innerBlock.destroy();
    this.popup.destroy();
    if (this.overlay) this.overlay.destroy();
    if (this.onCloseCallback) this.onCloseCallback()
  }

  setCloseListeners() {
    this.overlay.on('click', this.closeHandler);
    this.closeBtn.on('click', this.closeHandler);
  }

  removeCloseListeners() {
    this.overlay.off('click', this.closeHandler);
    this.closeBtn.off('click', this.closeHandler);
  }

  createOverlay() {
    this.overlay = domElt.create('div', 'overlay');
    domElt(document.body).append(this.overlay);
  }

  createPopup() {
    let wrapperElt = domElt.create('div', 'popup__wrapper');
    this.innerBlock = domElt.create('div', 'popup__inside');
    this.popup = domElt.create('div', 'popup');
    this.closeBtn = domElt.create('button', 'btn')
      .addClass('popup__close')
      .attr('ariaLabel', 'закрыть попап')
      .html(`<svg fill="none" width="20" height="20" id="cross" viewBox="0 0 22 22"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16.303 5.697 5.697 16.303M5.697 5.697l10.606 10.606"></path></svg>`)
    wrapperElt.append(this.closeBtn);
    wrapperElt.append(this.innerBlock);
    if (this.additionalClass) this.popup.addClass(this.additionalClass);
    this.popup.append(wrapperElt);
    domElt(document.body).append(this.popup);
  }

  async setInnerHtml(innerHtml) {
    await this.innerBlock.html(innerHtml)
  }
}

const options = (id) => ({
  debug: "info",
  modules: {
    toolbar: `#toolbar${id}`,
  },
  placeholder: "Compose an epic...",
  theme: "snow",
});

class QuillEditorClass {
  constructor({ item, options }) {
    this.item = item;
    this.popup = null;
    this.id = null;
    this.toolbarBlock = null;
    this.codeBtn = null;
    this.options = options || {};
    this.currentOptions = null;
    this.codeButton = null;
    this.saveCodeBtn = null;
    this.saveBtn = null;
    this.quill = null;
    this.textareaBlock = null;
    this.openPopup = this.openPopup.bind(this);
    this.getQuillInfo = this.getQuillInfo.bind(this);
    this.saveQuill = this.saveQuill.bind(this);
    this.setQuillInfo = this.setQuillInfo.bind(this);
    this.init();
  }

  init() {
    if (!this.item) return;
    this.id = this.item.dataset.editor;
    this.currentOptions = this.options(this.id)
    this.toolbarBlock = domElt(`#toolbar${this.id}`);
    this.quill = new Quill(this.item, this.currentOptions);
    this.codeButton = this.toolbarBlock.find('.code-button');
    this.saveBtn = this.item.parentElement.querySelector('.saveQuill')
    this.setListeners();
  }

  setListeners() {
    if (this.codeButton) this.codeButton.on('click', this.openPopup);
    if (this.saveBtn) this.saveBtn.addEventListener('click', this.saveQuill);
  }

  beforeOpenCallback(quillContent) {
    const insidePopupBlock = domElt.create('div', 'popup__textarea-wrapper');
    this.textareaBlock = domElt.create('textarea', 'popup__textarea');
    this.textareaBlock.attr('name', `#textarea${this.id}`)
    this.saveCodeBtn = domElt.create('button');
    this.saveCodeBtn.attr('class', 'btn btn--blue btn--lg');
    this.saveCodeBtn.text('Сохранить');
    this.textareaBlock.text(quillContent)
    insidePopupBlock.append(this.textareaBlock)
    insidePopupBlock.append(this.saveCodeBtn)
    return insidePopupBlock;
  }

  onCloseCallback() {
    this.saveCodeBtn.off('click', this.setQuillInfo);
    this.saveCodeBtn = null;
    this.textareaBlock = null;
  }

  openPopup(evt) {
    const quillContent = this.getQuillInfo(evt);
    this.popup = new PopupElt({
      togglerClass: `#toolbar${this.id}`,
      onOpenCallback: (popup) => {
        popup.innerBlock.append(this.beforeOpenCallback(quillContent).$el) ;
      },
      onCloseCallback: null,
      additionalClass: 'popup--editor'
    });
    this.popup.start();
    this.saveCodeBtn.on('click', this.setQuillInfo)
  }

  setQuillInfo(evt) {
    evt.preventDefault();
    console.log(this.textareaBlock.$el.value,123)
    var delta = this.quill.clipboard.convert({html: this.textareaBlock.$el.value});
    this.quill.setContents(delta);
    this.popup.closePopup();
  }

  getQuillInfo(evt) {
    evt.preventDefault();
    const length = this.quill.getLength();
    const html = this.quill.getSemanticHTML(0, length);
    const delta = this.quill.getContents();
    console.log(delta);
    console.log(html, 'html');
    return html;
  }

  saveQuill(evt) {
    evt.preventDefault();
    const length = this.quill.getLength();
    const html = this.quill.getSemanticHTML(0, length);
    const delta = this.quill.getContents();
    console.log(delta);
    console.log(html, 'html');
    return html;
  }
}


const startQuill = () => {
  const allQuills = document.querySelectorAll('[data-role="editor"]');

  if (allQuills.length) {
    allQuills.forEach((item) => {
      console.log(item)
      new QuillEditorClass({ item, options });
    });
  }
};

startQuill()
:root {
  --grey_2: #b5b5b5;;
  --padding: 10px;
  --white: #fff;
  --blue: #04043c;;
}
.label {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.label__wrapper {
  display: flex;
  width: 100%;
  position: relative;
  flex-wrap: wrap;
}

.label__wrapper--quill {
  padding-top: 0;
  flex-direction: column;
  flex-wrap: nowrap;  
  border-radius: 10px;
}

 .ql-toolbar {
    border-radius: 10px 10px 0 0;
    background-color: var(--grey_2);
  }

  .ql-container {
    margin-bottom: 10px;
    border-radius: 0 0 10px 10px;
    background-color: var(--grey_2);
  }

  .ql-editor {
    min-height: 200px;

    img {
      width: auto;
    }
  }

  .saveQuill {
    margin-right: auto;
  }

.label__wrapper--row{
  gap: 20px;
  flex-wrap: nowrap;
}

.popup {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 13;
  width: calc(100% - 2 * var(--padding));
  max-width: 520px;
  border-radius: 10px;
  overflow: hidden;
  transform: translate(-50%, -50%);
}

.popup--imgs {
  max-width: 1440px;
}

.popup__textarea {
  padding: 8px;
  width: 100%;
  border-radius: 10px;
  border: 1px solid var(--grey_2);
}

.popup__textarea-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  gap: 14px;
}

.popup__wrapper {
  padding: 30px 20px 30px 20px;
  background-color: var(--white);
  max-height: calc(100svh - 2*var(--padding));
  overflow: hidden;
  overflow-y: auto;
  scrollbar-width: thin;
  box-sizing: border-box;
}

.popup__inside {
  position: relative;
  width: 100%;
}

.panel {
  width: 100%;
  height: 100%;
  overflow-y: auto;
}

.panel__name {
  margin-top: 0;
  margin-bottom: 34px;
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
}

.popup__close {
  position: absolute;
  top: 10px;
  right: 10px;
  transition: opacity 0.3s;
}

.popup__close:hover {
    opacity: 0.8;
  }

.panel__form {
  width: 100%;
}

.btn {
  display: flex;
  padding: 0;
  margin: 0;
  appearance: none;
  text-decoration: none;
  background-color: transparent;
  border: none;
  cursor: pointer;
  box-sizing: border-box;  
}

.btn svg {
    flex-shrink: 0;
}

.btn--lg {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 9px 18px;
  font-weight: 700;
}

.btn--blue {
  margin-right: auto;
  color: var(--white);
  background-color: var(--blue);
  border: 2px solid var(--blue);
  border-radius: 10px;
  transition: opacity 0.3s;
}
<div class="label__wrapper label__wrapper--quill">
    <div id="toolbar1">
      <!-- Add buttons as you would before -->
      <button class="ql-bold"></button>
      <button class="ql-italic"></button>  
      <select class="ql-size">
        <option value="small"></option>
        <!-- Note a missing, thus falsy value, is used to reset to default -->
        <option selected></option>
        <option value="large"></option>
        <option value="huge"></option>
      </select>
      <!-- Add a bold button -->
      <button class="ql-bold"></button>
      <!-- Add subscript and superscript buttons -->
      <button class="ql-script" value="sub"></button>
      <button class="ql-script" value="super"></button>
      <!-- But you can also add your own -->
      <button class="code-button">code</button>
    </div>
    <div rows="5" data-role="editor" data-editor="1"></div>
    <button class="btn btn--blue btn--lg saveQuill">Сохранить текст</button>
  </div>

Button "code" initiates popup? where you can try to edit HTML

Upvotes: 0

Views: 35

Answers (0)

Related Questions